#include "FluidSim/Scenario.hpp" #include "FluidSim/SimulationRingBuffer.hpp" #include #include "FluidSim/Atmosphere.hpp" #include "FluidSim/Crankcase.hpp" #include "FluidSim/DynoCurve.hpp" #include "FluidSim/TwoStrokeCylinder.hpp" #include "FluidSim/TwoStrokeScenario.hpp" #include "FluidSim/OutdoorExhaustReverb.hpp" #include "FluidSim/HelmholtzScenario.hpp" #include "FluidSim/PulseTestScenario.hpp" #include "FluidSim/Units.hpp" #include "FluidSim/Desktop/PedalControl.hpp" #include "FluidSim/Desktop/PipePath.hpp" #include "FluidSim/Desktop/PressurePalette.hpp" #include #include #include namespace { void require(bool condition, const char* message) { if (!condition) { std::cerr << "FAIL: " << message << '\n'; throw std::runtime_error(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, "ECU throttle must obey its per-second travel limit"); for (int i = 0; i < 44100; ++i) controller.step(ecu_dt, 1.0f, 2000.0f); require(controller.throttle_opening() > 0.99f && controller.target_afr() < 13.0f, "ECU must reach full opening and enrich the high-load mixture"); const float before_close = controller.throttle_opening(); controller.step(ecu_dt, 0.0f, 2000.0f); require(controller.throttle_opening() >= before_close - 2.01f * ecu_dt, "ECU must also rate-limit throttle closing"); controller.step(ecu_dt, 1.0f, 10001.0f); require(controller.fuel_cut() && controller.fuel_for_air(0.001f) == 0.0f, "ECU rev limit must inhibit injection"); controller.step(ecu_dt, 1.0f, 9900.0f); require(controller.rev_limited(), "ECU rev limiter must latch through hysteresis"); controller.rev_limit_enabled = false; controller.step(ecu_dt, 1.0f, 15000.0f); require(!controller.rev_limited() && !controller.fuel_cut() && controller.fuel_for_air(.001f) > 0, "disabling a latched limiter must restore fuel above the old limit"); controller.rev_limit_enabled = true; controller.step(ecu_dt, 1.0f, 9700.0f); require(!controller.fuel_cut() && controller.fuel_for_air(0.001f) > 0.0f, "ECU must resume injection below the rev recovery threshold"); controller.step(ecu_dt, 0.0f, 4000.0f); require(controller.fuel_cut(), "closed-pedal overrun must cut injection"); controller.step(ecu_dt, 0.0f, 1300.0f); require(!controller.fuel_cut() && controller.idle_active(), "idle catch must restore injection"); controller.step(ecu_dt, 0.0f, std::nanf("")); require(controller.sensor_fault() && controller.fuel_cut(), "invalid sensor data must inhibit fuel"); // Unit-rate playback must preserve every sample across callback boundaries. SimulationRingBuffer audio_ring(8); for (int i = 0; i < 8; ++i) audio_ring.write(float(i), -float(i)); std::array left{}, right{}; require(audio_ring.read_interpolated(left, right, 1.0) == 4, "unit-rate ring read must fill a block"); for (int i = 0; i < 4; ++i) require(left[i] == float(i) && right[i] == -float(i), "ring must preserve stereo samples"); require(audio_ring.read_interpolated(left, right, 1.0) == 3, "ring underrun must stop before unavailable interpolation lookahead"); for (int i = 8; i < 12; ++i) audio_ring.write(float(i), -float(i)); require(audio_ring.read_interpolated(left, right, 1.0) == 4 && left[0] == 7.0f && left[3] == 10.0f, "ring must resume continuously after underrun and wraparound"); SoundProcessor linear_source(44100, 1.0f); linear_source.process(0.0f); const float response = linear_source.process(0.01f); SoundProcessor doubled_source(44100, 1.0f); doubled_source.process(0.0f); require(std::abs(doubled_source.process(0.02f) - 2.0f * response) < 1e-5f, "radiation source must remain linear instead of saturating"); require(std::abs(units::celsius(25.0) - 298.15) < 1e-9 && std::abs(units::area_from_diameter(0.02) - static_cast(pi) * 1e-4) < 1e-12, "unit helpers must preserve the C# conversions"); DynoCurve dyno_curve; dyno_curve.update(1000.0f, 10.0f, 50.0f); dyno_curve.update(1150.0f, 22.0f, 80.0f); require(dyno_curve.bins().size() == 24 && std::abs(dyno_curve.bins()[21].power_kw - 14.0f) < 1e-5f && std::abs(dyno_curve.bins()[22].torque_nm - 70.0f) < 1e-5f, "dyno history must interpolate skipped 50-RPM bins like the C# scenario base"); dyno_curve.update(1150.0f, 5.0f, 40.0f); require(std::abs(dyno_curve.bins()[23].power_kw - 22.0f) < 1e-5f && std::abs(dyno_curve.bins()[23].torque_nm - 80.0f) < 1e-5f, "dyno bins must retain their peak power and torque"); dyno_curve.reset(); require(dyno_curve.bins().empty(), "dyno reset must clear accumulated history"); Crankshaft crank; FourStrokeCylinder cylinder; cylinder.initialize(crank); require(std::abs(cylinder.compute_volume(0.0f) - cylinder.clearance_volume()) < 1e-8f, "cylinder volume at TDC must equal clearance volume"); require(std::abs(cylinder.compute_volume(pi) - cylinder.clearance_volume() - cylinder.swept_volume()) < 1e-8f, "cylinder volume at BDC must include the swept volume"); TwoStrokeCylinder two_stroke{0.054f, 0.0545f, 0.110f, 7.2f, 140.0f, 195.0f}; two_stroke.initialize(crank); two_stroke.intake_valve_diameter = 0.030f; two_stroke.intake_valve_lift = 0.010f; two_stroke.exhaust_valve_diameter = 0.040f; two_stroke.exhaust_valve_lift = 0.010f; require(two_stroke.valve_area(180.0f, true) > 0.0f && two_stroke.valve_area(0.0f, true) == 0.0f, "two-stroke transfer port must open around BDC only"); require(two_stroke.valve_area(180.0f, false) > 0.0f && two_stroke.valve_area(0.0f, false) == 0.0f, "two-stroke exhaust port must open around BDC only"); TwoStrokeScenario two_stroke_scenario; two_stroke_scenario.set_throttle(0.25f); for (int i = 0; i < 2000; ++i) two_stroke_scenario.process(); const auto two_stroke_state = two_stroke_scenario.snapshot(); require(std::isfinite(two_stroke_state.rpm) && std::isfinite(two_stroke_state.cylinder_pressure) && std::isfinite(two_stroke_state.crankcase_pressure) && std::isfinite(two_stroke_state.intake_pressure), "two-stroke scenario must remain finite while running"); Crankcase crankcase{crank, 0.02725f, 0.110f, 0.054f, 150e-6f}; two_stroke.set_crankcase(crankcase); crankcase.pre_step(1.0f / 44100.0f); require(std::isfinite(crankcase.pressure()) && crankcase.pressure() > 0.0f && crankcase.volume() > 0.0f, "crankcase must retain a finite thermodynamic state"); Atmosphere atmosphere; require(std::abs(atmosphere.port.pressure - 101325.0f) < 1e-3f && std::abs(atmosphere.port.air_fraction - 1.0f) < 1e-6f, "atmosphere must expose ambient air through its port"); OutdoorExhaustReverb reverb{44100}; bool reverb_tail = false; for (int i = 0; i < 12000; ++i) { const AudioFrame output = reverb.process_stereo(i == 0 ? 0.8f : 0.0f); require(std::isfinite(output.left) && std::isfinite(output.right), "outdoor reverb must remain finite after a transient"); if (i > 300 && (std::abs(output.left) > 1e-5f || std::abs(output.right) > 1e-5f)) reverb_tail = true; } require(reverb_tail, "outdoor reverb must produce a delayed reflection tail"); SoundProcessor flow_sound{44100, 1.0f}; for (int i = 0; i < 2000; ++i) require(std::isfinite(flow_sound.process(i < 1000 ? 0.0f : 0.01f)), "flow sound processor must remain finite for a flow step"); HelmholtzScenario helmholtz; float resonator_peak = 0.0f; for (int i = 0; i < 3000; ++i) { const float sample = helmholtz.process(); require(std::isfinite(sample) && std::isfinite(helmholtz.cavity_pressure()), "Helmholtz resonator must remain finite while discharging"); resonator_peak = std::max(resonator_peak, std::abs(sample)); } require(resonator_peak > 0.0f, "overpressurized Helmholtz cavity must excite the neck"); PulseTestScenario pulse_test; for (int i = 0; i < 3000; ++i) { const float sample = pulse_test.process(); require(std::isfinite(sample) && std::isfinite(pulse_test.pipe().state(0).pressure), "pulse test pipe must remain finite while the pressure pulse travels"); } Volume0D chamber{1e-3f}; Port& chamber_port = chamber.create_port(); std::vector pipes; pipes.emplace_back(1e-3f, 0.1f, 4); BoundarySystem boundary{pipes}; float closed_area = 0.0f; boundary.add_orifice(chamber_port, 0, false, closed_area); const float initial_pressure = chamber.pressure(); boundary.resolve(1e-5f); require(chamber_port.mass_flow == 0.0f, "closed orifice must have zero mass flow"); require(std::abs(chamber.pressure() - initial_pressure) < 1e-2f, "resolving a closed orifice must not alter the chamber state"); Volume0D equal_pressure_volume{1e-3f}; Port& equal_pressure_port = equal_pressure_volume.create_port(); equal_pressure_port.density = 1.0f; std::vector equal_pressure_pipes; equal_pressure_pipes.emplace_back(1e-3f, 0.1f, 4); BoundarySystem equal_pressure_boundary{equal_pressure_pipes}; float equal_pressure_area = 1e-4f; equal_pressure_boundary.add_orifice(equal_pressure_port, 0, false, equal_pressure_area); equal_pressure_boundary.resolve(1e-6f); require(std::abs(equal_pressure_pipes.front().right_ghost.rho - equal_pressure_port.density) < 1e-6f, "equal-pressure orifices must retain the volume-side density in the ghost state"); Volume0D configurable_volume{1e-3f, 150000.0f, 350.0f, 300.0f, 1.3f}; auto& configurable_port = configurable_volume.create_port(); require(std::abs(configurable_volume.temperature() - 350.0f) < 0.01f && std::abs(configurable_port.enthalpy - 1.3f / 0.3f * 300.0f * 350.0f) < 1.0f, "0-D volumes must honor configurable gamma and gas constant"); configurable_volume.set_pressure(200000.0f, 400.0f); require(std::abs(configurable_volume.pressure() - 200000.0f) < 0.1f && std::abs(configurable_volume.temperature() - 400.0f) < 1e-3f, "setting volume pressure and temperature must update conserved mass and energy"); configurable_volume.energy_relaxation_rate = 0.0f; configurable_volume.volume_change_rate = 0.01f; const float before_volume_work = configurable_volume.pressure(); configurable_volume.update_state(1e-3f); require(configurable_volume.pressure() < before_volume_work && std::abs(configurable_volume.pressure() - before_volume_work * std::pow(1.0f / 1.01f, 1.3f)) < 1.0f, "moving-volume work must conserve the closed adiabatic p V^gamma invariant"); Pipe1D profiled_pipe{{0.001f, 0.002f, 0.003f}, {0.1f, 0.2f, 0.3f}}; require(std::abs(profiled_pipe.cell_mass(1) - 1.225f * 0.002f * 0.2f) < 1e-8f && std::abs(profiled_pipe.minimum_cell_length() - 0.1f) < 1e-8f, "profiled pipes must retain each cell's area and length for mass/CFL calculations"); Vehicle bike; bike.set_first_gear(); const float rpm = 10000.0f; const float first_gear_speed = (rpm * 2.0f * pi / 60.0f) / bike.total_gear_ratio() * bike.wheel_radius * 3.6f; require(first_gear_speed > 55.0f && first_gear_speed < 65.0f, "first gear should be geared for about 60 km/h at the rev limit"); Vehicle neutral; neutral.update(500.0f, 0.04f, 1e-3f); require(neutral.speed == 0.0f, "neutral must isolate the driveline"); bike.clutch_input = 0.0f; for (int i = 0; i < 2000; ++i) bike.update(500.0f, 0.04f, 1e-3f); require(bike.speed > 0.0f && bike.kinetic_energy() > 0.0f, "clutch impulse must transfer engine angular momentum into the vehicle"); FourStrokeTwinScenario scenario; scenario.throttle = 0.35f; for (int i = 0; i < 10000; ++i) scenario.process(); require(std::isfinite(scenario.crankshaft.omega) && scenario.crankshaft.omega > 0.0f, "twin scenario must retain a finite running crank speed"); require(std::isfinite(scenario.intake_plenum.pressure()) && std::isfinite(scenario.exhaust_collector.pressure()), "shared intake and exhaust volumes must remain finite"); require(scenario.cylinder1.pressure > 0.0f && scenario.cylinder2.pressure > 0.0f, "both cylinders must retain positive pressure"); FourStrokeTwinScenario idling; for (int i = 0; i < 6 * 44100; ++i) idling.process(); const auto idle_state = idling.snapshot(); require(idle_state.idle_control && idle_state.rpm > 1000.0f && idle_state.rpm < 3000.0f, "ECU idle control must sustain the unloaded twin without artificial torque"); require(idle_state.throttle > 0.0f && idle_state.pedal == 0.0f, "idle control must open the butterfly independently of the pedal"); for (int i = 0; i < 1000; ++i) { const AudioFrame frame = scenario.process(); require(std::isfinite(frame.left) && std::isfinite(frame.right), "the integrated fluid-derived audio path must remain finite"); } std::cout << "FluidSim C++ physics tests passed\n"; return 0; }