Add turbocharged twin scenario and cross-platform test harness
This commit is contained in:
75
tests/b205_regression.cpp
Normal file
75
tests/b205_regression.cpp
Normal file
@@ -0,0 +1,75 @@
|
||||
#include "FluidSim/Scenario.hpp"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
using namespace fluidsim::port;
|
||||
namespace {
|
||||
void check(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
}
|
||||
int main() {
|
||||
SaabB205Scenario s;
|
||||
const auto cylinders = s.cylinders();
|
||||
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");
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
check(cylinders[i]->compression == 8.8f && cylinders[i]->intake_valve_count == 2 &&
|
||||
cylinders[i]->exhaust_valve_count == 2, "B205 compression/valve count incorrect");
|
||||
}
|
||||
s.set_controls(.5f, 0, 1);
|
||||
std::array<bool, 4> burning{};
|
||||
std::vector<int> firing_order;
|
||||
float peak = 0;
|
||||
const auto started = std::chrono::steady_clock::now();
|
||||
const auto step = [&] {
|
||||
const auto audio = s.process();
|
||||
check(std::isfinite(audio.left) && std::isfinite(audio.right), "nonfinite B205 audio");
|
||||
peak = std::max({peak, std::abs(audio.left), std::abs(audio.right)});
|
||||
for (const auto* c : cylinders)
|
||||
check(std::isfinite(c->pressure) && c->pressure > 0 && c->total_mass() > 0,
|
||||
"nonphysical B205 cylinder");
|
||||
};
|
||||
// Prescribed-speed absorbing dyno isolates phasing from acceleration.
|
||||
for (int sample = 0; sample < 44100; ++sample) {
|
||||
s.crankshaft.omega = 2000 * 2*pi/60;
|
||||
step();
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
if (sample > 22050 && cylinders[i]->combustion_active && !burning[i])
|
||||
firing_order.push_back(i);
|
||||
burning[i] = cylinders[i]->combustion_active;
|
||||
}
|
||||
}
|
||||
check(firing_order.size() >= 28 && firing_order.size() <= 36, "B205 did not fire twice per revolution");
|
||||
constexpr std::array<int, 4> next{2, 0, 3, 1};
|
||||
for (std::size_t i = 1; i < firing_order.size(); ++i)
|
||||
check(firing_order[i] == next[firing_order[i-1]], "B205 firing order is not 1-3-4-2");
|
||||
s.set_first_gear();
|
||||
for (int sample = 0; sample < 4*44100; ++sample) {
|
||||
const float clutch = std::max(0.f, 1-sample/(.8f*44100));
|
||||
s.set_controls(.6f, 0, clutch);
|
||||
step();
|
||||
}
|
||||
check(s.vehicle.speed > 1 && s.vehicle.current_gear == 1, "B205 failed to launch wagon");
|
||||
const float speed = s.vehicle.speed*3.6f;
|
||||
for (int i = 0; i < 8; ++i) s.shift_up();
|
||||
check(s.vehicle.current_gear == 5, "wagon has a phantom sixth gear");
|
||||
check(std::abs(s.vehicle.total_gear_ratio()-2.67f) < 1e-5f, "wagon top overall ratio incorrect");
|
||||
s.set_neutral();
|
||||
s.set_controls(0, 0, 1);
|
||||
for (int sample = 0; sample < 3*44100; ++sample) step();
|
||||
const auto final = s.snapshot();
|
||||
check(final.rpm > 300 && final.rpm < 2500, "B205 did not recover toward idle");
|
||||
check(final.cylinder_count == 4 && !final.pipe_states[9].empty(), "B205 desktop snapshot incomplete");
|
||||
for (const auto& pipe : s.pipes)
|
||||
check(pipe.positivity_repairs == 0, "B205 required pipe positivity repairs");
|
||||
const double wall = std::chrono::duration<double>(std::chrono::steady_clock::now()-started).count();
|
||||
std::cout << "B205 passed: firing events=" << firing_order.size() << ", wagon speed=" << speed
|
||||
<< " km/h, recovered RPM=" << final.rpm << ", audio peak=" << peak
|
||||
<< ", wall seconds=" << wall << " for 8 simulated seconds\n";
|
||||
}
|
||||
625
tests/fluid_validation.cpp
Normal file
625
tests/fluid_validation.cpp
Normal file
@@ -0,0 +1,625 @@
|
||||
#include "FluidSim/Atmosphere.hpp"
|
||||
#include "FluidSim/Crankcase.hpp"
|
||||
#include "FluidSim/Fluid.hpp"
|
||||
#include "FluidSim/Solver.hpp"
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
using namespace fluidsim::port;
|
||||
namespace {
|
||||
void check(bool b, const std::string &s) {
|
||||
if (!b)
|
||||
throw std::runtime_error(s);
|
||||
}
|
||||
void near(double a, double b, double tol, const char *s) {
|
||||
check(std::isfinite(a) && std::abs(a - b) <= tol, s);
|
||||
}
|
||||
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};
|
||||
}
|
||||
void lossless(Pipe1D &p) {
|
||||
p.damping_multiplier = 0;
|
||||
p.energy_relaxation_rate = 0;
|
||||
}
|
||||
double mass(const Pipe1D &p) {
|
||||
double v = 0;
|
||||
for (int i = 0; i < int(p.cells.size()); i++)
|
||||
v += p.cell_mass(i);
|
||||
return v;
|
||||
}
|
||||
double energy(const Pipe1D &p) {
|
||||
double v = 0;
|
||||
for (int i = 0; i < int(p.cells.size()); i++)
|
||||
v += double(p.cells[i].energy) * p.cell_area(i) * p.cell_length(i);
|
||||
return v;
|
||||
}
|
||||
double vol_energy(const Volume0D &v) { return v.pressure() * v.volume() / (v.gamma - 1); }
|
||||
} // namespace
|
||||
int main() {
|
||||
int failed = 0, total = 0;
|
||||
auto test = [&](const char *n, std::function<void()> fn) {
|
||||
++total;
|
||||
try {
|
||||
fn();
|
||||
std::cout << "PASS " << n << '\n';
|
||||
} catch (const std::exception &e) {
|
||||
++failed;
|
||||
std::cout << "FAIL " << n << ": " << e.what() << '\n';
|
||||
}
|
||||
};
|
||||
test("invalid uniform geometry", [] {
|
||||
bool caught = false;
|
||||
try {
|
||||
Pipe1D p(0, .1f, 4);
|
||||
} catch (const std::invalid_argument &) {
|
||||
caught = true;
|
||||
}
|
||||
check(caught, "zero area accepted");
|
||||
});
|
||||
test("closed pipe conservation", [] {
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, 1, 50);
|
||||
lossless(p[0]);
|
||||
for (int i = 0; i < 50; i++)
|
||||
set(p[0], i, i < 25 ? 120000 : 100000, 1.2f);
|
||||
BoundarySystem b(p);
|
||||
b.add_closed_end(0, true);
|
||||
b.add_closed_end(0, false);
|
||||
Solver s(p, b);
|
||||
auto m = mass(p[0]), e = energy(p[0]);
|
||||
for (int i = 0; i < 2000; i++)
|
||||
s.step(1.f / 44100);
|
||||
near(mass(p[0]), m, m * 2e-5, "closed mass drift");
|
||||
near(energy(p[0]), e, e * 2e-5, "closed energy drift");
|
||||
});
|
||||
test("profiled pipe conservation", [] {
|
||||
std::vector<float> a(40), l(40, .025f);
|
||||
for (int i = 0; i < 40; i++)
|
||||
a[i] = .001f * (1 + .5f * std::sin(i * .1f));
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(a, l);
|
||||
lossless(p[0]);
|
||||
for (int i = 0; i < 40; i++)
|
||||
set(p[0], i, i < 20 ? 120000 : 100000, 1.2f);
|
||||
BoundarySystem b(p);
|
||||
b.add_closed_end(0, true);
|
||||
b.add_closed_end(0, false);
|
||||
Solver s(p, b);
|
||||
auto m = mass(p[0]), e = energy(p[0]);
|
||||
for (int i = 0; i < 1000; i++)
|
||||
s.step(1.f / 44100);
|
||||
near(mass(p[0]), m, m * 3e-5, "profile mass drift");
|
||||
near(energy(p[0]), e, e * 3e-5, "profile energy drift");
|
||||
});
|
||||
test("volume pipe sealed conservation", [] {
|
||||
Volume0D v(.001f, 180000, 400);
|
||||
v.energy_relaxation_rate = 0;
|
||||
auto &port = v.create_port();
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.0003f, .5f, 25);
|
||||
lossless(p[0]);
|
||||
BoundarySystem b(p);
|
||||
float a = .0001f;
|
||||
b.add_orifice(port, 0, true, a);
|
||||
b.add_closed_end(0, false);
|
||||
Solver s(p, b);
|
||||
s.add_component(v);
|
||||
auto m = mass(p[0]) + v.mass(), e = energy(p[0]) + vol_energy(v);
|
||||
for (int i = 0; i < 2000; i++)
|
||||
s.step(1.f / 44100);
|
||||
near(mass(p[0]) + v.mass(), m, m * 3e-5, "coupling creates/deletes mass");
|
||||
near(energy(p[0]) + vol_energy(v), e, e * 3e-5, "coupling creates/deletes energy");
|
||||
});
|
||||
test("equal pressure hot contact", [] {
|
||||
Volume0D v(.001f, 101325, 600);
|
||||
v.energy_relaxation_rate = 0;
|
||||
auto &port = v.create_port();
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, .2f, 20);
|
||||
lossless(p[0]);
|
||||
BoundarySystem b(p);
|
||||
float a = .001f;
|
||||
b.add_orifice(port, 0, true, a);
|
||||
b.add_closed_end(0, false);
|
||||
Solver s(p, b);
|
||||
s.add_component(v);
|
||||
double m = mass(p[0]) + v.mass();
|
||||
for (int i = 0; i < 500; i++)
|
||||
s.step(1.f / 44100);
|
||||
near(mass(p[0]) + v.mass(), m, m * 2e-5, "contact produces mass");
|
||||
near(v.pressure(), 101325, 5, "stationary contact pressure changes");
|
||||
});
|
||||
test("NASA choked nozzle mass flow", [] {
|
||||
Atmosphere v;
|
||||
v.pressure = 200000;
|
||||
v.update_state(0);
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, .2f, 10);
|
||||
for (int i = 0; i < 10; i++)
|
||||
set(p[0], i, 10000, 10000 / (gas_constant * 300));
|
||||
BoundarySystem b(p);
|
||||
float a = .0001f;
|
||||
b.add_orifice(v.port, 0, true, a, .8f);
|
||||
b.resolve(1e-8f);
|
||||
double expected = .8 * a * 200000 / std::sqrt(300.) *
|
||||
std::sqrt(fluidsim::port::gamma / gas_constant) *
|
||||
std::pow(2 / (fluidsim::port::gamma + 1),
|
||||
(fluidsim::port::gamma + 1) / (2 * (fluidsim::port::gamma - 1)));
|
||||
near(-v.port.mass_flow, expected, expected * 2e-5,
|
||||
"choked mdot differs from isentropic sonic-throat formula");
|
||||
});
|
||||
test("inertance units", [] {
|
||||
Atmosphere v;
|
||||
v.pressure = 102325;
|
||||
v.update_state(0);
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, .1f, 10);
|
||||
BoundarySystem b(p);
|
||||
float a = .0001f;
|
||||
b.add_orifice_with_inertance(v.port, 0, true, a, 1, .05f);
|
||||
b.resolve(1e-6f);
|
||||
double expected = a / .05 * 1000 * 1e-6;
|
||||
near(-v.port.mass_flow, expected, expected * 1e-4,
|
||||
"mass-flow inertance must satisfy d(mdot)/dt=A*dp/L");
|
||||
});
|
||||
test("moving volume adiabatic law", [] {
|
||||
Volume0D v(.001f);
|
||||
v.energy_relaxation_rate = 0;
|
||||
v.volume_change_rate = .0001f;
|
||||
for (int i = 0; i < 1000; i++)
|
||||
v.update(1e-3f);
|
||||
near(v.volume(), .0011, 1e-8, "volume work did not change geometric volume");
|
||||
near(v.pressure(), 101325 * std::pow(.001 / .0011, 1.4), 3, "p V^gamma not conserved");
|
||||
});
|
||||
test("profile at rest is well balanced", [] {
|
||||
std::vector<float> a(80), l(80, .01f);
|
||||
for (int i = 0; i < 80; i++)
|
||||
a[i] = .001f * (1 + .7f * std::sin(.1f * i));
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(a, l);
|
||||
lossless(p[0]);
|
||||
BoundarySystem b(p);
|
||||
b.add_closed_end(0, true);
|
||||
b.add_closed_end(0, false);
|
||||
Solver s(p, b);
|
||||
for (int j = 0; j < 1000; j++)
|
||||
s.step(1.f / 44100);
|
||||
for (int i = 0; i < 80; i++) {
|
||||
near(p[0].state(i).velocity, 0, 1e-3, "stationary nozzle accelerates");
|
||||
near(p[0].state(i).pressure, 101325, 2, "stationary nozzle pressure drifts");
|
||||
}
|
||||
});
|
||||
test("pipe connection all four orientations", [] {
|
||||
for (bool la : {false, true})
|
||||
for (bool lb : {false, true}) {
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, .3f, 20);
|
||||
p.emplace_back(.001f, .3f, 20);
|
||||
for (auto &q : p)
|
||||
lossless(q);
|
||||
for (int i = 0; i < 20; i++)
|
||||
set(p[0], i, 130000, 1.4f, 0, .2f);
|
||||
BoundarySystem b(p);
|
||||
b.add_connection(0, la, 1, lb);
|
||||
b.add_closed_end(0, !la);
|
||||
b.add_closed_end(1, !lb);
|
||||
Solver s(p, b);
|
||||
double m = mass(p[0]) + mass(p[1]), e = energy(p[0]) + energy(p[1]);
|
||||
for (int i = 0; i < 2000; i++)
|
||||
s.step(1.f / 44100);
|
||||
near(mass(p[0]) + mass(p[1]), m, m * 4e-5, "junction mass drift");
|
||||
near(energy(p[0]) + energy(p[1]), e, e * 4e-5, "junction energy drift");
|
||||
for (auto &q : p)
|
||||
check(q.positivity_repairs == 0, "junction positivity repair");
|
||||
}
|
||||
});
|
||||
test("split pipe matches unsplit pipe", [] {
|
||||
std::vector<Pipe1D> a, b;
|
||||
a.emplace_back(.001f, 1, 80);
|
||||
b.emplace_back(.001f, .5f, 40);
|
||||
b.emplace_back(.001f, .5f, 40);
|
||||
lossless(a[0]);
|
||||
lossless(b[0]);
|
||||
lossless(b[1]);
|
||||
for (int i = 0; i < 80; i++) {
|
||||
float pr = 101325 + 100 * std::exp(-std::pow((i - 30) / 5.f, 2));
|
||||
set(a[0], i, pr, 1.2f);
|
||||
set(b[i / 40], i % 40, pr, 1.2f);
|
||||
}
|
||||
BoundarySystem ba(a), bb(b);
|
||||
ba.add_closed_end(0, true);
|
||||
ba.add_closed_end(0, false);
|
||||
bb.add_closed_end(0, true);
|
||||
bb.add_closed_end(1, false);
|
||||
bb.add_connection(0, false, 1, true);
|
||||
Solver sa(a, ba), sb(b, bb);
|
||||
for (int j = 0; j < 300; j++) {
|
||||
sa.step(1.f / 44100);
|
||||
sb.step(1.f / 44100);
|
||||
}
|
||||
for (int i = 0; i < 80; i++)
|
||||
near(a[0].state(i).pressure, b[i / 40].state(i % 40).pressure, .1,
|
||||
"split junction changes solution");
|
||||
});
|
||||
test("three volumes shared donor conservation", [] {
|
||||
Volume0D a(.000001f, 1e6f, 500), b(.001f), c(.001f);
|
||||
a.energy_relaxation_rate = b.energy_relaxation_rate = c.energy_relaxation_rate = 0;
|
||||
auto &ap = a.create_port();
|
||||
auto &ap2 = a.create_port();
|
||||
auto &bp = b.create_port();
|
||||
auto &cp = c.create_port();
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, .1f, 10);
|
||||
p.emplace_back(.001f, .1f, 10);
|
||||
for (auto &q : p)
|
||||
lossless(q);
|
||||
BoundarySystem bs(p);
|
||||
float ar = .001f;
|
||||
bs.add_orifice(ap, 0, true, ar);
|
||||
bs.add_orifice(ap2, 1, true, ar);
|
||||
bs.add_orifice(bp, 0, false, ar);
|
||||
bs.add_orifice(cp, 1, false, ar);
|
||||
Solver s(p, bs);
|
||||
s.add_component(a);
|
||||
s.add_component(b);
|
||||
s.add_component(c);
|
||||
double m = mass(p[0]) + mass(p[1]) + a.mass() + b.mass() + c.mass();
|
||||
double e = energy(p[0]) + energy(p[1]) + vol_energy(a) + vol_energy(b) + vol_energy(c);
|
||||
for (int j = 0; j < 3000; j++)
|
||||
s.step(1.f / 44100);
|
||||
near(mass(p[0]) + mass(p[1]) + a.mass() + b.mass() + c.mass(), m, m * 5e-5,
|
||||
"shared donor mass drift");
|
||||
near(energy(p[0]) + energy(p[1]) + vol_energy(a) + vol_energy(b) + vol_energy(c), e, e * 5e-5,
|
||||
"shared donor energy drift");
|
||||
check(a.pressure() > 0, "donor vacuum");
|
||||
});
|
||||
test("open boundary reports actual conserved flux both ends", [] {
|
||||
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_open_end(0, left, 101325, .001f);
|
||||
b.add_closed_end(0, !left);
|
||||
double m = mass(p[0]);
|
||||
b.resolve(1e-6f);
|
||||
double dm = -b.open_end_mass_flow(0) * 1e-6;
|
||||
p[0].advance(1e-6f);
|
||||
near(mass(p[0]) - m, dm, 1e-11,
|
||||
"reported radiation flow differs from finite-volume mass exchange");
|
||||
}
|
||||
});
|
||||
test("duplicate and invalid boundary rejection", [] {
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, 1, 10);
|
||||
BoundarySystem b(p);
|
||||
b.add_closed_end(0, true);
|
||||
bool caught = false;
|
||||
try {
|
||||
b.add_open_end(0, true, 101325, .001f);
|
||||
} catch (const std::invalid_argument &) {
|
||||
caught = true;
|
||||
}
|
||||
check(caught, "duplicate face accepted");
|
||||
caught = false;
|
||||
try {
|
||||
b.add_closed_end(9, true);
|
||||
} catch (const std::invalid_argument &) {
|
||||
caught = true;
|
||||
}
|
||||
check(caught, "invalid pipe accepted");
|
||||
});
|
||||
test("supersonic outlet extrapolates", [] {
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, 1, 10);
|
||||
for (int i = 0; i < 10; i++)
|
||||
set(p[0], i, 101325, 1.2f, 700);
|
||||
BoundarySystem b(p);
|
||||
b.add_open_end(0, false, 50000, .001f);
|
||||
b.resolve(1e-6f);
|
||||
near(b.open_end_mass_flow(0), .001 * 1.2 * 700, 1e-5,
|
||||
"supersonic outlet responds to downstream pressure");
|
||||
});
|
||||
test("underdefined supersonic inlet rejected", [] {
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, 1, 10);
|
||||
set(p[0], 0, 101325, 1.2f, 700);
|
||||
BoundarySystem b(p);
|
||||
b.add_open_end(0, true, 101325, .001f);
|
||||
bool caught = false;
|
||||
try {
|
||||
b.resolve(1e-6f);
|
||||
} catch (const std::runtime_error &) {
|
||||
caught = true;
|
||||
}
|
||||
check(caught, "supersonic inlet silently accepted pressure-only data");
|
||||
});
|
||||
test("closed and open acoustic reflection signs", [] {
|
||||
for (bool closed : {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.;
|
||||
float dp = 100 * std::exp(-std::pow((x - .3) / .04, 2));
|
||||
set(p[0], i, 101325 + dp, 1.2f + dp / (c * c), dp / (1.2 * c));
|
||||
}
|
||||
BoundarySystem b(p);
|
||||
b.add_closed_end(0, true);
|
||||
if (closed)
|
||||
b.add_closed_end(0, false);
|
||||
else
|
||||
b.add_open_end(0, false, 101325, .001f);
|
||||
Solver s(p, b);
|
||||
s.minimum_substeps = 1;
|
||||
const int steps = int((1.4 / c) * 44100);
|
||||
for (int j = 0; j < steps; j++)
|
||||
s.step(1.f / 44100);
|
||||
float dp = p[0].state(119).pressure - 101325;
|
||||
check(closed ? dp > 30 : dp < -30, "wrong reflected pressure sign/amplitude");
|
||||
check(p[0].positivity_repairs == 0, "acoustic positivity repair");
|
||||
}
|
||||
});
|
||||
test("Sod shock tube convergence", [] {
|
||||
double previous = 1e9;
|
||||
for (int n : {100, 200, 400}) {
|
||||
Pipe1D p(1, 1, n);
|
||||
lossless(p);
|
||||
for (int i = 0; i < n; i++)
|
||||
set(p, i, i < n / 2 ? 1 : .1f, i < n / 2 ? 1 : .125f);
|
||||
double t = 0;
|
||||
while (t < .2) {
|
||||
p.left_ghost = p.state(0);
|
||||
p.right_ghost = p.state(n - 1);
|
||||
float dt = std::min(.2 - t, double(p.stable_timestep(.7f)));
|
||||
p.advance(dt);
|
||||
t += dt;
|
||||
}
|
||||
double error = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
double xi = ((i + .5) / n - .5) / .2;
|
||||
double rho;
|
||||
if (xi < -std::sqrt(1.4))
|
||||
rho = 1;
|
||||
else if (xi < -.0702728)
|
||||
rho = std::pow((2 / (1.4 + 1)) + (1.4 - 1) / (1.4 + 1) * (-xi / std::sqrt(1.4)),
|
||||
2 / (1.4 - 1));
|
||||
else if (xi < .9274526)
|
||||
rho = .4263194;
|
||||
else if (xi < 1.7521557)
|
||||
rho = .2655737;
|
||||
else
|
||||
rho = .125;
|
||||
error += std::abs(p.state(i).rho - rho) / n;
|
||||
}
|
||||
std::cout << " Sod n=" << n << " density L1=" << error << '\n';
|
||||
check(error < previous * .9, "shock density error does not decrease on refinement");
|
||||
previous = error;
|
||||
check(p.positivity_repairs == 0, "Sod positivity repair");
|
||||
}
|
||||
check(previous < .025, "Sod density error excessive");
|
||||
});
|
||||
test("strong pulse and near-vacuum rarefaction", [] {
|
||||
for (bool vacuum : {false, true}) {
|
||||
Pipe1D p(1, 1, 200);
|
||||
lossless(p);
|
||||
for (int i = 0; i < 200; i++)
|
||||
set(p, i, vacuum ? .4f : (i < 100 ? 1000 : .01f), 1, vacuum ? (i < 100 ? -2 : 2) : 0);
|
||||
double t = 0;
|
||||
while (t < .005) {
|
||||
p.left_ghost = p.state(0);
|
||||
p.right_ghost = p.state(199);
|
||||
float dt = std::min(.005 - t, double(p.stable_timestep(.5f)));
|
||||
p.advance(dt);
|
||||
t += dt;
|
||||
}
|
||||
for (int i = 0; i < 200; i++)
|
||||
check(p.state(i).rho > 0 && p.state(i).pressure > 0, "negative shock/vacuum state");
|
||||
check(p.positivity_repairs == 0, "stress case required conservation-breaking positivity repair");
|
||||
}
|
||||
});
|
||||
|
||||
test("small-signal Helmholtz resonance", [] {
|
||||
std::vector<Pipe1D> p;
|
||||
float area = .000314159f;
|
||||
p.emplace_back(area, .05f, 40);
|
||||
Volume0D v(.001f, 101335, 300);
|
||||
auto &port = v.create_port();
|
||||
BoundarySystem b(p);
|
||||
b.add_orifice(port, 0, true, area, 1);
|
||||
b.add_open_end(0, false, 101325, area);
|
||||
Solver solver(p, b);
|
||||
solver.minimum_substeps = 1;
|
||||
solver.add_component(v);
|
||||
double prev = v.pressure() - 101325, first = 0, last = 0;
|
||||
int count = 0;
|
||||
for (int i = 0; i < 4410; i++) {
|
||||
solver.step(1.f / 44100);
|
||||
double now = v.pressure() - 101325;
|
||||
check(std::abs(now) < 15, "small-signal cavity grows instead of oscillating/decaying");
|
||||
if (prev > 0 && now <= 0 && i > 441) {
|
||||
double t = (i + prev / (prev - now)) / 44100;
|
||||
if (!count)
|
||||
first = t;
|
||||
last = t;
|
||||
count++;
|
||||
}
|
||||
prev = now;
|
||||
}
|
||||
double frequency = (count - 1) / (last - first);
|
||||
double expected =
|
||||
std::sqrt(1.4 * 287 * 300) / (2 * 3.141592653589793) * std::sqrt(area / (.001 * .05));
|
||||
std::cout << " Helmholtz frequency=" << frequency << " analytic lumped=" << expected << '\n';
|
||||
check(count > 5, "not enough cavity oscillations");
|
||||
near(frequency, expected, expected * .05, "Helmholtz mode frequency error exceeds 5%");
|
||||
check(p[0].positivity_repairs == 0, "cavity requires positivity repairs");
|
||||
});
|
||||
test("invalid volume and timestep rejection", [] {
|
||||
bool caught = false;
|
||||
try {
|
||||
Volume0D v(-1);
|
||||
} catch (const std::invalid_argument &) {
|
||||
caught = true;
|
||||
}
|
||||
check(caught, "negative volume accepted");
|
||||
Pipe1D p(.001f, .1f, 10);
|
||||
caught = false;
|
||||
try {
|
||||
p.required_substeps(std::nanf(""));
|
||||
} catch (const std::invalid_argument &) {
|
||||
caught = true;
|
||||
}
|
||||
check(caught, "NaN timestep accepted");
|
||||
});
|
||||
test("fresh-air species conservation across connection", [] {
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, .5f, 40);
|
||||
p.emplace_back(.001f, .5f, 40);
|
||||
for (int k = 0; k < 2; k++) {
|
||||
lossless(p[k]);
|
||||
for (int i = 0; i < 40; i++)
|
||||
set(p[k], i, k ? 100000 : 120000, 1.2f, 0, k ? .9f : .1f);
|
||||
}
|
||||
auto air_mass = [&] {
|
||||
double sum = 0;
|
||||
for (auto &q : p)
|
||||
for (int i = 0; i < 40; i++)
|
||||
sum += q.cell_mass(i) * q.cells[i].air;
|
||||
return sum;
|
||||
};
|
||||
double before = air_mass();
|
||||
BoundarySystem b(p);
|
||||
b.add_closed_end(0, true);
|
||||
b.add_closed_end(1, false);
|
||||
b.add_connection(0, false, 1, true);
|
||||
Solver solver(p, b);
|
||||
for (int i = 0; i < 3000; i++)
|
||||
solver.step(1.f / 44100);
|
||||
near(air_mass(), before, before * 4e-5, "species created at shared face");
|
||||
});
|
||||
test("unsupported unequal junction rejected", [] {
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, .5f, 20);
|
||||
p.emplace_back(.002f, .5f, 20);
|
||||
BoundarySystem b(p);
|
||||
bool caught = false;
|
||||
try {
|
||||
b.add_connection(0, false, 1, true);
|
||||
} catch (const std::invalid_argument &) {
|
||||
caught = true;
|
||||
}
|
||||
check(caught, "unvalidated sudden area junction silently accepted");
|
||||
});
|
||||
test("unconnected endpoint rejected by solver", [] {
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, .5f, 20);
|
||||
BoundarySystem b(p);
|
||||
b.add_closed_end(0, true);
|
||||
Solver solver(p, b);
|
||||
bool caught = false;
|
||||
try {
|
||||
solver.step(1.f / 44100);
|
||||
} catch (const std::runtime_error &) {
|
||||
caught = true;
|
||||
}
|
||||
check(caught, "unconnected face silently used default ghost");
|
||||
});
|
||||
test("closed duct acoustic eigenfrequency", [] {
|
||||
for (int n : {50, 100, 200}) {
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, 1, n);
|
||||
double c = std::sqrt(1.4 * 101325 / 1.2);
|
||||
for (int i = 0; i < n; i++) {
|
||||
double dp = 100 * std::cos(3.141592653589793 * (i + .5) / n);
|
||||
set(p[0], i, 101325 + dp, 1.2f + dp / (c * c));
|
||||
}
|
||||
BoundarySystem b(p);
|
||||
b.add_closed_end(0, true);
|
||||
b.add_closed_end(0, false);
|
||||
Solver solver(p, b);
|
||||
solver.minimum_substeps = 1;
|
||||
double prev = 1, first = 0, last = 0;
|
||||
int count = 0;
|
||||
for (int j = 0; j < 1764; j++) {
|
||||
solver.step(1.f / 44100);
|
||||
double projection = 0;
|
||||
for (int i = 0; i < n; i++)
|
||||
projection +=
|
||||
(p[0].state(i).pressure - 101325) * std::cos(3.141592653589793 * (i + .5) / n);
|
||||
if (prev > 0 && projection <= 0) {
|
||||
double t = (j + prev / (prev - projection)) / 44100;
|
||||
if (!count)
|
||||
first = t;
|
||||
last = t;
|
||||
count++;
|
||||
}
|
||||
prev = projection;
|
||||
}
|
||||
double f = (count - 1) / (last - first);
|
||||
std::cout << " closed mode n=" << n << " f=" << f << " analytic=" << c / 2 << '\n';
|
||||
check(count > 4, "insufficient acoustic cycles");
|
||||
near(f, c / 2, c / 2 * .01, "closed duct frequency exceeds 1% error");
|
||||
}
|
||||
});
|
||||
test("inertance impulse and dissipative resistance", [] {
|
||||
Atmosphere v;
|
||||
v.pressure = 102325;
|
||||
v.update_state(0);
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, .1f, 10);
|
||||
BoundarySystem b(p);
|
||||
float area = .0001f;
|
||||
b.add_orifice_with_inertance(v.port, 0, true, area, 1, .05f, 1);
|
||||
for (int j = 0; j < 10000; j++)
|
||||
b.resolve(.0001f);
|
||||
double equilibrium = std::sqrt(2 * v.density() * area * area * 1000);
|
||||
near(-v.port.mass_flow, equilibrium, equilibrium * .001,
|
||||
"inertance resistance steady flow has wrong units or sign");
|
||||
});
|
||||
test("incompatible gas coupling rejected", [] {
|
||||
Volume0D v(.001f, 101325, 300, 300, 1.3f);
|
||||
auto &port = v.create_port();
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, .1f, 10);
|
||||
BoundarySystem b(p);
|
||||
float area = .0001f;
|
||||
bool caught = false;
|
||||
try {
|
||||
b.add_orifice(port, 0, true, area);
|
||||
} catch (const std::invalid_argument &) {
|
||||
caught = true;
|
||||
}
|
||||
check(caught, "different gamma gases silently coupled");
|
||||
});
|
||||
test("thermal relaxation matches analytic energy decay", [] {
|
||||
Volume0D v(.001f, 101325, 600);
|
||||
v.energy_relaxation_rate = 2;
|
||||
double initial = vol_energy(v), target = v.mass() * 287 * 300 / (1.4 - 1);
|
||||
for (int i = 0; i < 1000; i++)
|
||||
v.update(.001f);
|
||||
near(vol_energy(v), target + (initial - target) * std::exp(-2), initial * 3e-5,
|
||||
"thermal relaxation differs from exact exponential");
|
||||
});
|
||||
|
||||
test("sealed crankcase compression conserves mass above five bar", [] {
|
||||
Crankshaft crank;
|
||||
crank.angle = 0;
|
||||
Crankcase v(crank, .02725f, .11f, .054f, 1e-5f);
|
||||
double m = v.mass(), invariant = v.pressure() * std::pow(v.volume(), 1.4);
|
||||
for (int i = 1; i <= 1000; i++) {
|
||||
crank.angle = pi * i / 1000;
|
||||
v.pre_step(1e-5f);
|
||||
v.update_state(1e-5f);
|
||||
}
|
||||
near(v.mass(), m, m * 1e-6, "crankcase pressure cap deletes mass");
|
||||
near(v.pressure() * std::pow(v.volume(), 1.4), invariant, invariant * 1e-4,
|
||||
"crankcase compression violates adiabatic invariant");
|
||||
check(v.pressure() > 5e5, "compression did not exercise old hard pressure cap");
|
||||
});
|
||||
std::cout << total - failed << "/" << total << " passed\n";
|
||||
return failed ? 1 : 0;
|
||||
}
|
||||
51
tests/intake_regression.cpp
Normal file
51
tests/intake_regression.cpp
Normal file
@@ -0,0 +1,51 @@
|
||||
#include "FluidSim/Scenario.hpp"
|
||||
#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);
|
||||
}
|
||||
void run(FourStrokeTwinScenario &s, float seconds, float pedal) {
|
||||
s.throttle = pedal;
|
||||
for (int i = 0; i < int(std::lround(seconds * 44100)); ++i) {
|
||||
const auto audio = s.process();
|
||||
check(std::isfinite(audio.left) && std::isfinite(audio.right), "nonfinite transient audio");
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
int main() {
|
||||
FourStrokeTwinScenario s;
|
||||
run(s, 3, 0);
|
||||
run(s, .5f, .8f);
|
||||
run(s, 4.5f, 0);
|
||||
const float idle_map = s.intake_plenum.pressure();
|
||||
check(idle_map > 20000 && idle_map < 80000, "idle MAP did not recover after first blip");
|
||||
run(s, 1, 1);
|
||||
const float open_map = s.intake_plenum.pressure();
|
||||
const float open_temperature = s.intake_plenum.temperature();
|
||||
const auto inlet = s.pipes.front().state(0);
|
||||
const float inlet_temperature = inlet.pressure / (inlet.rho * gas_constant);
|
||||
check(open_map > 75000 && open_map > idle_map + 20000,
|
||||
"opening throttle did not relieve manifold vacuum");
|
||||
check(open_temperature > 250 && open_temperature < 400,
|
||||
"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");
|
||||
check(recovered.intake_pressure > 20000 && recovered.intake_pressure < 80000,
|
||||
"manifold remains evacuated after idle recovery");
|
||||
for (const auto &p : s.pipes)
|
||||
check(p.positivity_repairs == 0, "intake transient required positivity repairs");
|
||||
std::cout << "Intake regression passed: idle_MAP=" << idle_map / 1000
|
||||
<< " kPa, open_MAP=" << open_map / 1000 << " kPa, open_T=" << open_temperature
|
||||
<< " K, inlet_T=" << inlet_temperature
|
||||
<< " K, recovered_MAP=" << recovered.intake_pressure / 1000
|
||||
<< " kPa, recovered_RPM=" << recovered.rpm << '\n';
|
||||
}
|
||||
114
tests/outdoor_audio_tests.cpp
Normal file
114
tests/outdoor_audio_tests.cpp
Normal file
@@ -0,0 +1,114 @@
|
||||
#include "FluidSim/OutdoorAudio.hpp"
|
||||
#include "FluidSim/ReedValve.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
using namespace fluidsim::port;
|
||||
namespace {
|
||||
void check(bool c, const char *m) {
|
||||
if (!c)
|
||||
throw std::runtime_error(m);
|
||||
}
|
||||
OutdoorAudioConfig config() {
|
||||
OutdoorAudioConfig c;
|
||||
c.exhaust_position = {0, 1, 0};
|
||||
c.listener = {0, 1, 3};
|
||||
c.ear_spacing = 0;
|
||||
c.wall_x = -2;
|
||||
return c;
|
||||
}
|
||||
} // namespace
|
||||
int main() {
|
||||
// Geometric reflections must not arrive before the direct path or before
|
||||
// the ground image's path length, even with the diffuse network enabled.
|
||||
OutdoorAudio dry(44100, config()), wall(44100, config());
|
||||
dry.set_scene(OutdoorScene::Dry);
|
||||
dry.set_monitor_filter(false);
|
||||
wall.set_monitor_filter(false);
|
||||
for (int i = 0; i < 4410; i++) {
|
||||
dry.process(0, 0);
|
||||
wall.process(0, 0);
|
||||
}
|
||||
int first = -1, first_reflection = -1;
|
||||
double tail_energy = 0, late_energy = 0;
|
||||
float peak = 0;
|
||||
for (int i = 0; i < 88200; i++) {
|
||||
const auto d = dry.process(0, .001f), w = wall.process(0, .001f);
|
||||
if (first < 0 && std::abs(d.left) > 1e-9)
|
||||
first = i;
|
||||
if (first_reflection < 0 && std::abs(w.left - d.left) > 1e-9)
|
||||
first_reflection = i;
|
||||
peak = std::max(peak, std::abs(w.left));
|
||||
if (i > 2000 && i < 10000)
|
||||
tail_energy += w.left * w.left + w.right * w.right;
|
||||
if (i > 60000)
|
||||
late_energy += w.left * w.left + w.right * w.right;
|
||||
check(std::isfinite(w.left) && std::isfinite(w.right), "unstable outdoor tail");
|
||||
}
|
||||
check(first == 385, "direct fractional propagation delay is wrong");
|
||||
check(first_reflection >= 463 && first_reflection <= 465, "ground image reflection time is wrong");
|
||||
check(tail_energy > 0 && late_energy < tail_energy * 1e-6, "outdoor tail does not decay");
|
||||
check(peak < 1, "impulse reflection exceeds device range");
|
||||
|
||||
OutdoorAudio filtered(44100, config()), unfiltered(44100, config()), doubled(44100, config());
|
||||
for (auto *p : {&filtered, &unfiltered, &doubled})
|
||||
p->set_scene(OutdoorScene::Dry);
|
||||
unfiltered.set_monitor_filter(false);
|
||||
float prevf = 0, prevu = 0, maxf = 0, maxu = 0;
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
const float q = i == 0 ? .001f : 0;
|
||||
auto f = filtered.process(0, q), u = unfiltered.process(0, q), twice = doubled.process(0, 2 * q);
|
||||
maxf = std::max(maxf, std::abs(f.left - prevf));
|
||||
maxu = std::max(maxu, std::abs(u.left - prevu));
|
||||
prevf = f.left;
|
||||
prevu = u.left;
|
||||
check(std::abs(twice.left - 2 * f.left) < 1e-7, "monitoring path is nonlinear");
|
||||
}
|
||||
check(maxf < .6f * maxu, "monitor bandwidth fails to reduce adjacent-sample impulse steps");
|
||||
|
||||
auto near_config = config(), far_config = config();
|
||||
near_config.listener.z = 1;
|
||||
far_config.listener.z = 2;
|
||||
OutdoorAudio near(44100, near_config), far(44100, far_config);
|
||||
near.set_scene(OutdoorScene::Dry);
|
||||
far.set_scene(OutdoorScene::Dry);
|
||||
near.set_monitor_filter(false);
|
||||
far.set_monitor_filter(false);
|
||||
double sum_near = 0, sum_far = 0;
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
sum_near += near.process(0, .001f).left;
|
||||
sum_far += far.process(0, .001f).left;
|
||||
}
|
||||
check(std::abs(sum_near - 2 * sum_far) < 1e-7, "direct spreading does not follow inverse distance");
|
||||
|
||||
auto stereo_config = config();
|
||||
stereo_config.ear_spacing = .18f;
|
||||
stereo_config.exhaust_position.x = 1;
|
||||
OutdoorAudio stereo(44100, stereo_config);
|
||||
stereo.set_scene(OutdoorScene::Dry);
|
||||
stereo.set_monitor_filter(false);
|
||||
int left = -1, right = -1;
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
auto a = stereo.process(0, .001f);
|
||||
if (left < 0 && std::abs(a.left) > 1e-9)
|
||||
left = i;
|
||||
if (right < 0 && std::abs(a.right) > 1e-9)
|
||||
right = i;
|
||||
}
|
||||
check(right < left, "source on right does not reach right ear first");
|
||||
|
||||
ReedValve reed;
|
||||
for (int i = 0; i < 10000; i++)
|
||||
reed.step(1.f / 44100, 100);
|
||||
const double inertia = reed.width * std::pow(reed.thickness, 3) / 12;
|
||||
const double expected = 100 * reed.width * std::pow(reed.length, 4) / (8 * reed.youngs_modulus * inertia);
|
||||
check(std::abs(reed.lift() - expected) < expected * .001,
|
||||
"reed static compliance differs from cantilever equation");
|
||||
for (int i = 0; i < 1000; i++)
|
||||
reed.step(1.f / 44100, -1000);
|
||||
check(reed.lift() == 0, "reed does not seat under reverse pressure");
|
||||
std::cout << "Outdoor audio tests passed: direct=" << first << " samples, ground=" << first_reflection
|
||||
<< " samples, filtered step ratio=" << maxf / maxu << ", stereo lead=" << left - right
|
||||
<< " samples\n";
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
115
tests/turbo_physics.cpp
Normal file
115
tests/turbo_physics.cpp
Normal file
@@ -0,0 +1,115 @@
|
||||
#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";
|
||||
}
|
||||
43
tests/turbo_twin_regression.cpp
Normal file
43
tests/turbo_twin_regression.cpp
Normal file
@@ -0,0 +1,43 @@
|
||||
#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";
|
||||
}
|
||||
103
tests/two_stroke_physics.cpp
Normal file
103
tests/two_stroke_physics.cpp
Normal file
@@ -0,0 +1,103 @@
|
||||
#include "FluidSim/TwoStrokeScenario.hpp"
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
||||
using namespace fluidsim::port;
|
||||
namespace {
|
||||
void check(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
float reflected_pulse(bool expanding) {
|
||||
const float inlet = expanding ? .03f : .08f, outlet = expanding ? .08f : .03f;
|
||||
const std::array<PipeSection, 3> sections{{{.1f, inlet, inlet}, {.1f, inlet, outlet}, {.4f, outlet, outlet}}};
|
||||
auto pipe = make_profiled_pipe(sections, .002f);
|
||||
const float rho = 1.2f, pressure = 101325, c = std::sqrt(fluidsim::port::gamma*pressure/rho);
|
||||
float x = 0;
|
||||
for (std::size_t i = 0; i < pipe.cells.size(); ++i) {
|
||||
const float center = x + .5f * pipe.cell_lengths[i];
|
||||
const float dp = 500 * std::exp(-std::pow((center-.04f)/.006f, 2));
|
||||
const float density = rho + dp/(c*c), velocity = dp/(rho*c);
|
||||
pipe.cells[i] = {density, density*velocity,
|
||||
(pressure+dp)/(fluidsim::port::gamma-1) + .5f*density*velocity*velocity, 1};
|
||||
x += pipe.cell_lengths[i];
|
||||
}
|
||||
pipe.left_ghost = pipe.right_ghost = {rho, 0, pressure, 1};
|
||||
float extremum = 0;
|
||||
for (float time = 0; time < .0012f;) {
|
||||
const float dt = pipe.stable_timestep(.5f);
|
||||
pipe.advance(dt);
|
||||
time += dt;
|
||||
if (time > .00025f) {
|
||||
const float dp = pipe.state(29).pressure-pressure;
|
||||
extremum = expanding ? std::min(extremum, dp) : std::max(extremum, dp);
|
||||
}
|
||||
}
|
||||
check(pipe.positivity_repairs == 0, "taper pulse needed positivity repairs");
|
||||
return extremum;
|
||||
}
|
||||
}
|
||||
int main() {
|
||||
const std::array<PipeSection, 1> cone{{{.4f, .03f, .10f}}};
|
||||
const float expected = pi/12 * .4f * (.03f*.03f + .03f*.10f + .10f*.10f);
|
||||
for (float mesh : {.03f, .01f, .004f}) {
|
||||
const auto pipe = make_profiled_pipe(cone, mesh);
|
||||
double volume = 0;
|
||||
for (std::size_t i = 0; i < pipe.cells.size(); ++i)
|
||||
volume += pipe.cell_areas[i] * pipe.cell_lengths[i];
|
||||
check(std::abs(volume-expected) < expected*2e-6, "frustum mesh changes physical volume");
|
||||
}
|
||||
const float negative = reflected_pulse(true), positive = reflected_pulse(false);
|
||||
check(negative < -5, "diffuser did not return a rarefaction");
|
||||
check(positive > 5, "convergent did not return a compression");
|
||||
|
||||
TwoStrokeScenario s;
|
||||
auto& cylinder = s.cylinder;
|
||||
const float closed = cylinder.compute_volume(cylinder.exhaust_close()*pi/180);
|
||||
check(std::abs(closed/cylinder.clearance_volume()-8.2f) < 1e-4f,
|
||||
"trapped compression was confused with geometric compression");
|
||||
for (bool intake : {false, true}) {
|
||||
const float opens = intake ? cylinder.transfer_open() : cylinder.exhaust_open();
|
||||
const float width = intake ? cylinder.transfer_width : cylinder.exhaust_width;
|
||||
const float area = pi*cylinder.bore*cylinder.bore*.25f;
|
||||
const float expected_area = width * (cylinder.compute_volume(pi)-cylinder.compute_volume(opens*pi/180))/area;
|
||||
check(std::abs(cylinder.valve_area(180, intake)-expected_area) < 1e-8f,
|
||||
"port is not a piston-uncovered window");
|
||||
float previous = 0;
|
||||
for (float angle = opens; angle <= 180; angle += .5f) {
|
||||
const float current = cylinder.valve_area(angle, intake);
|
||||
check(current >= previous, "port does not open monotonically toward BDC");
|
||||
check(std::abs(current-cylinder.valve_area(360-angle, intake)) < 1e-8f,
|
||||
"piston port area is not symmetric");
|
||||
previous = current;
|
||||
}
|
||||
check(cylinder.valve_area(0, intake) == 0 && cylinder.valve_area(360, intake) == 0,
|
||||
"port is open at TDC");
|
||||
}
|
||||
s.ecu.step(1.f/44100, 1, 6000);
|
||||
const float low_spark = s.ecu.spark_advance();
|
||||
s.ecu.step(1.f/44100, 1, 17000);
|
||||
check(s.ecu.spark_advance() < low_spark && !s.ecu.fuel_cut(),
|
||||
"two-stroke high-speed ignition does not retard without cutting fuel");
|
||||
s.vehicle.current_gear = 1;
|
||||
const float first = s.vehicle.total_gear_ratio();
|
||||
s.vehicle.current_gear = 6;
|
||||
check(std::abs(first-30.27f)<.03f && std::abs(s.vehicle.total_gear_ratio()-13.4f)<.03f,
|
||||
"YZ125 primary/final/gear reduction incorrect");
|
||||
|
||||
Crankshaft crank;
|
||||
crank.omega = 100;
|
||||
crank.load_torque = 10;
|
||||
crank.friction_constant = 2;
|
||||
crank.add_torque(12);
|
||||
crank.step(1.f/44100);
|
||||
check(std::abs(crank.omega-100)<1e-6 && std::abs(crank.average_power-1000)<.01f &&
|
||||
std::abs(crank.average_torque-10)<1e-6,
|
||||
"loaded steady-speed dyno lost shaft power");
|
||||
crank.load_torque = 0;
|
||||
crank.friction_constant = 0;
|
||||
crank.friction_quadratic = .001f;
|
||||
crank.step(1.f/44100);
|
||||
check(crank.omega < 100 && crank.average_power < 1000, "windage is not dissipative");
|
||||
std::cout << "Two-stroke physics passed: diffuser reflection=" << negative
|
||||
<< " Pa, baffle reflection=" << positive << " Pa, first reduction=" << first << '\n';
|
||||
}
|
||||
87
tests/two_stroke_regression.cpp
Normal file
87
tests/two_stroke_regression.cpp
Normal file
@@ -0,0 +1,87 @@
|
||||
#include "FluidSim/TwoStrokeScenario.hpp"
|
||||
#include "FluidSim/DynoCurve.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
||||
using namespace fluidsim::port;
|
||||
|
||||
int main() {
|
||||
TwoStrokeScenario scenario;
|
||||
bool reed_open_seen = false, reed_closed_seen = false, above_old_limit = false;
|
||||
DynoCurve dyno;
|
||||
float peak = 0, maximum_rpm = 0;
|
||||
for (int sample = 0; sample < 14 * 44100; ++sample) {
|
||||
scenario.set_throttle(sample >= 2 * 44100 && sample < 6 * 44100 ? .6f : 0.f);
|
||||
const auto audio = scenario.process();
|
||||
if (!std::isfinite(audio.left) || !std::isfinite(audio.right))
|
||||
throw std::runtime_error("Nonfinite two-stroke audio");
|
||||
peak = std::max({peak, std::abs(audio.left), std::abs(audio.right)});
|
||||
if (sample % 44 != 0)
|
||||
continue;
|
||||
const auto state = scenario.snapshot();
|
||||
if (!std::isfinite(state.rpm) || !(state.cylinder_pressure > 0) ||
|
||||
!(state.crankcase_pressure > 0) || !(state.intake_pressure > 0))
|
||||
throw std::runtime_error("Invalid two-stroke physical state");
|
||||
if (scenario.ecu.rev_limited() || scenario.cylinder.rev_limit_active)
|
||||
throw std::runtime_error("Two-stroke rev limiter is active");
|
||||
if (sample >= 2 * 44100 && sample < 6 * 44100) {
|
||||
if (state.fuel_cut)
|
||||
throw std::runtime_error("Two-stroke cut fuel with the pedal open");
|
||||
above_old_limit |= state.rpm > 10200;
|
||||
}
|
||||
dyno.update(state.rpm, scenario.crankshaft.average_power / 1000,
|
||||
scenario.crankshaft.average_torque);
|
||||
reed_open_seen |= state.reed_open;
|
||||
reed_closed_seen |= !state.reed_open;
|
||||
maximum_rpm = std::max(maximum_rpm, state.rpm);
|
||||
}
|
||||
const auto final = scenario.snapshot();
|
||||
if (!above_old_limit)
|
||||
throw std::runtime_error("Two-stroke did not exceed the old rev limit");
|
||||
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");
|
||||
if (peak >= 1)
|
||||
throw std::runtime_error("Two-stroke audio exceeds device range");
|
||||
for (const auto &pipe : scenario.pipes)
|
||||
if (pipe.positivity_repairs != 0)
|
||||
throw std::runtime_error("Two-stroke required pipe positivity repairs");
|
||||
// Launch the same motorcycle used by the twin, then exercise gear controls.
|
||||
scenario.shift_up();
|
||||
scenario.set_controls(.7f, 0, 0);
|
||||
for (int sample = 0; sample < 6 * 44100; ++sample) {
|
||||
const auto audio = scenario.process();
|
||||
if (!std::isfinite(audio.left) || !std::isfinite(audio.right))
|
||||
throw std::runtime_error("Nonfinite motorcycle launch audio");
|
||||
if (scenario.ecu.rev_limited())
|
||||
throw std::runtime_error("Limiter re-enabled under motorcycle load");
|
||||
}
|
||||
if (scenario.vehicle.current_gear != 1 || scenario.vehicle.speed < 1 ||
|
||||
!std::isfinite(scenario.vehicle.kinetic_energy()) || scenario.vehicle.engagement < .9f)
|
||||
throw std::runtime_error("Two-stroke motorcycle did not launch with clutch engaged");
|
||||
scenario.shift_up();
|
||||
if (scenario.vehicle.current_gear != 2)
|
||||
throw std::runtime_error("Two-stroke did not shift up");
|
||||
scenario.shift_down();
|
||||
if (scenario.vehicle.current_gear != 1)
|
||||
throw std::runtime_error("Two-stroke did not shift down");
|
||||
scenario.set_neutral();
|
||||
if (scenario.vehicle.current_gear != 0)
|
||||
throw std::runtime_error("Two-stroke neutral selection failed");
|
||||
bool positive_power = false, positive_torque = false;
|
||||
for (const auto& point : dyno.bins()) {
|
||||
positive_power |= point.power_kw > 0;
|
||||
positive_torque |= point.torque_nm > 0;
|
||||
}
|
||||
if (!positive_power || !positive_torque)
|
||||
throw std::runtime_error("Two-stroke dyno did not record power and torque");
|
||||
for (const auto& pipe : scenario.pipes)
|
||||
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
|
||||
<< " km/h\n";
|
||||
}
|
||||
Reference in New Issue
Block a user