Migrate 32 engine definitions into validated startup-loaded TOML files and separate configuration and snapshots from scenario assembly. Add camera-based stereo propagation from shared pipe geometry, preserve exhaust character, tune carburetor mixtures and idle, use ignition cuts for carb engines, and support returning to the scenario menu. Validation: full Windows build and all 14 CTest checks passed.
120 lines
6.1 KiB
C++
120 lines
6.1 KiB
C++
#include "FluidSim/Carburetor.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
namespace fluidsim::port {
|
|
namespace {
|
|
constexpr float pi = 3.14159265358979323846f;
|
|
float circle_area(float diameter) { return .25f * pi * diameter * diameter; }
|
|
float smoothstep(float value) {
|
|
value = std::clamp(value, 0.0f, 1.0f);
|
|
return value * value * (3.0f - 2.0f * value);
|
|
}
|
|
}
|
|
|
|
Carburetor::Carburetor(float throat_diameter)
|
|
: bowl_mass_(bowl_capacity), throat_area_(circle_area(std::max(throat_diameter, 1e-4f))) {}
|
|
|
|
void Carburetor::step(float dt, float pedal, float slide, float rpm, float air_mass_flow,
|
|
float air_density, float upstream_pressure, float downstream_pressure) {
|
|
fault_ = !std::isfinite(dt) || dt <= 0.0f || !std::isfinite(pedal) ||
|
|
!std::isfinite(slide) || !std::isfinite(rpm) || rpm < 0.0f ||
|
|
!std::isfinite(air_mass_flow) || !std::isfinite(air_density) || air_density <= 0.0f ||
|
|
!std::isfinite(upstream_pressure) ||
|
|
!std::isfinite(downstream_pressure) || upstream_pressure <= 0.0f ||
|
|
downstream_pressure <= 0.0f;
|
|
if (fault_) {
|
|
delivered_fuel_flow_ = 0.0f;
|
|
overrun_ = false;
|
|
return;
|
|
}
|
|
|
|
pedal = std::clamp(pedal, 0.0f, 1.0f);
|
|
slide = std::clamp(slide, 0.0f, 1.0f);
|
|
air_mass_flow = std::max(air_mass_flow, 0.0f);
|
|
if (!initialized_) {
|
|
previous_pedal_ = pedal;
|
|
initialized_ = true;
|
|
}
|
|
const bool throttle_lift = previous_pedal_ > 0.08f && pedal <= 0.005f && rpm > overrun_rpm;
|
|
if (throttle_lift)
|
|
lift_enrichment_ = 1.0f;
|
|
previous_pedal_ = pedal;
|
|
lift_enrichment_ *= std::exp(-dt / 0.32f);
|
|
if (pedal > 0.005f || rpm < overrun_exit_rpm)
|
|
overrun_ = false;
|
|
else if (throttle_lift)
|
|
overrun_ = true;
|
|
|
|
const float air_alpha = -std::expm1(-dt / 0.012f);
|
|
filtered_air_flow_ += (air_mass_flow - filtered_air_flow_) * air_alpha;
|
|
|
|
// The open slide controls the effective venturi area. Its local dynamic
|
|
// pressure and the measured static-pressure loss provide the jet signal.
|
|
const float venturi_area = throat_area_ * 0.55f * (0.10f + 0.90f * slide);
|
|
const float velocity = filtered_air_flow_ / std::max(air_density * venturi_area, 1e-9f);
|
|
const float dynamic_pressure = 0.5f * air_density * velocity * velocity;
|
|
const float static_drop = std::max(upstream_pressure - downstream_pressure, 0.0f);
|
|
venturi_depression_ = std::clamp(dynamic_pressure + 0.18f * static_drop, 0.0f, 60000.0f);
|
|
|
|
const float pilot_area = circle_area(pilot_jet_diameter);
|
|
const float main_area = circle_area(main_jet_diameter);
|
|
const float needle_jet_area = circle_area(needle_jet_diameter);
|
|
// Piecewise needle annulus area, interpolated continuously between configured
|
|
// slide positions. A monotone area profile avoids artificial lean/rich holes
|
|
// from interpolating a multiplier over a curved base taper.
|
|
const std::array<float,10> positions{.04f,.045f,.05f,.07f,.1f,.15f,.25f,.5f,.75f,1.f};
|
|
const std::array<float,10> scales{1.f,std::lerp(1.f,needle_low_scale,.005f/.11f),
|
|
needle_off_idle_scale,needle_transition_scale,needle_tip_scale,needle_low_scale,needle_quarter_scale,
|
|
needle_mid_scale,needle_upper_scale,1.f};
|
|
float needle_lift=0;
|
|
for(std::size_t i=1;i<positions.size();++i) {
|
|
const float a=smoothstep((positions[i-1]-.04f)/.86f)*scales[i-1];
|
|
const float b=smoothstep((positions[i]-.04f)/.86f)*scales[i];
|
|
needle_lift=std::lerp(a,b,std::clamp((slide-positions[i-1])/(positions[i]-positions[i-1]),0.f,1.f));
|
|
if(slide<=positions[i]) break;
|
|
}
|
|
const float needle_area = needle_jet_area * needle_lift;
|
|
const float main_path_area = needle_area > 1e-12f ?
|
|
1.0f / std::sqrt(1.0f / (main_area * main_area) + 1.0f / (needle_area * needle_area)) : 0.0f;
|
|
const float pilot_depression = bowl_head_pressure + (0.62f * static_drop +
|
|
0.25f * dynamic_pressure) * (1.0f + 0.25f * lift_enrichment_);
|
|
const float main_depression = bowl_head_pressure + venturi_depression_;
|
|
const float pilot_progression = slide<.05f ? std::lerp(1.f,pilot_transition_scale,
|
|
std::clamp((slide-.03f)/.02f,0.f,1.f)) : slide<.1f ?
|
|
std::lerp(pilot_transition_scale,pilot_tip_scale,(slide-.05f)/.05f) :
|
|
std::lerp(pilot_tip_scale,1.f,std::clamp((slide-.1f)/.1f,0.f,1.f));
|
|
const float pilot_flow = pilot_progression * pilot_discharge_coefficient * pilot_area *
|
|
std::sqrt(2.0f * fuel_density * std::max(pilot_depression, 0.0f));
|
|
const float main_flow = main_discharge_coefficient * main_path_area *
|
|
std::sqrt(2.0f * fuel_density * std::max(main_depression, 0.0f));
|
|
float demanded_fuel_flow = pilot_flow + main_flow;
|
|
|
|
const float deposit_fraction = std::clamp(film_fraction * (0.55f + 0.45f * slide), 0.0f, 0.8f);
|
|
const float deposited = demanded_fuel_flow * deposit_fraction;
|
|
const float evaporation_time = film_time_constant * (overrun_ ? 0.58f : 1.0f);
|
|
const float evaporated = std::min(film_mass_ / dt,
|
|
film_mass_ / std::max(evaporation_time, 1e-4f));
|
|
float jet_flow = demanded_fuel_flow;
|
|
const float available_flow = bowl_mass_ / dt;
|
|
if (jet_flow > available_flow) {
|
|
const float scale = available_flow / std::max(jet_flow, 1e-12f);
|
|
jet_flow *= scale;
|
|
}
|
|
bowl_mass_ -= jet_flow * dt;
|
|
bowl_mass_ += std::min(bowl_refill_rate * dt, bowl_capacity - bowl_mass_);
|
|
bowl_mass_ = std::clamp(bowl_mass_, 0.0f, bowl_capacity);
|
|
|
|
// Deposition cannot exceed the fuel that actually left the bowl.
|
|
const float actual_deposit = std::min(deposited, jet_flow);
|
|
film_mass_ = std::max(film_mass_ + (actual_deposit - evaporated) * dt, 0.0f);
|
|
delivered_fuel_flow_ = std::max(jet_flow - actual_deposit + evaporated, 0.0f);
|
|
const float fuel_alpha = -std::expm1(-dt / 0.025f);
|
|
filtered_fuel_flow_ += (delivered_fuel_flow_ - filtered_fuel_flow_) * fuel_alpha;
|
|
if (filtered_air_flow_ > 1e-7f && filtered_fuel_flow_ > 1e-9f)
|
|
indicated_afr_ = filtered_air_flow_ / filtered_fuel_flow_;
|
|
}
|
|
|
|
} // namespace fluidsim::port
|