all of the changes
This commit is contained in:
@@ -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});
|
||||
|
||||
Reference in New Issue
Block a user