This commit is contained in:
2026-10-23 03:00:20 +02:00
parent 589d22aa5e
commit 7a5c77c1c8
31 changed files with 1196 additions and 44 deletions

View File

@@ -63,7 +63,7 @@ std::optional<ScenarioKind> choose_scenario(sf::RenderWindow& window, const sf::
const std::array<const char*, 4> descriptions{
"Four-stroke twin, motorcycle test and dyno",
"125 cc single, expansion chamber, motorcycle test and dyno",
"2.0 litre inline-four, no turbo yet, five-speed wagon and dyno",
"2.0 litre inline-four, GT17 turbo, ECU boost control and five-speed wagon",
"Coupled turbine/compressor, shaft inertia and pressure diagnostics"};
int selected = 0;
while (window.isOpen()) {
@@ -330,7 +330,7 @@ void draw_dashboard(sf::RenderWindow& window, const sf::Font* font, const Scenar
if (state.turbo_enabled) {
const auto& t = state.turbo;
std::ostringstream text;
text << std::fixed << std::setprecision(0) << "VZ21-size: " << t.rpm << " rpm "
text << std::fixed << std::setprecision(0) << (state.cylinder_count==4 ? "GT17-size: " : "VZ21-size: ") << t.rpm << " rpm "
<< std::setprecision(2) << "CPR " << t.compressor_pressure_ratio
<< " Charge " << t.charge_pressure/1000 << " kPa / " << std::setprecision(0) << t.charge_temperature << " K"
<< "\nTurbine inlet " << t.turbine_inlet_pressure/1000 << " kPa Outlet " << t.turbine_outlet_pressure/1000 << " kPa"
@@ -416,9 +416,15 @@ void draw_twin_scene(sf::RenderWindow& window, const sf::Font* font, const Scena
void draw_b205_scene(sf::RenderWindow& window, const sf::Font* font, const ScenarioSnapshot& s) {
window.clear({9, 12, 17});
draw_label(window, font, "SAAB B205 | 1985 cc inline-four | firing 1-3-4-2 | no turbo yet",
draw_label(window, font, s.turbo_enabled ? "SAAB B205 | GT17 turbo | ECU wastegate control" : "SAAB B205 | naturally aspirated baseline",
{260, 181}, sf::Color::White, 16);
draw_pipe(window, s, 0, 215, 25, 110);
if (s.turbo_enabled) {
draw_pipe(window, s, 10, 320, 25, 70);
draw_volume(window, float(s.turbo.charge_pressure), {{75, 300}, {35, 38}});
draw_graph_line(window, {90, 300}, {25, 215}, {80,145,200});
draw_label(window, font, "COMP", {75, 345}, sf::Color::White, 11);
}
draw_volume(window, s.intake_pressure, {{120, 195}, {65, 65}});
draw_label(window, font, "PLENUM", {120, 270}, sf::Color::White, 12);
draw_graph_line(window, {190, 211}, {820, 211}, {80, 145, 200});
@@ -436,8 +442,15 @@ void draw_b205_scene(sf::RenderWindow& window, const sf::Font* font, const Scena
{x, 445}, {180,190,205}, 12);
}
draw_volume(window, s.exhaust_pressure, {{850, 393}, {65, 48}});
draw_pipe(window, s, 9, 420, 920, 1240);
draw_label(window, font, "COLLECTOR / OPEN EXHAUST", {930, 390}, sf::Color::White, 12);
draw_pipe(window, s, 9, 420, 920, s.turbo_enabled ? 990 : 1240);
if (s.turbo_enabled) {
draw_volume(window, float(s.turbo.turbine_inlet_pressure), {{995, 397}, {55, 45}});
draw_pipe(window, s, 11, 420, 1055, 1240);
draw_label(window, font, "GT17", {1000, 450}, sf::Color::White, 12);
draw_label(window, font, "Boost target " + std::to_string(int(s.boost_target/1000)) + " kPa gauge | WG duty " + std::to_string(int(100*s.wastegate_duty)) + "%",
{25, 515}, sf::Color::White, 14);
}
draw_label(window, font, s.turbo_enabled ? "COLLECTOR / GT17 / EXHAUST" : "COLLECTOR / OPEN EXHAUST", {930, 375}, sf::Color::White, 12);
draw_label(window, font, "9-5 wagon baseline: 1675 kg with driver, five-speed manual, 4.05 final drive",
{25, 550}, sf::Color::White, 14);
draw_label(window, font, "Engine dimensions/firing from Saab WIS. Manifolds, cams, losses and ECU are estimates.",
@@ -465,6 +478,7 @@ int main(int argc, char** argv) {
ScenarioKind selected_kind=ScenarioKind::Twin;
bool scenario_from_args=false;
bool turbo_from_args=false;
std::string turbo_name;
TwoStrokeConfig two_stroke_config;
OutdoorScene selected_scene=OutdoorScene::Wall;
for(int i=1;i<argc;++i){
@@ -477,8 +491,9 @@ int main(int argc, char** argv) {
(value=="two-stroke" ? ScenarioKind::TwoStroke : ScenarioKind::Twin);
scenario_from_args=true;
}else if(option=="--turbo"){
if(value!="none"&&value!="vz21"){std::cerr<<"Choose none or vz21 turbo\n";return 2;}
turbo_from_args=value=="vz21";
if(value!="none"&&value!="vz21"&&value!="gt17"){std::cerr<<"Choose none, vz21 or gt17 turbo\n";return 2;}
turbo_name=value;
turbo_from_args=value!="none";
}else if(option=="--exhaust"){
if(value=="racing") two_stroke_config.exhaust=TwoStrokeExhaust::Racing;
else if(value=="straight") two_stroke_config.exhaust=TwoStrokeExhaust::Straight;
@@ -490,7 +505,7 @@ int main(int argc, char** argv) {
else if(value=="woodland")selected_scene=OutdoorScene::Woodland;
else {std::cerr<<"Choose dry, field, wall or woodland\n";return 2;}
}
}else{std::cerr<<"Usage: fluidsim_desktop [--scenario twin|two-stroke|b205] [--scene dry|field|wall|woodland] [--exhaust racing|straight] [--turbo none|vz21]\n";return 2;}
}else{std::cerr<<"Usage: fluidsim_desktop [--scenario twin|two-stroke|b205] [--scene dry|field|wall|woodland] [--exhaust racing|straight] [--turbo none|vz21|gt17]\n";return 2;}
}
const sf::Vector2u initial_size = scenario_from_args ? sf::Vector2u{window_width, window_height}
: sf::Vector2u{720, 606};
@@ -519,14 +534,17 @@ int main(int argc, char** argv) {
window.setView(sf::View(sf::FloatRect({0, 0}, {float(window_width), float(window_height)})));
}
if(turbo_from_args) {
if(selected_kind!=ScenarioKind::Twin && selected_kind!=ScenarioKind::TurboTwin) {
std::cerr<<"The VZ21 turbo test uses the twin scenario\n";return 2;
if(turbo_name=="gt17" && selected_kind==ScenarioKind::SaabB205) {
// Saab preset selects its GT17 calibration.
} else if(turbo_name=="vz21" && (selected_kind==ScenarioKind::Twin || selected_kind==ScenarioKind::TurboTwin)) {
selected_kind=ScenarioKind::TurboTwin;
} else {
std::cerr<<"Use gt17 with b205 or vz21 with twin\n";return 2;
}
selected_kind=ScenarioKind::TurboTwin;
}
window.setTitle(selected_kind == ScenarioKind::TurboTwin ? "FluidSim - twin + VZ21 turbo" : selected_kind == ScenarioKind::SaabB205 ? "FluidSim - Saab B205 / 9-5 wagon" :
(selected_kind == ScenarioKind::TwoStroke ? "FluidSim - YZ125 two-stroke" : "FluidSim - parallel twin"));
DesktopScenario scenario(selected_kind, two_stroke_config);
DesktopScenario scenario(selected_kind, two_stroke_config, turbo_name!="none");
scenario.set_audio_scene(selected_scene);
SimulationRingBuffer ring(ring_capacity);
for (std::size_t i = 0; i < preroll_samples; ++i) {

View File

@@ -3,6 +3,27 @@
#include <cmath>
namespace fluidsim::port {
void Ecu::control_boost(float dt, float pedal, float rpm, float manifold, float charge, float ambient) {
boost_target_ = boost_duty_ = 0;
if (!boost_control_enabled) { boost_integral_ = 0; boost_cut_ = false; return; }
if (!std::isfinite(dt) || dt <= 0 || !std::isfinite(pedal) || !std::isfinite(rpm) ||
!std::isfinite(manifold) || !std::isfinite(charge) || !std::isfinite(ambient) ||
manifold <= 0 || charge <= 0 || ambient <= 0) {
boost_integral_ = 0; boost_cut_ = true; return;
}
const float boost = manifold-ambient;
filtered_boost_ += (boost-filtered_boost_)*(-std::expm1(-dt/.05f));
if (std::max(manifold, charge)-ambient > maximum_boost+30000) boost_cut_ = true;
else if (std::max(manifold, charge)-ambient < maximum_boost+10000) boost_cut_ = false;
boost_target_ = maximum_boost*std::clamp((pedal-.5f)/.5f, 0.f, 1.f)*std::clamp((rpm-1500)/1500, 0.f, 1.f);
if (boost_target_ <= 0 || fuel_cut()) { boost_integral_ = 0; return; }
const float error = boost_target_-filtered_boost_;
const float proposed = boost_integral_+boost_ki*error*dt;
const float command = .3f+boost_kp*error+proposed;
if ((command >= 0 && command <= .85f) || (command > .85f && error < 0) || (command < 0 && error > 0))
boost_integral_ = proposed;
boost_duty_ = std::clamp(.3f+boost_kp*error+boost_integral_, 0.f, .85f);
}
void Ecu::step(float dt, float pedal, float rpm) {
fault_ = !std::isfinite(dt) || dt <= 0.0f || !std::isfinite(pedal) || !std::isfinite(rpm) || rpm < 0.0f;
if (fault_) { target_ = 0.0f; idle_ = false; return; }

View File

@@ -17,6 +17,7 @@ int main(int argc, char** argv) {
std::string scenario_name = "twin";
TwoStrokeConfig two_stroke_config;
bool turbo_enabled = false;
std::string turbo_name;
for (int i = 1; i + 1 < argc; ++i) {
const std::string option = argv[i];
if (option == "--seconds") duration_seconds = std::max(0.0f, std::strtof(argv[++i], nullptr));
@@ -24,8 +25,9 @@ int main(int argc, char** argv) {
else if (option == "--scenario") scenario_name = argv[++i];
else if (option == "--turbo") {
const std::string value = argv[++i];
if (value != "none" && value != "vz21") { std::cerr << "Choose none or vz21 turbo\n"; return 2; }
turbo_enabled = value == "vz21";
if (value != "none" && value != "vz21" && value != "gt17") { std::cerr << "Choose none, vz21 or gt17 turbo\n"; return 2; }
turbo_name = value;
turbo_enabled = value != "none";
}
else if (option == "--exhaust") {
const std::string value = argv[++i];
@@ -35,14 +37,19 @@ int main(int argc, char** argv) {
}
}
if (turbo_enabled && scenario_name != "twin") { std::cerr << "The turbo test preset uses --scenario twin\n"; return 2; }
if (turbo_name.empty() && scenario_name == "b205") turbo_enabled = true;
if ((!turbo_name.empty() && turbo_name != "none" &&
!((turbo_name == "gt17" && scenario_name == "b205") || (turbo_name == "vz21" && scenario_name == "twin"))) ||
(turbo_enabled && scenario_name != "twin" && scenario_name != "b205")) {
std::cerr << "Use gt17 with b205 or vz21 with twin\n"; return 2;
}
constexpr int sample_rate = 44100;
const auto total_samples = static_cast<std::uint64_t>(duration_seconds * sample_rate);
const auto report_interval = static_cast<std::uint64_t>(sample_rate / 10);
if (scenario_name == "twin" || scenario_name == "b205") {
FourStrokeScenario scenario(sample_rate, scenario_name == "b205" ? FourStrokePreset::SaabB205 : FourStrokePreset::Twin, turbo_enabled);
scenario.throttle = throttle;
std::cout << (scenario_name == "b205" ? "FluidSimCpp | Saab B205 / 9-5 wagon (no turbo) | throttle " : "FluidSimCpp | 600 cc parallel twin | throttle ") << std::fixed << std::setprecision(0)
std::cout << (scenario_name == "b205" ? "FluidSimCpp | Saab B205 / 9-5 wagon | throttle " : "FluidSimCpp | 600 cc parallel twin | throttle ") << std::fixed << std::setprecision(0)
<< throttle * 100.0f << "%\n";
for (std::uint64_t i = 0; i < total_samples; ++i) {
scenario.process();

View File

@@ -81,7 +81,8 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selecte
solver_.add_component(intake_plenum);
solver_.add_component(exhaust_collector);
if (turbo_enabled) {
turbo = std::make_unique<Turbocharger>();
turbo = std::make_unique<Turbocharger>(saab ? TurboConfig::gt17() : TurboConfig{});
ecu.boost_control_enabled = saab;
const int fresh = 2+2*count, exhaust_exit = fresh+1;
boundaries_.add_orifice(intake_atmosphere.port, fresh, true, intake_boundary_area_);
turbo->connect(boundaries_, pipes, fresh, false, 0, true, 1+2*count, false, exhaust_exit, true);
@@ -109,6 +110,10 @@ AudioFrame FourStrokeScenario::process() {
const std::lock_guard guard(mutex_);
step_motorcycle(crankshaft, dt_, preset == FourStrokePreset::SaabB205 ? 200.f : 30.f);
ecu.step(dt_, throttle, crankshaft.omega * 60 / (2*pi));
if (turbo) {
ecu.control_boost(dt_, throttle, crankshaft.omega * 60/(2*pi), intake_plenum.pressure(), turbo->compressor_outlet.pressure());
turbo->set_wastegate_duty(ecu.wastegate_duty());
}
const auto engines = cylinders();
for (int i = 0; i < cylinder_count(); ++i) {
auto& c = *engines[i];
@@ -180,6 +185,8 @@ ScenarioSnapshot FourStrokeScenario::snapshot() const {
const std::lock_guard<std::mutex> guard(mutex_);
ScenarioSnapshot result;
result.turbo_enabled = bool(turbo);
result.boost_target = ecu.boost_target();
result.wastegate_duty = ecu.wastegate_duty();
if (turbo) result.turbo = turbo->snapshot();
result.solver_substeps = solver_.substep_count();
result.boundary_iterations = boundaries_.nonlinear_iterations;
@@ -240,7 +247,7 @@ std::vector<Pipe1D> FourStrokeScenario::create_pipes(FourStrokePreset preset, bo
result.emplace_back(area_for_diameter(saab ? .032f : .0275f), saab ? .25f : .12f, 10);
for (int i = 0; i < count; ++i)
result.emplace_back(area_for_diameter(saab ? .034f : .028f), saab ? .28f : .20f, saab ? 10 : 6);
result.emplace_back(area_for_diameter(saab ? .060f : .028f), turbo_enabled ? .12f : (saab ? 1.4f : .40f), saab ? 40 : 10);
result.emplace_back(area_for_diameter(saab ? .060f : .028f), turbo_enabled ? .12f : (saab ? 1.4f : .40f), saab ? (turbo_enabled ? 8 : 40) : 10);
if (turbo_enabled) {
result.emplace_back(area_for_diameter(saab ? .060f : .0275f), .10f, 8);
result.emplace_back(area_for_diameter(saab ? .060f : .032f), .60f, 24);

View File

@@ -22,6 +22,26 @@ TurboConfig checked(TurboConfig c) {
return c;
}
}
TurboConfig TurboConfig::gt17() {
TurboConfig c;
c.shaft_inertia = 2.0e-5;
c.compressor_radius = .026;
c.turbine_radius = .019;
c.compressor_efficiency = .70;
c.turbine_peak_efficiency = .70;
c.compressor_effective_area = 420e-6;
c.turbine_effective_area = 350e-6;
c.bearing_viscous = 2e-7;
c.windage = 5e-11;
c.gate_diaphragm_area = .001;
c.gate_spring_preload = 25;
c.gate_diameter = .024;
c.gate_maximum_lift = .007;
c.compressor_inlet_volume = c.compressor_outlet_volume = 100e-6f;
c.turbine_inlet_volume = c.turbine_outlet_volume = 100e-6f;
c.connection_diameters = {.045f, .045f, .040f, .050f};
return c;
}
Turbocharger::Turbocharger(TurboConfig configuration)
: config(checked(configuration)),
compressor_inlet(config.compressor_inlet_volume), compressor_outlet(config.compressor_outlet_volume),
@@ -114,7 +134,7 @@ void Turbocharger::update_state(float timestep) {
const double tp = turbine_inlet.pressure(), tout = turbine_outlet.pressure();
const double can_force = std::max(0., double(compressor_outlet.pressure())-config.reference_pressure)*
config.gate_diaphragm_area;
config.gate_diaphragm_area*(1-gate_duty_);
const double equilibrium_lift = (can_force-config.gate_spring_preload)/config.gate_spring_stiffness;
// Overdamped spring-can linkage: b*dx/dt + k*x = F-Fpreload, with seats.
wastegate_lift_ = std::clamp(equilibrium_lift+(wastegate_lift_-equilibrium_lift)*