all of the changes
This commit is contained in:
@@ -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");
|
||||
|
||||
37
tests/desktop_ui_tests.cpp
Normal file
37
tests/desktop_ui_tests.cpp
Normal 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";
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
33
tests/scenario_examples.cpp
Normal file
33
tests/scenario_examples.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user