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,13 +1,16 @@
#include "FluidSim/Atmosphere.hpp"
#include "FluidSim/Crankcase.hpp"
#include "FluidSim/Fluid.hpp"
#include "FluidSim/Junction.hpp"
#include "FluidSim/Solver.hpp"
#include <cmath>
#include <complex>
#include <functional>
#include <iostream>
#include <string>
using namespace fluidsim::port;
namespace {
PipeSolver selected_solver=PipeSolver::Rusanov;
void check(bool b, const std::string &s) {
if (!b)
throw std::runtime_error(s);
@@ -19,6 +22,7 @@ void set(Pipe1D &p, int i, float pressure, float rho, float u = 0, float air = 1
p.cells[i] = {rho, rho * u, pressure / pipe_gm1 + .5f * rho * u * u, air, fuel};
}
void lossless(Pipe1D &p) {
p.solver=selected_solver;
p.damping_multiplier = 0;
p.energy_relaxation_rate = 0;
}
@@ -36,7 +40,8 @@ double energy(const Pipe1D &p) {
}
double vol_energy(const Volume0D &v) { return v.pressure() * v.volume() / (v.gamma - 1); }
} // namespace
int main() {
int main(int argc,char** argv) {
if(argc>1) selected_solver=parse_pipe_solver(argv[1]);
int failed = 0, total = 0;
auto test = [&](const char *n, std::function<void()> fn) {
++total;
@@ -48,6 +53,163 @@ int main() {
std::cout << "FAIL " << n << ": " << e.what() << '\n';
}
};
test("solver flux consistency and reflection symmetry", [] {
for(auto solver : {PipeSolver::Rusanov,PipeSolver::Hlle,PipeSolver::Hllc}) {
GasState a{1.2f,30,101325,.8f,.05f}, b{.5f,-10,60000,.1f,.3f};
const auto equal=Pipe1D::numerical_flux(a,a,solver);
near(equal.m,a.rho*a.velocity,1e-5,"uniform mass flux");
near(equal.p,a.rho*a.velocity*a.velocity+a.pressure,.02,"uniform momentum flux");
const auto forward=Pipe1D::numerical_flux(a,b,solver);
a.velocity=-a.velocity; b.velocity=-b.velocity;
const auto reverse=Pipe1D::numerical_flux(b,a,solver);
near(forward.m,-reverse.m,.0001,"orientation mass symmetry");
near(forward.p,reverse.p,.1,"orientation momentum symmetry");
near(forward.e,-reverse.e,1,"orientation energy symmetry");
near(forward.y,-reverse.y,.0001,"orientation air symmetry");
near(forward.fuel,-reverse.fuel,.0001,"orientation fuel symmetry");
}
});
test("HLLC preserves stationary contact and falls back at expansion vacuum", [] {
GasState a{1,0,1,.8f,.1f}, b{.125f,0,1,.1f,.5f};
auto f=Pipe1D::numerical_flux(a,b,PipeSolver::Hllc);
near(f.m,0,1e-7,"stationary contact mass diffusion");
near(f.e,0,1e-7,"stationary contact energy diffusion");
near(f.y,0,1e-7,"stationary contact tracer diffusion");
a.velocity=-10; b.velocity=10;
bool fallback=false;
f=Pipe1D::numerical_flux(a,b,PipeSolver::Hllc,std::sqrt(1.4f),std::sqrt(1.4f/.125f),&fallback);
check(fallback && std::isfinite(f.e),"strong expansion did not use safe HLLE flux");
});
test("unflanged radiation reflection magnitude and phase", [] {
for (double ka : {.1, .5, 1., 2.}) {
OpenEndRadiation reflection;
std::complex<double> input{}, output{};
double t = 0;
const double dt = 2*3.141592653589793/(ka*4096);
for (int i=0; i<4096*32; ++i) {
t += dt;
const double incident = std::cos(ka*t);
const double reflected = reflection.process(incident, dt);
if (i >= 4096*16) {
auto phase = std::exp(std::complex<double>(0, -ka*t));
input += incident*phase;
output += reflected*phase;
}
}
constexpr double n=.167, d=n+2*.6133, e=(d*d-n*n-1)/2;
const std::complex<double> z(0,ka);
const auto expected = -(1.+n*z)/(1.+d*z+e*z*z);
near(std::abs(output/input-expected), 0, .001, "radiation amplitude/phase mismatch");
check(std::abs(output/input) < 1, "radiation boundary creates acoustic energy");
}
});
test("N-port junction conserves sealed network mass and energy", [] {
std::vector<Pipe1D> pipes;
for(int i=0;i<3;++i) { pipes.emplace_back(.001f*(i+1),.3f,10);lossless(pipes.back()); }
for(int i=0;i<10;++i) set(pipes[0],i,120000,1.4f);
BoundarySystem boundaries(pipes);
Junction junction(.0003f);
const std::array<JunctionEnd,3> ends{{{0,false},{1,true},{2,true}}};
junction.connect(boundaries,pipes,ends);
boundaries.add_closed_end(0,true);boundaries.add_closed_end(1,false);boundaries.add_closed_end(2,false);
Solver solver(pipes,boundaries);solver.add_component(junction);
auto totals=[&] { std::array<double,2> t{junction.mass(),junction.internal_energy()};
for(auto& p:pipes) { t[0]+=mass(p);t[1]+=energy(p); } return t; };
const auto before=totals();
for(int i=0;i<1000;++i) solver.step(1.f/44100);
const auto after=totals();
if(std::abs(after[0]-before[0])>before[0]*2e-6) std::cerr << "mass relative drift=" << (after[0]-before[0])/before[0] << "\n";
near(after[0],before[0],before[0]*2e-6,"junction mass drift");
near(after[1],before[1],before[1]*2e-6,"junction energy drift");
check(pipes[1].state(0).pressure>101325,"junction did not transmit pressure to branch");
for(auto& p:pipes) check(p.positivity_repairs==0,"junction required positivity repairs");
});
test("Zero-storage N-port junction conserves sealed network mass and energy", [] {
std::vector<Pipe1D> pipes;
for(int i=0;i<3;++i) { pipes.emplace_back(.001f*(i+1),.3f,10);lossless(pipes.back()); }
for(int i=0;i<10;++i) set(pipes[0],i,120000,1.4f);
BoundarySystem boundaries(pipes);
boundaries.add_junction({{0,false},{1,true},{2,true}});
boundaries.add_closed_end(0,true);boundaries.add_closed_end(1,false);boundaries.add_closed_end(2,false);
Solver solver(pipes,boundaries);
auto totals=[&] { std::array<double,2> t{0,0};
for(auto& p:pipes) { t[0]+=mass(p);t[1]+=energy(p); } return t; };
const auto before=totals();
for(int i=0;i<1000;++i) solver.step(1.f/44100);
const auto after=totals();
// Float cell updates accumulate roundoff over 4000 substeps (50 ppm bound).
near(after[0],before[0],before[0]*5e-5,"junction mass drift");
near(after[1],before[1],before[1]*5e-5,"junction energy drift");
check(pipes[1].state(0).pressure>101325,"junction did not transmit pressure to branch");
for(auto& p:pipes) check(p.positivity_repairs==0,"junction required positivity repairs");
});
test("radiating acoustic volume source is independent of donor density", [] {
for (bool left : {false,true}) {
std::vector<Pipe1D> hot, cold;
hot.emplace_back(.001f,.1f,1); cold.emplace_back(.001f,.1f,1);
BoundarySystem a(hot), b(cold);
a.add_radiating_open_end(0,left,101325,.001f);
b.add_radiating_open_end(0,left,101325,.001f);
for(int i=0;i<4096;++i) {
const float velocity=2*std::sin(2*pi*150*i/44100);
set(hot[0],0,101325,.5f,velocity);
set(cold[0],0,101325,ambient_density,velocity);
a.resolve(1.f/44100); b.resolve(1.f/44100);
near(a.open_end_volume_flow(0),b.open_end_volume_flow(0),2e-7,
"temperature change contaminated acoustic volume source");
}
}
});
test("radiating mouth conserves reported mass at both orientations", [] {
for (bool left : {false,true}) {
std::vector<Pipe1D> p;
p.emplace_back(.001f,.1f,10);
lossless(p[0]);
for (int i=0;i<10;++i) set(p[0],i,105000,1.2f);
BoundarySystem b(p);
b.add_radiating_open_end(0,left,101325,.001f);
b.add_closed_end(0,!left);
const double initial = mass(p[0]);
b.resolve(1e-6f);
const double exchange = -b.open_end_mass_flow(0)*1e-6;
p[0].advance(1e-6f);
near(mass(p[0])-initial,exchange,1e-11,"radiating mass flux mismatch");
}
});
test("radiating mouth reflects rarefaction at either pipe orientation", [] {
for (bool left : {false,true}) {
std::vector<Pipe1D> p;
p.emplace_back(.001f,1,400);
lossless(p[0]);
const double c=std::sqrt(1.4*101325/1.2);
for(int i=0;i<400;++i) {
double x=(i+.5)/400.;
if(left) x=1-x;
float dp=100*std::exp(-std::pow((x-.3)/.04,2));
set(p[0],i,101325+dp,1.2f+dp/(c*c),(left?-1:1)*dp/(1.2*c));
}
BoundarySystem b(p);
b.add_closed_end(0,!left);
b.add_radiating_open_end(0,left,101325,.001f);
Solver solver(p,b); solver.minimum_substeps=1;
for(int i=0;i<int(1.4/c*44100);++i) solver.step(1.f/44100);
check(p[0].state(left?280:119).pressure < 101305,"open mouth did not return an inverted pressure pulse");
check(p[0].positivity_repairs==0,"radiation boundary required positivity repair");
}
});
test("radiating inflow supplies fresh ambient stagnation enthalpy", [] {
std::vector<Pipe1D> p;
p.emplace_back(.001f,.1f,10);
for(int i=0;i<10;++i) set(p[0],i,99000,1.2f,0,.1f,.2f);
BoundarySystem b(p);
b.add_radiating_open_end(0,true,101325,.001f,300);
b.add_closed_end(0,false);
b.resolve(1e-6f);
const auto face=p[0].left_ghost;
check(face.velocity>0 && face.air==1 && face.fuel==0,"backflow supplied exhaust or fuel");
near(fluidsim::port::gamma/(fluidsim::port::gamma-1)*face.pressure/face.rho+.5*face.velocity*face.velocity,
fluidsim::port::gamma/(fluidsim::port::gamma-1)*gas_constant*300, .1,"ambient enthalpy mismatch");
});
test("tracer underflow preserves bulk gas and normal concentrations", [] {
for (float fraction : {1e-40f, 1e-30f, .5f}) {
Pipe1D p(.001f, 1, 8);