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

@@ -5,6 +5,7 @@
#include <iostream>
#include <stdexcept>
#include <vector>
#include <span>
using namespace fluidsim::port;
namespace {
@@ -14,7 +15,8 @@ void check(bool condition, const char* message) {
}
int main() {
SaabB205Scenario s;
const auto cylinders = s.cylinders();
const auto storage = s.cylinders();
const std::span<FourStrokeCylinder* const> cylinders(storage.data(), s.cylinder_count());
check(s.cylinder_count() == 4 && s.pipes.size() == 10, "B205 topology is not four-cylinder");
check(std::abs(4*s.cylinder1.swept_volume()*1e6f-1984.9f) < 1, "B205 displacement incorrect");
check(s.vehicle.gear_ratios.size() == 5 && s.vehicle.mass == 1675, "wagon configuration incorrect");

View File

@@ -0,0 +1,37 @@
#include "FluidSim/Desktop/DesktopUi.hpp"
#include <cmath>
#include <iostream>
#include <stdexcept>
using namespace fluidsim::port::desktop;
void check(bool condition,const char* reason) {if(!condition)throw std::runtime_error(reason);}
int main() {
for(auto size : {sf::Vector2u{933,1010},sf::Vector2u{1920,1080},sf::Vector2u{1040,800},sf::Vector2u{800,700}}) {
const auto l=WorkspaceLayout::calculate(size);
for(auto r : {l.sidebar,l.viewport,l.chart,l.inspector,l.pedal}) {
check(r.position.x>=0 && r.position.y>=0 && r.size.x>0 && r.size.y>0,"invalid panel bounds");
check(r.position.x+r.size.x<=size.x && r.position.y+r.size.y<=size.y,"panel escapes window");
}
check(l.sidebar.position.x+l.sidebar.size.x<l.viewport.position.x,"sidebar overlaps engine");
check(l.viewport.position.y+l.viewport.size.y<l.chart.position.y,"engine overlaps dyno");
check(l.chart.position.x+l.chart.size.x<l.inspector.position.x,"dyno overlaps inspector");
}
DesktopUi ui;
const sf::Vector2u size{1440,900};const auto track=WorkspaceLayout::calculate(size).pedal;
const sf::Vector2i point{int(track.position.x+track.size.x*.7f),int(track.position.y+12)};
ui.handle_event(sf::Event::MouseButtonPressed{sf::Mouse::Button::Left,point},size);
check(std::abs(ui.pedal()-.7f)<.01f,"slider should set direct pedal request");
ui.update_pedal(.5f,false,true);
check(std::abs(ui.pedal()-.7f)<.01f,"keyboard ramp must not interfere while dragging slider");
ui.handle_event(sf::Event::MouseButtonReleased{sf::Mouse::Button::Left,point},size);
check(ui.pedal()==0,"slider must spring to zero on release");
ui.update_pedal(.5f,true,false);check(ui.pedal()==1,"W full travel must take 0.5 seconds");
ui.update_pedal(.25f,false,false);check(std::abs(ui.pedal()-.5f)<1e-6f,"keyboard release travel timing");
ui.update_pedal(.25f,false,false);check(ui.pedal()==0,"keyboard must return to idle request");
ui.handle_event(sf::Event::MouseButtonPressed{sf::Mouse::Button::Left,point},size);
ui.handle_event(sf::Event::FocusLost{},size);check(ui.pedal()==0,"lost focus must release UI pedal");
OrbitCamera camera;camera.reset(2);auto before=camera.target;
camera.orbit({40,20});camera.pan({20,15},500);camera.zoom(2);
check(camera.pitch<=80 && camera.pitch>=-75 && camera.distance>.35f,"camera bounds");
check(camera.target.x!=before.x || camera.target.y!=before.y,"camera panning must move target");
std::cout<<"Desktop layout, spring pedal, keyboard travel and orbit camera checks passed\n";
}

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);

View File

@@ -34,8 +34,6 @@ int main() {
"fresh charge heats progressively after repeated blips");
check(inlet_temperature > 280 && inlet_temperature < 320,
"inlet no longer supplies near-ambient fresh air");
check(std::abs(s.intake_atmosphere.port.temperature - 300) < .01f, "reservoir temperature drift");
check(std::abs(s.intake_atmosphere.port.air_fraction - 1) < 1e-6, "reservoir admits exhaust composition");
run(s, 7, 0);
const auto recovered = s.snapshot();
check(recovered.rpm > 1000 && recovered.rpm < 2600, "engine did not recover idle after second blip");

View File

@@ -9,8 +9,10 @@
#include "FluidSim/OutdoorExhaustReverb.hpp"
#include "FluidSim/HelmholtzScenario.hpp"
#include "FluidSim/PulseTestScenario.hpp"
#include "FluidSim/ThreadLoadTracker.hpp"
#include "FluidSim/Units.hpp"
#include "FluidSim/Desktop/PedalControl.hpp"
#include "FluidSim/Desktop/PipePath.hpp"
#include "FluidSim/Desktop/PressurePalette.hpp"
#include <cmath>
#include <iostream>
@@ -28,7 +30,64 @@ void require(bool condition, const char* message) {
int main() {
using namespace fluidsim::port;
Ecu fixed_idle;
fixed_idle.idle_control=Ecu::IdleControl::FixedOpening;
fixed_idle.idle_base_opening=.049f;
fixed_idle.overrun_fuel_cut_enabled=false;
for(float rpm : {0.f,500.f,3000.f,9000.f}) {
for(int i=0;i<4410;++i) fixed_idle.step(1.f/44100,0,rpm);
require(std::abs(fixed_idle.throttle_opening()-.049f)<1e-5f,
"fixed carb idle must not vary with RPM");
}
for(int i=0;i<4410;++i) fixed_idle.step(1.f/44100,.2f,3000);
require(std::abs(fixed_idle.throttle_opening()-.2f)<1e-5f,"pedal must open above carb stop");
for(int i=0;i<4410;++i) fixed_idle.step(1.f/44100,.01f,3000);
require(std::abs(fixed_idle.throttle_opening()-.049f)<1e-5f,"release must return to carb stop");
for(auto preset : {FourStrokePreset::Twin,FourStrokePreset::SaabB205,FourStrokePreset::AmericanV8}) {
FourStrokeScenario starter(44100,preset);
starter.crankshaft.omega=0;
starter.set_starter(true);
starter.process();
require(starter.crankshaft.omega>0,"starter must turn every four-stroke crank from rest");
starter.set_starter(false);
require(!starter.starter_engaged,"starter release must disengage");
}
{
TwoStrokeConfig config; config.idle_slide_opening=.071f;
TwoStrokeScenario starter(44100,config);
require(starter.ecu.idle_base_opening==.071f,"scenario must configure carb idle stop");
starter.crankshaft.omega=0; starter.set_starter(true); starter.process();
require(starter.crankshaft.omega>0,"starter must turn two-stroke crank from rest");
starter.set_starter(false);
}
desktop::PedalControl pedal;
for(int i=0;i<100;++i)pedal.advance(.0025f,true,false);
require(std::abs(pedal.value()-.5f)<1e-5f,"W pedal travel must be halfway after 0.25 seconds");
for(int i=0;i<100;++i)pedal.advance(.0025f,true,false);
require(std::abs(pedal.value()-1)<1e-5f,"W must reach full pedal in 0.5 seconds");
for(int i=0;i<200;++i)pedal.advance(.0025f,false,false);
require(pedal.value()<1e-5f,"keyboard release must return pedal in 0.5 seconds");
pedal.set(.8f);
for(int i=0;i<200;++i)pedal.advance(.0025f,false,true);
require(pedal.value()==0,"S must release pedal");
desktop::PipePath path({{0,{0,0,0}},{.25f,{.2f,.1f,0}},{1,{1,0,.1f}}});
auto knot=path.position(.25f),direction=path.tangent(.25f);
require(std::abs(knot.x-.2f)<1e-6f && std::abs(knot.y-.1f)<1e-6f,"pipe path must interpolate its metre-referenced control point");
require(std::abs(direction.x*direction.x+direction.y*direction.y+direction.z*direction.z-1)<1e-4f,"pipe path tangent must be normalized");
auto atm=desktop::pressure_tint(101325),vac=desktop::pressure_tint(0),peak=desktop::pressure_tint(1000000);
require(atm.y>.8f && vac.z>.99f && peak.x>.99f,"pressure palette must show atmosphere green, vacuum blue, spikes red");
Ecu fast_actuator;
for(int i=0;i<230;++i)fast_actuator.step(1.f/44100,1,5000);
require(fast_actuator.throttle_opening()>.99f,"throttle actuator must settle within about 5 ms");
for(int i=0;i<230;++i)fast_actuator.step(1.f/44100,0,5000);
require(fast_actuator.throttle_opening()<.01f,"throttle actuator must close within about 5 ms");
Ecu idle_actuator;
for(int i=0;i<441;++i)idle_actuator.step(1.f/44100,0,1000);
require(idle_actuator.idle_active() && idle_actuator.throttle_opening()>0,"zero UI pedal must retain ECU idle authority");
Ecu controller;
controller.throttle_time_constant=.08f;controller.throttle_rate=2;
constexpr float ecu_dt = 1.0f / 44100.0f;
controller.step(ecu_dt, 1.0f, 2000.0f);
require(controller.throttle_opening() > 0.0f && controller.throttle_opening() <= 2.0f * ecu_dt,
@@ -98,21 +157,6 @@ int main() {
"dyno bins must retain their peak power and torque");
dyno_curve.reset();
require(dyno_curve.bins().empty(), "dyno reset must clear accumulated history");
RadiationSource propagation;
int first_arrival = -1;
for (int i = 0; i < 240; ++i) {
const auto frame = propagation.process(i == 0 ? 0.001f : 0.0f);
if (first_arrival < 0 && (frame.left != 0.0f || frame.right != 0.0f)) first_arrival = i;
}
require(first_arrival >= 190 && first_arrival <= 195,
"1.5 m propagation must delay sound about 193 samples at 44.1 kHz");
ThreadLoadTracker thread_load;
thread_load.record(1.0, 2.0);
require(std::abs(thread_load.load_percent() - 2.5) < 1e-9 &&
std::abs(thread_load.last_step_microseconds() - 1000.0) < 1e-9,
"thread load tracker must report EMA budget use and last-step time");
Crankshaft crank;
FourStrokeCylinder cylinder;
cylinder.initialize(crank);
@@ -264,14 +308,11 @@ int main() {
require(idle_state.throttle > 0.0f && idle_state.pedal == 0.0f,
"idle control must open the butterfly independently of the pedal");
scenario.toggle_mechanical_sound();
for (int i = 0; i < 1000; ++i) {
const AudioFrame frame = scenario.process();
require(std::isfinite(frame.left) && std::isfinite(frame.right),
"the integrated mechanical/reverb audio path must remain finite");
"the integrated fluid-derived audio path must remain finite");
}
require(scenario.snapshot().mechanical_sound_enabled,
"mechanical-noise toggle must be reflected in the scenario snapshot");
std::cout << "FluidSim C++ physics tests passed\n";
return 0;

View File

@@ -1,46 +0,0 @@
#include "FluidSim/Scenario.hpp"
#include <cmath>
#include <iostream>
#include <stdexcept>
using namespace fluidsim::port;
void check(bool ok, const char* message) { if (!ok) throw std::runtime_error(message); }
int main() {
Ecu ecu;
ecu.boost_control_enabled = true;
ecu.step(.001f, 1, 3500);
for (int i=0;i<1000;++i) ecu.control_boost(.001f,1,3500,110000,115000);
check(ecu.wastegate_duty()>0 && ecu.wastegate_duty()<=.85f,"no spool duty");
ecu.control_boost(.001f,1,3500,190000,190000);
check(ecu.fuel_cut() && ecu.wastegate_duty()==0,"overboost protection failed");
ecu.control_boost(.001f,1,3500,140000,140000);
check(!ecu.fuel_cut(),"overboost recovery failed");
ecu.control_boost(.001f,0,3500,100000,100000);
check(ecu.wastegate_duty()==0 && ecu.boost_target()==0,"lift-off did not restore spring control");
ecu.control_boost(.001f,1,3500,NAN,100000);
check(ecu.fuel_cut() && ecu.wastegate_duty()==0,"boost sensor fail-safe failed");
FourStrokeScenario s(44100,FourStrokePreset::SaabB205,true);
check(s.pipes.size()==12 && s.turbo && s.ecu.boost_control_enabled,"Saab turbo topology missing");
double peak_rpm=0, peak_map=0, peak_audio=0, settled_pressure=0;
for(int second=0;second<10;++second) {
for(int i=0;i<44100;++i) {
s.throttle = second>=1 && second<7 ? 1.f : 0.f;
if(second<7) s.crankshaft.omega=3500*2*pi/60;
const auto a=s.process();
if(second>=4 && second<7) settled_pressure+=s.intake_plenum.pressure()/(3.*44100);
check(std::isfinite(a.left) && std::isfinite(a.right),"nonfinite audio");
peak_audio=std::max(peak_audio,double(std::abs(a.left)));
}
auto t=s.turbo->snapshot();
peak_rpm=std::max(peak_rpm,t.rpm);
peak_map=std::max(peak_map,double(s.intake_plenum.pressure()));
check(std::isfinite(t.rpm) && std::abs(t.energy_residual)<.1,"turbo energy balance failed");
check(t.limited_transfers==0,"housing donor limit hit");
std::cout<<second+1<<" MAP="<<s.intake_plenum.pressure()/1000<<" charge="<<t.charge_pressure/1000
<<" rpm="<<t.rpm<<" duty="<<s.ecu.wastegate_duty()<<" gate_mm="<<t.wastegate_lift*1000<<'\n';
}
check(peak_rpm>50000 && peak_map>110000,"GT17 failed to spool/boost");
check(peak_audio<1,"audio exceeds monitoring range");
check(std::abs(settled_pressure-146325)<8000,"boost did not settle near target");
check(s.ecu.wastegate_duty()==0 && s.turbo->snapshot().rpm<peak_rpm,"lift-off control/spool-down failed");
for(auto& p:s.pipes) check(p.positivity_repairs==0,"pipe state repaired");
}

View File

@@ -0,0 +1,33 @@
#include "FluidSim/ScenarioCatalog.hpp"
#include "FluidSim/DesktopScenario.hpp"
#include <cmath>
#include <chrono>
#include <iostream>
#include <set>
#include <stdexcept>
using namespace fluidsim::port;
int main() {
std::set<std::string> ids;
for(const auto& example:scenario_examples()) {
if(!ids.insert(example.id).second)throw std::runtime_error("duplicate example id");
DesktopScenario scenario(example.kind,example.two,example.exhaust,example.v8,example.engine,example.title);
scenario.set_audio_scene(OutdoorScene::Dry);
const auto started=std::chrono::steady_clock::now();
float peak_power=0;
for(int i=0;i<88200;++i) {
scenario.set_controls(i<44100?.45f:0.f,0,1);
const auto audio=scenario.process();
if(i%4410==0) peak_power=std::max(peak_power,scenario.snapshot().average_power);
if(!std::isfinite(audio.left)||!std::isfinite(audio.right))throw std::runtime_error(example.id+" nonfinite audio");
}
const auto snapshot=scenario.snapshot();
if(snapshot.pipe_positivity_repairs)throw std::runtime_error(example.id+" required positivity repairs");
if(example.id=="big-carb" && peak_power<10000)throw std::runtime_error("big carb V8 failed to produce power");
if(!std::isfinite(snapshot.rpm)||snapshot.rpm<=0)throw std::runtime_error(example.id+" invalid engine speed");
if(example.engine.cylinders && (snapshot.cylinder_count!=example.engine.cylinders || snapshot.layout!=example.engine.layout))throw std::runtime_error("custom cylinder layout lost");
if(snapshot.example_name!=example.title)throw std::runtime_error("example name lost");
if(example.kind==ScenarioKind::AmericanV8 && snapshot.h_pipe!=example.v8.h_pipe)throw std::runtime_error("example crossover selection lost");
if(example.kind==ScenarioKind::SaabB205 && snapshot.silencers!=example.exhaust.enabled)throw std::runtime_error("example silencer selection lost");
std::cout<<example.id<<" passed, rpm="<<snapshot.rpm<<", wall/sim="<<std::chrono::duration<double>(std::chrono::steady_clock::now()-started).count()/2<<std::endl;
}
}

View File

@@ -1,115 +0,0 @@
#include "FluidSim/Turbocharger.hpp"
#include "FluidSim/Atmosphere.hpp"
#include "FluidSim/Solver.hpp"
#include <iostream>
#include <stdexcept>
using namespace fluidsim::port;
namespace {
void check(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); }
double mass(const Turbocharger& t) {
return double(t.compressor_inlet.mass())+t.compressor_outlet.mass()+t.turbine_inlet.mass()+t.turbine_outlet.mass();
}
double energy(const Turbocharger& t) { return t.fluid_energy()+t.shaft_energy()+t.snapshot().bearing_heat; }
void closed_balance(double initial_rpm) {
Turbocharger t;
t.set_initial_rpm(initial_rpm);
t.turbine_inlet.set_pressure(250000, 900);
const double before = energy(t), initial_mass = mass(t);
for (int i = 0; i < 10000; ++i) t.update_state(1e-6f);
check(std::abs(mass(t)-initial_mass) < initial_mass*3e-5, "turbo created/lost gas mass");
check(std::abs(energy(t)-before) < before*2e-4, "fluid + shaft + bearing heat energy drift");
check(t.omega() > 0, "turbine did not start its shaft from pressure energy");
const auto s = t.snapshot();
check(std::abs(s.energy_residual) < before*2e-4, "turbo energy residual excessive");
std::cout << "Closed balance initial_rpm=" << initial_rpm << " mass_error=" << mass(t)-initial_mass
<< " energy_error=" << energy(t)-before << " J\n";
}
double turbine_flow(float downstream) {
Turbocharger t;
t.turbine_inlet.set_pressure(300000, 900);
t.turbine_outlet.set_pressure(downstream, 600);
t.update_state(1e-7f);
return t.snapshot().turbine_flow;
}
}
int main() {
Turbocharger rest;
const double e = energy(rest);
for (int i = 0; i < 1000; ++i) rest.update_state(1.f/44100);
check(rest.omega() == 0 && energy(rest) == e && rest.snapshot().compressor_pressure_ratio == 1,
"stationary equilibrium generated boost/shaft energy");
closed_balance(0);
closed_balance(120000);
check(std::abs(turbine_flow(100000)-turbine_flow(50000)) < 1e-10, "choked turbine flow depends on back pressure");
check(turbine_flow(250000) < turbine_flow(100000), "higher backpressure did not reduce subcritical flow");
Turbocharger reverse;
reverse.compressor_outlet.set_pressure(150000, 350);
reverse.turbine_outlet.set_pressure(150000, 600);
const auto reverse_energy = energy(reverse);
reverse.update_state(1e-6f);
check(reverse.snapshot().compressor_flow < 0 && reverse.snapshot().turbine_flow < 0,
"turbo acted as an artificial one-way valve");
check(std::abs(energy(reverse)-reverse_energy) < reverse_energy*1e-6, "reverse flow creates energy");
Turbocharger gate;
gate.compressor_outlet.set_pressure(220000, 350);
gate.turbine_inlet.set_pressure(250000, 900);
gate.update_state(1e-6f);
check(gate.snapshot().wastegate_lift > 0 && gate.snapshot().wastegate_flow > 0,
"pressure can did not open the wastegate bypass");
check(std::abs(gate.snapshot().energy_residual) < 1e-4, "wastegate bypass created energy");
Turbocharger coarse;
coarse.set_initial_rpm(200000);
const double coarse_energy = energy(coarse);
coarse.update_state(.5f);
check(coarse.omega() >= 0 && std::abs(energy(coarse)-coarse_energy) < coarse_energy*1e-5,
"large turbo timestep creates energy or reverses shaft");
bool caught = false;
try { TurboConfig bad; bad.shaft_inertia = 0; Turbocharger invalid(bad); }
catch (const std::invalid_argument&) { caught = true; }
check(caught, "zero rotor inertia accepted");
// Hot pulsing reservoir -> real pipes -> turbine -> exhaust reservoir.
// Compressor feeds a pressure reservoir representing a loaded charge duct.
std::vector<Pipe1D> pipes;
for (int i = 0; i < 4; ++i) pipes.emplace_back(pi*.024f*.024f*.25f, .08f, 8);
Atmosphere fresh, charge, hot, exhaust;
charge.pressure = 130000;
hot.temperature = 900;
hot.pressure = 220000;
charge.update_state(0); hot.update_state(0);
BoundarySystem boundaries(pipes);
Turbocharger turbo;
turbo.connect(boundaries, pipes, 0, false, 1, true, 2, false, 3, true);
std::array<float, 4> areas;
const std::array<Atmosphere*, 4> reservoirs{&fresh, &charge, &hot, &exhaust};
for (int i = 0; i < 4; ++i) {
areas[i] = pipes[i].area;
boundaries.add_orifice(reservoirs[i]->port, i, i%2 == 0, areas[i]);
}
Solver solver(pipes, boundaries);
solver.add_component(turbo);
float minimum_power = 1e9f, maximum_power = -1e9f;
double initial_speed = 0;
for (int i = 0; i < 44100; ++i) {
hot.pressure = 220000+60000*std::sin(2*pi*80*i/44100);
hot.update_state(0);
solver.step(1.f/44100);
const auto s = turbo.snapshot();
if (i == 100) initial_speed = s.rpm;
if (i > 40000) {
minimum_power = std::min(minimum_power, float(s.turbine_power));
maximum_power = std::max(maximum_power, float(s.turbine_power));
}
}
const auto result = turbo.snapshot();
check(result.rpm > initial_speed+10000, "hot gas did not spool the coupled turbo");
check(result.compressor_flow > 0 && result.compressor_power > 0 && result.charge_temperature > 300,
"loaded compressor did not deliver heated charge using shaft work");
check(maximum_power-minimum_power > 100, "exhaust pulses were averaged away before turbine torque");
for (const auto& pipe : pipes) check(pipe.positivity_repairs == 0, "turbo bench required pipe repairs");
check(result.limited_transfers == 0, "normal turbo bench depleted a storage node");
std::cout << "Turbo pipe bench passed: rpm=" << result.rpm << " compressor flow=" << result.compressor_flow
<< " kg/s, charge T=" << result.charge_temperature << " K, turbine power range="
<< minimum_power << ".." << maximum_power << " W, residual=" << result.energy_residual << " J\n";
}

View File

@@ -1,43 +0,0 @@
#include "FluidSim/Scenario.hpp"
#include <algorithm>
#include <cmath>
#include <iostream>
#include <stdexcept>
using namespace fluidsim::port;
namespace {
void check(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); }
}
int main() {
TurboTwinScenario s;
double maximum_speed = 0, maximum_charge = 0, maximum_turbine_pressure = 0;
double maximum_gate = 0, peak_audio = 0, coast_speed = 0;
for (int sample = 0; sample < 7*44100; ++sample) {
// Idle, accelerate/limiter operation, abrupt pedal release, then recover.
s.set_controls(sample >= 44100 && sample < 4*44100 ? .7f : 0, 0, 1);
const auto audio = s.process();
check(std::isfinite(audio.left) && std::isfinite(audio.right), "nonfinite turbo twin audio");
peak_audio = std::max({peak_audio, double(std::abs(audio.left)), double(std::abs(audio.right))});
const auto t = s.turbo->snapshot();
check(t.rpm >= 0 && std::isfinite(t.rpm) && t.charge_temperature > 0 &&
t.turbine_inlet_pressure > 0 && t.turbine_outlet_pressure > 0,
"turbo twin developed nonphysical turbo state");
maximum_speed = std::max(maximum_speed, t.rpm);
maximum_charge = std::max(maximum_charge, t.charge_pressure);
maximum_turbine_pressure = std::max(maximum_turbine_pressure, t.turbine_inlet_pressure);
maximum_gate = std::max(maximum_gate, t.wastegate_lift);
if (sample == 5*44100) coast_speed = t.rpm;
}
const auto final = s.turbo->snapshot();
check(maximum_speed > 50000 && maximum_charge > 120000, "turbo did not spool and generate boost");
check(maximum_turbine_pressure > maximum_charge, "twin has no turbine backpressure");
check(maximum_gate > 0, "wastegate never opened under boost");
check(final.rpm < coast_speed, "turbo did not coast down after throttle release");
check(final.limited_transfers == 0, "turbo twin depleted its internal volumes");
check(std::abs(final.energy_residual) < .1, "turbo twin accumulated excessive energy residual");
check(peak_audio < 1, "turbo twin audio exceeds device range");
for (const auto& p : s.pipes) check(p.positivity_repairs == 0, "turbo twin required pipe repairs");
std::cout << "Turbo twin passed: max_rpm=" << maximum_speed << ", max_charge=" << maximum_charge/1000
<< " kPa, max_turbine_pressure=" << maximum_turbine_pressure/1000 << " kPa, gate=" << maximum_gate*1000
<< " mm, final_rpm=" << final.rpm << ", peak_audio=" << peak_audio
<< ", energy_residual=" << final.energy_residual << " J\n";
}

View File

@@ -53,12 +53,11 @@ int main() {
std::to_string(maximum_rpm) + ", idle=" + std::to_string(final.rpm));
if (!reed_open_seen || !reed_closed_seen)
throw std::runtime_error("Reed did not open and close");
if (final.rpm < 2000 || final.rpm > 4000)
throw std::runtime_error("Two-stroke idle did not recover after throttle release: " +
std::to_string(final.rpm) + " RPM, coast firings=" +
std::to_string(one_second_coast_firings) + ", carb AFR=" +
std::to_string(scenario.carburetor.indicated_afr()) +
", cylinder fuel=" + std::to_string(scenario.cylinder.fuel_mass));
if (std::abs(final.throttle-scenario.configuration.idle_slide_opening)>1e-5f)
throw std::runtime_error("Two-stroke did not return to its configured fixed carb stop");
if (final.rpm < 300 || final.rpm >= release_rpm)
throw std::runtime_error("Two-stroke failed to coast down and remain running at fixed idle stop: " +
std::to_string(final.rpm));
if (peak >= 1)
throw std::runtime_error("Two-stroke audio exceeds device range");
for (const auto &pipe : scenario.pipes)
@@ -97,7 +96,7 @@ int main() {
if (pipe.positivity_repairs != 0)
throw std::runtime_error("Motorcycle launch required pipe positivity repairs");
std::cout << "Two-stroke regression passed: peak=" << peak << ", maximum RPM=" << maximum_rpm
<< ", recovered RPM=" << final.rpm << ", bike speed=" << scenario.vehicle.speed * 3.6f
<< ", post-release RPM=" << final.rpm << ", bike speed=" << scenario.vehicle.speed * 3.6f
<< " km/h, release RPM=" << release_rpm
<< ", first coast-second firings=" << one_second_coast_firings << '\n';
}