#include "FluidSim/Scenario.hpp" #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/ThreadLoadTracker.hpp" #include "FluidSim/Units.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; 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"); 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); 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 * 0.997f) < 1.0f, "moving-volume work must remove p*dV energy using the configured volume rate"); 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"); 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"); } 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; }