Add American V8 preset, carb fuel transport, and adaptive desktop audio

This commit is contained in:
2026-10-23 12:43:57 +02:00
parent 7a5c77c1c8
commit 85d4fb828a
36 changed files with 805 additions and 144 deletions

View File

@@ -15,8 +15,8 @@ void check(bool b, const std::string &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 set(Pipe1D &p, int i, float pressure, float rho, float u = 0, float air = 1, float fuel = 0) {
p.cells[i] = {rho, rho * u, pressure / pipe_gm1 + .5f * rho * u * u, air, fuel};
}
void lossless(Pipe1D &p) {
p.damping_multiplier = 0;
@@ -499,7 +499,8 @@ int main() {
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);
set(p[k], i, k ? 100000 : 120000, 1.2f, 0, k ? .9f : .1f,
k ? .02f : .05f);
}
auto air_mass = [&] {
double sum = 0;
@@ -508,7 +509,15 @@ int main() {
sum += q.cell_mass(i) * q.cells[i].air;
return sum;
};
auto fuel_mass = [&] {
double sum = 0;
for (auto &q : p)
for (int i = 0; i < 40; i++)
sum += q.cell_mass(i) * q.cells[i].fuel;
return sum;
};
double before = air_mass();
double fuel_before = fuel_mass();
BoundarySystem b(p);
b.add_closed_end(0, true);
b.add_closed_end(1, false);
@@ -517,6 +526,7 @@ int main() {
for (int i = 0; i < 3000; i++)
solver.step(1.f / 44100);
near(air_mass(), before, before * 4e-5, "species created at shared face");
near(fuel_mass(), fuel_before, fuel_before * 4e-5, "fuel species created at shared face");
});
test("unsupported unequal junction rejected", [] {
std::vector<Pipe1D> p;

View File

@@ -1,3 +1,4 @@
#include "FluidSim/Carburetor.hpp"
#include "FluidSim/TwoStrokeScenario.hpp"
#include <iostream>
#include <stdexcept>
@@ -37,6 +38,17 @@ float reflected_pulse(bool expanding) {
}
}
int main() {
Carburetor carburetor;
for (int i = 0; i < 1000; ++i)
carburetor.step(.001f, .55f, .55f, 8000.0f, .012f, 1.15f, 101325.0f, 90000.0f);
const float cruise_afr = carburetor.indicated_afr();
carburetor.step(.001f, 0.0f, .05f, 8000.0f, .004f, 1.15f, 101325.0f, 86000.0f);
check(carburetor.overrun_active() && carburetor.fuel_flow() > 0.0f,
"carburetor cut fuel when the throttle closed");
check(std::isfinite(cruise_afr) && cruise_afr >= 4.0f && cruise_afr <= 40.0f &&
carburetor.bowl_level() > 0.0f && carburetor.bowl_level() <= 1.0f,
"carburetor mixture or float bowl left its physical bounds");
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}) {
@@ -84,6 +96,16 @@ int main() {
check(std::abs(first-30.27f)<.03f && std::abs(s.vehicle.total_gear_ratio()-13.4f)<.03f,
"YZ125 primary/final/gear reduction incorrect");
s.set_throttle(.5f);
for (int sample = 0; sample < 22050; ++sample)
s.process();
bool transported_fuel = false;
for (int pipe_index : {1, 2})
for (const auto& cell : s.pipes[pipe_index].cells)
transported_fuel |= cell.fuel > 0.0f;
check(s.intake_plenum.fuel_mass() > 0.0f && s.crankcase.fuel_mass() > 0.0f && transported_fuel,
"carburetor fuel was not transported through intake, reed and crankcase");
Crankshaft crank;
crank.omega = 100;
crank.load_torque = 10;

View File

@@ -12,9 +12,17 @@ int main() {
bool reed_open_seen = false, reed_closed_seen = false, above_old_limit = false;
DynoCurve dyno;
float peak = 0, maximum_rpm = 0;
std::uint64_t release_firings = 0, one_second_coast_firings = 0;
float release_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 (sample == 6 * 44100) {
release_firings = scenario.cylinder.combustion_count();
release_rpm = scenario.snapshot().rpm;
} else if (sample == 7 * 44100) {
one_second_coast_firings = scenario.cylinder.combustion_count() - release_firings;
}
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)});
@@ -31,6 +39,8 @@ int main() {
throw std::runtime_error("Two-stroke cut fuel with the pedal open");
above_old_limit |= state.rpm > 10200;
}
if (sample >= 6 * 44100 && sample < 7 * 44100 && state.rpm > 5000 && state.fuel_cut)
throw std::runtime_error("Carbureted two-stroke cut fuel on overrun");
dyno.update(state.rpm, scenario.crankshaft.average_power / 1000,
scenario.crankshaft.average_torque);
reed_open_seen |= state.reed_open;
@@ -39,11 +49,16 @@ int main() {
}
const auto final = scenario.snapshot();
if (!above_old_limit)
throw std::runtime_error("Two-stroke did not exceed the old rev limit");
throw std::runtime_error("Two-stroke did not exceed the old rev limit: peak=" +
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");
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 (peak >= 1)
throw std::runtime_error("Two-stroke audio exceeds device range");
for (const auto &pipe : scenario.pipes)
@@ -83,5 +98,6 @@ int main() {
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";
<< " km/h, release RPM=" << release_rpm
<< ", first coast-second firings=" << one_second_coast_firings << '\n';
}