Add turbocharged twin scenario and cross-platform test harness

This commit is contained in:
2026-10-23 02:27:03 +02:00
parent a859740a95
commit 589d22aa5e
63 changed files with 4903 additions and 771 deletions

View File

@@ -1,4 +1,6 @@
#include "FluidSim/Scenario.hpp"
#include "FluidSim/SimulationRingBuffer.hpp"
#include <array>
#include "FluidSim/Atmosphere.hpp"
#include "FluidSim/Crankcase.hpp"
#include "FluidSim/DynoCurve.hpp"
@@ -26,6 +28,60 @@ void require(bool condition, const char* message) {
int main() {
using namespace fluidsim::port;
Ecu controller;
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<float, 4> 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<double>(pi) * 1e-4) < 1e-12,
"unit helpers must preserve the C# conversions");
@@ -42,6 +98,15 @@ 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 &&
@@ -159,8 +224,8 @@ int main() {
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");
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 &&
@@ -191,6 +256,14 @@ int main() {
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");
scenario.toggle_mechanical_sound();
for (int i = 0; i < 1000; ++i) {
const AudioFrame frame = scenario.process();