all of the changes

This commit is contained in:
2026-10-23 18:29:51 +02:00
parent 4c435fdd76
commit 5607dcf2b5
73 changed files with 4365 additions and 2059 deletions

View File

@@ -1,6 +1,8 @@
#pragma once
#include "FluidSim/Engine.hpp"
#include "FluidSim/OpenEndRadiation.hpp"
#include "FluidSim/PipeSolver.hpp"
#include <algorithm>
#include <cmath>
#include <deque>
@@ -165,10 +167,14 @@ struct GasState {
class Pipe1D {
public:
std::vector<float> linear_resistance, quadratic_resistance;
// Integrated face fluxes: kg/s, N, W, kg/s of fresh air and fuel, positive along +x.
struct Flux {
float m{}, p{}, e{}, y{}, fuel{};
};
struct RoeState { double root_density{}, enthalpy{}; };
PipeSolver solver{PipeSolver::Rusanov};
std::uint64_t hllc_fallbacks{};
float area{}, dx{};
float damping_multiplier{0.0f}, energy_relaxation_rate{0.0f}, ambient_pressure{101325.0f};
std::vector<float> cell_areas, cell_lengths;
@@ -180,7 +186,7 @@ public:
Pipe1D(std::vector<float> areas, std::vector<float> lengths)
: area(areas.empty() ? 0 : areas.front()), dx(lengths.empty() ? 0 : lengths.front()),
cell_areas(std::move(areas)), cell_lengths(std::move(lengths)), cells(cell_areas.size()),
next_cells_(cells.size()), states_(cells.size()), waves_(cells.size()), faces_(cells.size() + 1) {
next_cells_(cells.size()), states_(cells.size()), waves_(cells.size()), faces_(cells.size() + 1), roe_states_(cells.size()) {
if (cells.empty() || cell_lengths.size() != cells.size())
throw std::invalid_argument("pipe profiles must be nonempty and have equal length");
for (std::size_t i = 0; i < cells.size(); ++i)
@@ -219,6 +225,12 @@ public:
throw std::runtime_error("nonphysical pipe state in preparation");
dt = std::min(dt, cfl * cell_lengths[i] / waves_[i]);
}
if (solver != PipeSolver::Rusanov)
for (std::size_t i=0; i<cells.size(); ++i) {
const auto& s=states_[i];
const double energy=s.pressure*pipe_gm1_inverse+.5*double(s.rho)*s.velocity*s.velocity;
roe_states_[i]={std::sqrt(s.rho),(energy+s.pressure)/s.rho};
}
prepared_ = true;
return dt;
}
@@ -251,6 +263,70 @@ public:
static Flux riemann(const GasState &a, const GasState &b) {
return riemann(a, b, std::max(wave(a), wave(b)));
}
// HLLE/HLLC use bounds enclosing endpoint and Roe acoustic speeds.
template<PipeSolver method>
static Flux flux_kernel(const GasState& a, const GasState& b,
float ca, float cb, bool* fallback = nullptr,
const RoeState* left_roe = nullptr, const RoeState* right_roe = nullptr) {
if constexpr (method == PipeSolver::Rusanov)
return riemann(a,b,std::max(std::abs(a.velocity)+ca,std::abs(b.velocity)+cb));
const double ra=left_roe?left_roe->root_density:std::sqrt(a.rho);
const double rb=right_roe?right_roe->root_density:std::sqrt(b.rho), sum=ra+rb;
const double ea=a.pressure*pipe_gm1_inverse+.5*double(a.rho)*a.velocity*a.velocity;
const double eb=b.pressure*pipe_gm1_inverse+.5*double(b.rho)*b.velocity*b.velocity;
const double ua=a.velocity, ub=b.velocity;
const double uroe=(ra*ua+rb*ub)/sum;
const double ha=left_roe?left_roe->enthalpy:(ea+a.pressure)/a.rho;
const double hb=right_roe?right_roe->enthalpy:(eb+b.pressure)/b.rho;
const double hroe=(ra*ha+rb*hb)/sum;
const double croe=std::sqrt(std::max(0.,double(gamma-1)*(hroe-.5*uroe*uroe)));
const double sl=std::min({ua-ca,ub-cb,uroe-croe});
const double sr=std::max({ua+ca,ub+cb,uroe+croe});
const auto physical=[](const GasState& q,double energy) {
const float m=q.rho*q.velocity;
return Flux{m,m*q.velocity+q.pressure,float((energy+q.pressure)*q.velocity),m*q.air,m*q.fuel};
};
const Flux fa=physical(a,ea), fb=physical(b,eb);
if(sl>=0) return fa;
if(sr<=0) return fb;
const auto hlle=[&] {
const double inv=1/(sr-sl), diff=sl*sr;
return Flux{float((sr*fa.m-sl*fb.m+diff*(b.rho-a.rho))*inv),
float((sr*fa.p-sl*fb.p+diff*(b.rho*ub-a.rho*ua))*inv),
float((sr*fa.e-sl*fb.e+diff*(eb-ea))*inv),
float((sr*fa.y-sl*fb.y+diff*(b.rho*b.air-a.rho*a.air))*inv),
float((sr*fa.fuel-sl*fb.fuel+diff*(b.rho*b.fuel-a.rho*a.fuel))*inv)};
};
if constexpr(method==PipeSolver::Hlle) return hlle();
const double dl=a.rho*(sl-ua), dr=b.rho*(sr-ub);
const double sm=(b.pressure-a.pressure+dl*ua-dr*ub)/(dl-dr);
const double ps=a.pressure+dl*(sm-ua);
// Reject nonphysical HLLC intermediate states without modifying conserved cells.
const bool left=sm>=0;
const auto& q=left?a:b;
const auto f=left?fa:fb;
const double speed=left?sl:sr;
const double inv=1/(speed-sm);
const double density=(left?dl:dr)*inv;
const double energy=((speed-q.velocity)*(left?ea:eb)-q.pressure*q.velocity+ps*sm)*inv;
if(!std::isfinite(sm) || !(sl<sm && sm<sr) || !(ps>0) || !(density>0) ||
!(energy>.5*density*sm*sm)) {
if(fallback) *fallback=true;
return hlle();
}
const double mass=f.m+speed*(density-q.rho);
return {float(mass),float(f.p+speed*(density*sm-q.rho*q.velocity)),
float(f.e+speed*(energy-(left?ea:eb))),float(mass*q.air),float(mass*q.fuel)};
}
static Flux numerical_flux(const GasState& a,const GasState& b,PipeSolver solver,
float ca,float cb,bool* fallback=nullptr) {
if(solver==PipeSolver::Rusanov) return flux_kernel<PipeSolver::Rusanov>(a,b,ca,cb,fallback);
if(solver==PipeSolver::Hlle) return flux_kernel<PipeSolver::Hlle>(a,b,ca,cb,fallback);
return flux_kernel<PipeSolver::Hllc>(a,b,ca,cb,fallback);
}
static Flux numerical_flux(const GasState& a,const GasState& b,PipeSolver solver) {
return numerical_flux(a,b,solver,std::sqrt(gamma*a.pressure/a.rho),std::sqrt(gamma*b.pressure/b.rho));
}
void advance(float dt) {
if (dt <= 0)
return;
@@ -259,12 +335,27 @@ public:
const std::size_t n = cells.size();
if (!prepared_)
prepare_step();
faces_[0] = left_override_ ? left_flux_ : scaled(riemann(left_ghost, states_[0]), cell_areas[0]);
faces_[0] = left_override_ ? left_flux_ : scaled(numerical_flux(left_ghost, states_[0], solver), cell_areas[0]);
faces_[n] =
right_override_ ? right_flux_ : scaled(riemann(states_[n - 1], right_ghost), cell_areas[n - 1]);
for (std::size_t i = 1; i < n; ++i)
faces_[i] = scaled(riemann(states_[i - 1], states_[i], std::max(waves_[i - 1], waves_[i])),
.5f * (cell_areas[i - 1] + cell_areas[i]));
right_override_ ? right_flux_ : scaled(numerical_flux(states_[n - 1], right_ghost, solver), cell_areas[n - 1]);
if (solver == PipeSolver::Rusanov) {
for (std::size_t i = 1; i < n; ++i)
faces_[i] = scaled(riemann(states_[i-1], states_[i], std::max(waves_[i-1], waves_[i])),
.5f*(cell_areas[i-1]+cell_areas[i]));
} else {
const auto fill_faces=[&]<PipeSolver method>() {
for (std::size_t i = 1; i < n; ++i) {
bool fallback=false;
faces_[i] = scaled(flux_kernel<method>(states_[i-1], states_[i],
waves_[i-1]-std::abs(states_[i-1].velocity), waves_[i]-std::abs(states_[i].velocity),
&fallback, &roe_states_[i-1], &roe_states_[i]),
.5f*(cell_areas[i-1]+cell_areas[i]));
hllc_fallbacks += fallback;
}
};
if(solver==PipeSolver::Hlle) fill_faces.template operator()<PipeSolver::Hlle>();
else fill_faces.template operator()<PipeSolver::Hllc>();
}
const float relaxation = energy_relaxation_rate > 0 ? std::exp(-energy_relaxation_rate * dt) : 1.0f;
for (std::size_t i = 0; i < n; ++i) {
const auto fl = faces_[i], fr = faces_[i + 1];
@@ -283,6 +374,11 @@ public:
rho = 1e-12f;
++positivity_repairs;
}
if (!linear_resistance.empty()) {
const float speed = std::abs(mom/rho);
mom /= 1 + dt*(linear_resistance[i] + .5f*quadratic_resistance[i]*speed);
// Retain total energy: dissipated kinetic energy becomes heat.
}
if (damping_multiplier > 0)
mom *= std::exp(-.1f * damping_multiplier * dt / cells[i].rho);
if (energy_relaxation_rate > 0)
@@ -315,6 +411,7 @@ private:
std::vector<GasState> states_;
std::vector<float> waves_;
std::vector<Flux> faces_;
std::vector<RoeState> roe_states_;
Flux left_flux_{}, right_flux_{};
bool left_override_{}, right_override_{}, prepared_{};
static std::vector<float> uniform(float v, int n) {
@@ -387,6 +484,19 @@ public:
claim(pipe, left);
opens_.push_back({pipe, left, p, a});
}
// Full circular mouth radiating into free space; no flange or mean-flow correction.
void add_radiating_open_end(int pipe, bool left, float pressure, float area, float temperature = 300) {
if (!std::isfinite(temperature) || temperature <= 0)
throw std::invalid_argument("invalid ambient temperature");
if (pipe < 0 || static_cast<std::size_t>(pipe) >= pipes_.size())
throw std::invalid_argument("invalid boundary pipe index");
const float face_area = pipes_[pipe].cell_area(end(pipes_[pipe], left));
if (std::abs(area-face_area) > 1e-6f*face_area)
throw std::invalid_argument("radiating mouth must match pipe face area");
add_open_end(pipe, left, pressure, area);
opens_.back().radiating = true;
opens_.back().temperature = temperature;
}
// An ideal, zero-volume junction between two duct faces. Orientation is explicit.
void add_connection(int a, bool left_a, int b, bool left_b) {
if (a < 0 || b < 0 || static_cast<std::size_t>(a) >= pipes_.size() ||
@@ -404,7 +514,23 @@ public:
claim(b, left_b);
connections_.push_back({a, b, left_a, left_b});
}
struct JunctionFace { int pipe; bool left; };
void add_junction(std::initializer_list<JunctionFace> ends) {
if(ends.size()<2) throw std::invalid_argument("junction needs at least two faces");
JunctionNode node;
for(auto e:ends) {
check_face(e.pipe,e.left);
for(auto previous:node.ends) {
if(previous.pipe==e.pipe && previous.left==e.left) throw std::invalid_argument("duplicate junction face");
}
node.ends.push_back(e);
}
for(auto e:ends) claim(e.pipe,e.left);
node.states.resize(ends.size());
nodes_.push_back(std::move(node));
}
float open_end_mass_flow(std::size_t i) const { return opens_.at(i).last_mass_flow; }
float open_end_volume_flow(std::size_t i) const { return opens_.at(i).last_volume_flow; }
float open_end_pressure(std::size_t i) const { return opens_.at(i).last_face_pressure; }
std::uint64_t limited_fluxes{}, nonlinear_iterations{};
void validate_topology() const {
@@ -421,12 +547,52 @@ public:
for (auto &o : orifices_)
resolve_orifice(o, dt);
for (auto &o : opens_)
resolve_open(o);
resolve_open(o, dt);
for (auto &o : connections_)
resolve_connection(o);
for(auto& node:nodes_) resolve_junction(node,dt);
}
private:
struct JunctionNode {
std::vector<JunctionFace> ends;
std::vector<GasState> states;
};
std::vector<JunctionNode> nodes_;
// Zero-storage Rusanov star state. Integrated mass, total energy and
// species fluxes sum to zero algebraically across every connected face.
// The node has no conserved state, timestep or storage volume of its own.
void resolve_junction(JunctionNode& node,float) {
double speed=0;
for(std::size_t k=0;k<node.ends.size();++k) {
auto e=node.ends[k];auto& p=pipes_[e.pipe];auto s=p.state(end(p,e.left));
s.velocity*=e.left?-1:1;node.states[k]=s;
speed=std::max(speed,2*(std::sqrt(gamma*s.pressure/s.rho)+std::abs(double(s.velocity))));
}
double area_sum=0,rho=0,energy=0,air=0,fuel=0;
for(std::size_t k=0;k<node.ends.size();++k) {
auto e=node.ends[k];auto& p=pipes_[e.pipe];const auto s=node.states[k];
const double a=p.cell_area(end(p,e.left)), u=s.velocity;
const double en=s.pressure/pipe_gm1+.5*s.rho*u*u;
area_sum+=a;rho+=a*s.rho*(1+u/speed);
energy+=a*(en+(en+s.pressure)*u/speed);
air+=a*s.rho*s.air*(1+u/speed);fuel+=a*s.rho*s.fuel*(1+u/speed);
}
rho/=area_sum;energy/=area_sum;air/=area_sum;fuel/=area_sum;
const double pressure=energy*pipe_gm1;
for(std::size_t k=0;k<node.ends.size();++k) {
auto e=node.ends[k];auto& p=pipes_[e.pipe];const auto s=node.states[k];
const double a=p.cell_area(end(p,e.left)), u=s.velocity;
const double en=s.pressure/pipe_gm1+.5*s.rho*u*u;
const double orientation=e.left?-1:1;
const double m=.5*a*(s.rho*u-speed*(rho-s.rho));
const double heat=.5*a*((en+s.pressure)*u-speed*(energy-en));
const double y=.5*a*(s.rho*s.air*u-speed*(air-s.rho*s.air));
const double f=.5*a*(s.rho*s.fuel*u-speed*(fuel-s.rho*s.fuel));
const double momentum=.5*a*(s.rho*u*u+s.pressure+pressure+speed*s.rho*u);
p.set_boundary_flux(e.left,{float(orientation*m),float(momentum),float(orientation*heat),float(orientation*y),float(orientation*f)});
}
}
struct Budget {
float *air{}, *exhaust{}, *fuel{}, *energy{};
float mass{}, heat{};
@@ -443,7 +609,10 @@ private:
struct Open {
int pipe;
bool left;
float pressure, area, last_mass_flow{}, last_face_pressure{};
float pressure, area, last_mass_flow{}, last_face_pressure{}, last_volume_flow{};
bool radiating{};
float temperature{300};
OpenEndRadiation reflection{};
};
struct Connection {
int a, b;
@@ -630,7 +799,7 @@ private:
if (!from)
v.fuel_fraction = s.fuel;
}
void resolve_open(Open &o) {
void resolve_open(Open &o, float dt) {
auto &p = pipes_[o.pipe];
int i = end(p, o.left);
auto s = p.state(i);
@@ -638,6 +807,40 @@ private:
if (o.left ? s.velocity >= c : s.velocity <= -c)
throw std::runtime_error(
"supersonic inlet needs full upstream state; pressure-only boundary is insufficient");
if (o.radiating) {
const float direction = o.left ? -1.f : 1.f;
const double outward = direction*s.velocity;
GasState face = s;
if (outward < c) {
const double reference_c = c*std::pow(double(o.pressure)/s.pressure, (gamma-1)/(2*gamma));
const double incident = outward + 2*(c-reference_c)/(gamma-1);
const double radius = std::sqrt(o.area/pi);
const double ambient_c = std::sqrt(gamma*gas_constant*o.temperature);
const double reflected = o.reflection.process(incident, dt*ambient_c/radius);
const double sound = reference_c + (gamma-1)*.25*(incident+reflected);
if (!(sound > 0)) throw std::runtime_error("radiation characteristic outside subsonic model");
face.pressure = o.pressure*std::pow(sound/reference_c, 2*gamma/(gamma-1));
face.velocity = direction*.5*(incident-reflected);
face.rho = s.rho*std::pow(face.pressure/s.pressure, 1/gamma);
if (direction*face.velocity < 0) {
// Fresh ambient gas enters with ambient stagnation enthalpy.
const float temperature = o.temperature - .5f*face.velocity*face.velocity /
(gamma/(gamma-1)*gas_constant);
if (!(temperature > 0)) throw std::runtime_error("ambient inflow exceeds stagnation enthalpy");
face.rho = face.pressure/(gas_constant*temperature);
face.air = 1; face.fuel = 0;
}
}
ghost(p, o.left, face);
const float mass_flux = face.rho*face.velocity*o.area;
const float enthalpy = gamma/(gamma-1)*face.pressure/face.rho + .5f*face.velocity*face.velocity;
p.set_boundary_flux(o.left, {mass_flux, mass_flux*face.velocity+face.pressure*o.area,
mass_flux*enthalpy, mass_flux*face.air, mass_flux*face.fuel});
o.last_volume_flow = direction*face.velocity*o.area;
o.last_mass_flow = direction*mass_flux;
o.last_face_pressure = face.pressure;
return;
}
const float entropy = s.pressure / std::pow(s.rho, gamma);
const float rho = std::pow(o.pressure / entropy, 1 / gamma);
const float cb = std::sqrt(gamma * o.pressure / rho);
@@ -653,6 +856,7 @@ private:
f.p += s.pressure * (p.cell_area(i) - a);
p.set_boundary_flux(o.left, f);
o.last_mass_flow = f.m * (o.left ? -1 : 1);
o.last_volume_flow = o.last_mass_flow / (ambient_inflow ? rho : s.rho);
o.last_face_pressure = f.p / p.cell_area(i);
}
void resolve_connection(const Connection &o) {
@@ -664,7 +868,7 @@ private:
sa.velocity *= da;
sb.velocity *= db;
const float area = std::min(a.cell_area(ia), b.cell_area(ib));
auto f = Pipe1D::scaled(Pipe1D::riemann(sa, sb), area);
auto f = Pipe1D::scaled(Pipe1D::numerical_flux(sa, sb, a.solver == b.solver ? a.solver : PipeSolver::Rusanov), area);
a.set_boundary_flux(o.left_a,
{da * f.m, f.p + sa.pressure * (a.cell_area(ia) - area), da * f.e,
da * f.y, da * f.fuel});