Load engines from TOML and improve scenario audio and carb tuning
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.
This commit is contained in:
@@ -4,6 +4,7 @@ namespace fluidsim::port {
|
||||
|
||||
struct AudioFrame { float left{}, right{}; };
|
||||
struct Vec3 { float x{}, y{}, z{}; };
|
||||
struct AudioListener { Vec3 position{}, right{1,0,0}; };
|
||||
|
||||
// Simple flow-derivative source used by the Helmholtz and pulse scenarios.
|
||||
class SoundProcessor {
|
||||
|
||||
@@ -5,21 +5,30 @@
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
// Lumped slide-carburetor model for the crankcase-scavenged two-stroke. The
|
||||
// Lumped carburetor shared by two-stroke and four-stroke scenarios. The
|
||||
// jets use liquid-orifice flow from a float bowl into the measured venturi
|
||||
// depression. Delivered fuel is injected into the downstream fluid volume and
|
||||
// is then transported as a conserved species; this class never doses a cylinder.
|
||||
class Carburetor {
|
||||
public:
|
||||
struct CarburetorConfig {
|
||||
float fuel_density{740.0f};
|
||||
float pilot_jet_diameter{0.00040f}, main_jet_diameter{0.00150f};
|
||||
float needle_jet_diameter{0.00190f};
|
||||
// Needle annulus area corrections at configured slide positions; jets meter
|
||||
// liquid fuel from measured pressure, without AFR feedback.
|
||||
float needle_low_scale{1.f}, needle_mid_scale{1.f};
|
||||
float needle_off_idle_scale{1.f}, needle_transition_scale{1.f}, needle_tip_scale{1.f};
|
||||
float needle_quarter_scale{1.f}, needle_upper_scale{1.f};
|
||||
float pilot_transition_scale{1.f}, pilot_tip_scale{1.f};
|
||||
float pilot_discharge_coefficient{0.62f}, main_discharge_coefficient{0.72f};
|
||||
float bowl_head_pressure{};
|
||||
float film_fraction{0.28f}, film_time_constant{0.11f};
|
||||
float bowl_capacity{0.008f}, bowl_refill_rate{0.0025f}; // kg, kg/s
|
||||
float overrun_rpm{4200.0f}, overrun_exit_rpm{3300.0f};
|
||||
|
||||
};
|
||||
|
||||
class Carburetor : public CarburetorConfig {
|
||||
public:
|
||||
explicit Carburetor(float throat_diameter = 0.0381f);
|
||||
|
||||
// air_mass_flow is positive from atmosphere toward the engine.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#pragma once
|
||||
#include "FluidSim/Scenario.hpp"
|
||||
#include "FluidSim/ScenarioSnapshot.hpp"
|
||||
#include "FluidSim/DynoCurve.hpp"
|
||||
#include "FluidSim/Desktop/PedalControl.hpp"
|
||||
#include "FluidSim/Desktop/EngineRenderer3D.hpp"
|
||||
@@ -22,6 +22,7 @@ public:
|
||||
void nudge_pedal(float amount) {pedal_.set(pedal_.value()+amount);}
|
||||
void update_pedal(float dt,bool raise,bool lower) {if(!pedal_drag_)pedal_.advance(dt,raise,lower);}
|
||||
bool handle_event(const sf::Event&,sf::Vector2u);
|
||||
AudioListener listener(const ScenarioSnapshot&,sf::Vector2u);
|
||||
void draw(sf::RenderWindow&,const sf::Font*,const ScenarioSnapshot&,const PlaybackInfo&);
|
||||
private:
|
||||
DynoCurve dyno_;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#pragma once
|
||||
#include "FluidSim/Scenario.hpp"
|
||||
#include "FluidSim/ScenarioSnapshot.hpp"
|
||||
#include "FluidSim/Desktop/OrbitCamera.hpp"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <memory>
|
||||
|
||||
@@ -7,6 +7,13 @@ namespace fluidsim::port::desktop {
|
||||
struct OrbitCamera {
|
||||
Vec3 target{.12f,.25f,0};
|
||||
float yaw{-28},pitch{24},distance{2.2f};
|
||||
AudioListener listener(float distance_scale=1, float target_offset_x=0) const {
|
||||
const float y=yaw*.017453293f,p=pitch*.017453293f,d=distance*distance_scale;
|
||||
// Inverse of the renderer's pitch/yaw world-to-view rotation.
|
||||
return {{target.x+target_offset_x-std::sin(y)*std::cos(p)*d,
|
||||
target.y+std::sin(p)*d,target.z+std::cos(y)*std::cos(p)*d},
|
||||
{std::cos(y),0,std::sin(y)}};
|
||||
}
|
||||
void reset(float aspect) {target={.12f,.25f,0};yaw=-28;pitch=24;distance=std::max(.65f,1.6f/aspect)/.414214f;}
|
||||
void orbit(sf::Vector2i delta) {yaw+=delta.x*.35f;pitch=std::clamp(pitch+delta.y*.3f,-75.f,80.f);}
|
||||
void pan(sf::Vector2i delta,float height) {
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
#pragma once
|
||||
#include "FluidSim/Scenario.hpp"
|
||||
#include "FluidSim/ScenarioCatalog.hpp"
|
||||
#include "FluidSim/TwoStrokeScenario.hpp"
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
namespace fluidsim::port {
|
||||
// Shared desktop controls/snapshot contract. Two-stroke access is serialized;
|
||||
// the twin already protects its own simulation state.
|
||||
// Shared desktop controls and snapshots. Each simulation owns its state lock.
|
||||
class DesktopScenario {
|
||||
public:
|
||||
explicit DesktopScenario(ScenarioKind kind, TwoStrokeConfig config = {}, ExhaustConfig exhaust = {}, V8ExhaustConfig v8 = {}, EngineConfig engine = {}, std::string name = {}) : name_(std::move(name)) {
|
||||
explicit DesktopScenario(const ScenarioExample& e)
|
||||
: DesktopScenario(e.kind,e.two,e.exhaust,e.junctions,e.engine,e.title) {}
|
||||
explicit DesktopScenario(ScenarioKind kind, TwoStrokeConfig config = {}, ExhaustConfig exhaust = {}, JunctionConfig junctions = {}, EngineConfig engine = {}, std::string name = {}) : name_(std::move(name)) {
|
||||
if (kind == ScenarioKind::TwoStroke)
|
||||
two_ = std::make_unique<TwoStrokeScenario>(44100, config);
|
||||
else
|
||||
twin_ = std::make_unique<FourStrokeScenario>(44100, kind == ScenarioKind::SaabB205 ?
|
||||
FourStrokePreset::SaabB205 : kind == ScenarioKind::AmericanV8 ? FourStrokePreset::AmericanV8 : FourStrokePreset::Twin, exhaust, v8,engine);
|
||||
else {
|
||||
if(!engine.cylinders)engine=preset_engine_definition(kind==ScenarioKind::SaabB205?
|
||||
FourStrokePreset::SaabB205:kind==ScenarioKind::AmericanV8?FourStrokePreset::AmericanV8:FourStrokePreset::Twin).engine;
|
||||
four_ = std::make_unique<FourStrokeScenario>(44100,std::move(engine),exhaust,junctions);
|
||||
}
|
||||
}
|
||||
AudioFrame process() {
|
||||
if (twin_)
|
||||
return twin_->process();
|
||||
if (four_)
|
||||
return four_->process();
|
||||
const std::lock_guard lock(mutex_);
|
||||
return two_->process();
|
||||
}
|
||||
@@ -33,8 +37,11 @@ public:
|
||||
void set_audio_scene(OutdoorScene scene) {
|
||||
visit([&](auto& scenario) { scenario.set_audio_scene(scene); });
|
||||
}
|
||||
void set_listener(AudioListener listener) {
|
||||
visit([&](auto& scenario) { scenario.set_listener(listener); });
|
||||
}
|
||||
ScenarioSnapshot snapshot() const {
|
||||
if (twin_) { auto s=twin_->snapshot();s.example_name=name_;return s; }
|
||||
if (four_) { auto s=four_->snapshot();s.example_name=name_;return s; }
|
||||
const std::lock_guard lock(mutex_);
|
||||
const auto source = two_->snapshot();
|
||||
ScenarioSnapshot s;
|
||||
@@ -65,6 +72,7 @@ public:
|
||||
s.valve1_exhaust_area = source.exhaust_area;
|
||||
s.reed_open = source.reed_open;
|
||||
s.fuel_cut = source.fuel_cut;
|
||||
s.ignition_cut = source.ignition_cut;
|
||||
s.idle_control = two_->ecu.idle_active();
|
||||
s.rev_limit1 = two_->ecu.rev_limited();
|
||||
s.audio_scene = source.audio_scene;
|
||||
@@ -77,11 +85,11 @@ public:
|
||||
private:
|
||||
template<class Function> void visit(Function&& function) {
|
||||
const std::lock_guard lock(mutex_);
|
||||
if (twin_) function(*twin_);
|
||||
if (four_) function(*four_);
|
||||
else function(*two_);
|
||||
}
|
||||
std::string name_;
|
||||
std::unique_ptr<FourStrokeTwinScenario> twin_;
|
||||
std::unique_ptr<FourStrokeScenario> four_;
|
||||
std::unique_ptr<TwoStrokeScenario> two_;
|
||||
mutable std::mutex mutex_;
|
||||
};
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
namespace fluidsim::port {
|
||||
|
||||
// Ideal sensors and injectors; calibration parameters are not engine measurements.
|
||||
class Ecu {
|
||||
public:
|
||||
struct EcuConfig {
|
||||
enum class IdleControl { Feedback, FixedOpening };
|
||||
IdleControl idle_control{IdleControl::Feedback};
|
||||
struct IgnitionPoint { float rpm, degrees_btdc; };
|
||||
@@ -18,18 +17,25 @@ public:
|
||||
float throttle_closing_time_constant{}, throttle_closing_rate{};
|
||||
float rev_limit_rpm{10000.0f}, rev_resume_rpm{9750.0f};
|
||||
bool rev_limit_enabled{true};
|
||||
bool rev_limit_fuel_cut_enabled{true};
|
||||
float overrun_cut_rpm{3500.0f}, overrun_resume_rpm{2400.0f};
|
||||
bool overrun_fuel_cut_enabled{true};
|
||||
bool overrun_ignition_cut_enabled{false};
|
||||
float idle_spark_gain{0.0f}, idle_minimum_advance{-5.0f}; // degrees/RPM, degrees BTDC
|
||||
float stoichiometric_afr{14.7f}, power_afr{12.8f};
|
||||
|
||||
};
|
||||
|
||||
class Ecu : public EcuConfig {
|
||||
public:
|
||||
void step(float dt, float pedal, float rpm);
|
||||
float fuel_for_air(float trapped_air_mass) const;
|
||||
float throttle_opening() const { return opening_; }
|
||||
float throttle_target() const { return target_; }
|
||||
float target_afr() const { return afr_; }
|
||||
float spark_advance() const { return spark_; }
|
||||
bool fuel_cut() const { return limiter_ || (overrun_ && overrun_fuel_cut_enabled) || fault_; }
|
||||
bool fuel_cut() const { return (limiter_ && rev_limit_fuel_cut_enabled) || (overrun_ && overrun_fuel_cut_enabled) || fault_; }
|
||||
bool ignition_cut() const { return limiter_ || (overrun_ && overrun_ignition_cut_enabled) || fault_; }
|
||||
bool rev_limited() const { return limiter_; }
|
||||
bool idle_active() const { return idle_; }
|
||||
bool sensor_fault() const { return fault_; }
|
||||
|
||||
@@ -10,12 +10,16 @@ namespace fluidsim::port {
|
||||
|
||||
class Ecu;
|
||||
|
||||
class Crankshaft {
|
||||
public:
|
||||
float omega{400.0f * 2.0f * pi / 60.0f};
|
||||
float angle{}, previous_angle{}, inertia{0.2f};
|
||||
struct CrankshaftConfig {
|
||||
float inertia{0.2f};
|
||||
float friction_constant{}, friction_viscous{};
|
||||
float friction_quadratic{}; // Nm / (rad/s)^2, dissipative windage term
|
||||
};
|
||||
|
||||
class Crankshaft : public CrankshaftConfig {
|
||||
public:
|
||||
float omega{400.0f * 2.0f * pi / 60.0f};
|
||||
float angle{}, previous_angle{};
|
||||
float load_torque{}, drivetrain_torque{};
|
||||
float average_power{}, average_torque{};
|
||||
|
||||
@@ -28,18 +32,22 @@ private:
|
||||
float power_sum_{}, torque_sum_{};
|
||||
};
|
||||
|
||||
class EngineCylinder : public Component {
|
||||
public:
|
||||
Ecu* ecu{}; // Optional ideal electronic fuel/ignition controller.
|
||||
bool transported_fuel{}; // Upstream carb meters the fuel species.
|
||||
struct CylinderConfig {
|
||||
float intake_open{340}, intake_close{590}, exhaust_open{130}, exhaust_close{390};
|
||||
float bore{0.075f}, stroke{0.068f}, rod{0.112f}, compression{11.5f};
|
||||
float intake_valve_diameter{0.030f}, exhaust_valve_diameter{0.028f};
|
||||
int intake_valve_count{1}, exhaust_valve_count{1};
|
||||
float intake_valve_lift{0.005f}, exhaust_valve_lift{0.005f};
|
||||
float phase{};
|
||||
float spark_advance{20.0f}, wiebe_a{5.0f}, wiebe_m{2.0f}, burn_duration{60.0f}, burn_start{5.0f};
|
||||
float afr{14.7f}, fuel_lhv{44.0e6f}, energy_variation{0.05f}, misfire_probability{};
|
||||
float wall_area{0.02f}, heat_transfer{100.0f}, ambient_temperature{300.0f};
|
||||
};
|
||||
|
||||
class EngineCylinder : public Component, public CylinderConfig {
|
||||
public:
|
||||
Ecu* ecu{};
|
||||
bool transported_fuel{};
|
||||
float phase{};
|
||||
bool rev_limit_enabled{};
|
||||
float rev_limit_rpm{7500.0f}, rev_limit_cut_time{0.05f}, rev_limit_hysteresis{200.0f};
|
||||
float air_mass{}, exhaust_mass{}, energy{}, volume{}, pressure{101325.0f};
|
||||
@@ -78,7 +86,6 @@ protected:
|
||||
|
||||
class FourStrokeCylinder final : public EngineCylinder {
|
||||
public:
|
||||
float intake_open{340.0f}, intake_close{590.0f}, exhaust_open{130.0f}, exhaust_close{390.0f};
|
||||
float valve_area(float cycle_degrees, bool inlet) const override;
|
||||
void pre_step(Crankshaft& crank, float dt, float throttle) override;
|
||||
};
|
||||
|
||||
53
include/FluidSim/EngineConfig.hpp
Normal file
53
include/FluidSim/EngineConfig.hpp
Normal file
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
#include "FluidSim/Engine.hpp"
|
||||
#include "FluidSim/Ecu.hpp"
|
||||
#include "FluidSim/Vehicle.hpp"
|
||||
#include "FluidSim/Carburetor.hpp"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
enum class ScenarioKind { Twin, TwoStroke, SaabB205, AmericanV8 };
|
||||
enum class FourStrokePreset { Twin, SaabB205, AmericanV8 };
|
||||
enum class EngineLayout { Inline, V, Boxer };
|
||||
enum class ExhaustTopology { Collector, Staged, AmericanV8 };
|
||||
|
||||
struct PipeConfig {
|
||||
std::string id;
|
||||
float diameter{}, length{}; // metres
|
||||
int cells{}, count{1};
|
||||
bool silenced{};
|
||||
};
|
||||
|
||||
// Parsed calibration only. Runtime state belongs to the scenario components.
|
||||
// Dimensional fields use SI units after loading.
|
||||
struct EngineConfig {
|
||||
int cylinders{};
|
||||
EngineLayout layout{EngineLayout::Inline};
|
||||
ExhaustTopology topology{ExhaustTopology::Collector};
|
||||
float bank_angle{60};
|
||||
CylinderConfig cylinder;
|
||||
CrankshaftConfig crankshaft;
|
||||
EcuConfig ecu;
|
||||
VehicleConfig vehicle;
|
||||
CarburetorConfig fuel;
|
||||
bool carbureted{}, initialize_phased_charge{true};
|
||||
float initial_rpm{}, starter_torque{}, maximum_load_torque{};
|
||||
float plenum_volume{}, collector_volume{}, throttle_diameter{}, pipe_damping{};
|
||||
int minimum_substeps{1};
|
||||
std::vector<float> phase_degrees;
|
||||
std::vector<PipeConfig> pipes;
|
||||
|
||||
bool has_silenced_exhaust() const { for(const auto& p:pipes)if(p.silenced)return true;return false; }
|
||||
bool american_headers() const { return topology==ExhaustTopology::AmericanV8; }
|
||||
bool staged_headers() const { return topology==ExhaustTopology::Staged; }
|
||||
bool dual_outlets() const { return american_headers() || (staged_headers() && layout==EngineLayout::V); }
|
||||
int header_groups() const { return cylinders==3?1:2; }
|
||||
};
|
||||
|
||||
struct JunctionConfig {
|
||||
bool h_pipe{true};
|
||||
float junction_volume{50e-6f};
|
||||
};
|
||||
void validate_engine_config(const EngineConfig& config,const std::string& context="engine config");
|
||||
}
|
||||
@@ -25,7 +25,7 @@ struct OutdoorAudioConfig {
|
||||
float monitor_cutoff_hz{6000.0f}; // Second-order acquisition/rendering filter.
|
||||
};
|
||||
|
||||
// Static geometric direct/first-order reflection paths plus an explicitly
|
||||
// Moving geometric direct/first-order reflection paths plus an explicitly
|
||||
// approximate diffuse tail. No feedback into the fluid/combustion simulation.
|
||||
class OutdoorAudio {
|
||||
public:
|
||||
@@ -35,6 +35,9 @@ public:
|
||||
AudioFrame process(float intake_mass_flow, float exhaust_mass_flow, float second_exhaust_mass_flow = 0);
|
||||
void set_scene(OutdoorScene scene) { requested_scene_.store(scene, std::memory_order_relaxed); }
|
||||
OutdoorScene scene() const { return requested_scene_.load(std::memory_order_relaxed); }
|
||||
// Call on the simulation thread or under the scenario's simulation lock.
|
||||
void set_listener(AudioListener listener);
|
||||
void set_source_positions(const std::array<Vec3,3>& positions);
|
||||
void set_monitor_filter(bool enabled) { monitor_filter_ = enabled; } // Set before playback/capture.
|
||||
AudioFrame dry_frame() const { return last_dry_; }
|
||||
AudioFrame unfiltered_frame() const { return last_raw_; }
|
||||
@@ -45,8 +48,8 @@ private:
|
||||
float process(float x);
|
||||
};
|
||||
struct Path {
|
||||
std::size_t delay_left{}, delay_right{};
|
||||
float fraction_left{}, fraction_right{}, gain_left{}, gain_right{}, alpha{1};
|
||||
float delay_left{}, delay_right{}, gain_left{}, gain_right{}, alpha{1};
|
||||
float target_delay_left{}, target_delay_right{}, target_gain_left{}, target_gain_right{};
|
||||
float state_left{}, state_right{};
|
||||
int group{}; // direct, ground, wall, trees, tunnel
|
||||
};
|
||||
@@ -55,6 +58,7 @@ private:
|
||||
std::vector<Path> paths;
|
||||
std::size_t position{};
|
||||
float previous_flow{};
|
||||
float center_delay{}, target_center_delay{}, center_gain{}, target_center_gain{};
|
||||
LowPass filter;
|
||||
};
|
||||
int rate_{};
|
||||
@@ -66,8 +70,10 @@ private:
|
||||
OutdoorExhaustReverb late_reverb_;
|
||||
std::array<float, 5> weights_{1, 0, 0, 0, 0};
|
||||
float scene_alpha_{}, tail_mix_{};
|
||||
float geometry_alpha_{}, reverb_input_{};
|
||||
AudioFrame last_dry_{}, last_raw_{};
|
||||
void configure_emitter(Emitter &emitter, Vec3 position);
|
||||
void update_geometry();
|
||||
AudioFrame render(Emitter &emitter, float flow, AudioFrame &dry, AudioFrame &raw);
|
||||
};
|
||||
} // namespace fluidsim::port
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
#include "FluidSim/Silencer.hpp"
|
||||
#include "FluidSim/EngineConfig.hpp"
|
||||
#include "FluidSim/ScenarioSnapshot.hpp"
|
||||
#include "FluidSim/Carburetor.hpp"
|
||||
#include <string>
|
||||
#include "FluidSim/Junction.hpp"
|
||||
@@ -19,85 +21,24 @@
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
struct ScenarioExample;
|
||||
|
||||
enum class ScenarioKind { Twin, TwoStroke, SaabB205, AmericanV8 };
|
||||
enum class FourStrokePreset { Twin, SaabB205, AmericanV8 };
|
||||
|
||||
enum class EngineLayout { Inline, V, Boxer };
|
||||
struct EngineConfig {
|
||||
int cylinders{}; // zero selects the existing preset
|
||||
EngineLayout layout{EngineLayout::Inline};
|
||||
float bank_angle{60}, bore{.086f}, stroke{.086f}, rod{.145f}, compression{10};
|
||||
float idle_rpm{1000}, rev_limit{7000}, idle_opening{.045f};
|
||||
bool carbureted{};
|
||||
float carb_pilot_scale{1.f};
|
||||
float carb_jet_scale{1.f}; // multiplier on displacement-scaled effective jet diameters
|
||||
std::array<float,12> phase_degrees{}; // used when custom_phases is true
|
||||
bool custom_phases{};
|
||||
};
|
||||
|
||||
struct V8ExhaustConfig {
|
||||
float primary_length{.75f}, primary_diameter{.038f};
|
||||
bool h_pipe{true};
|
||||
float junction_volume{50e-6f}; // m3, 50 cc per fitting
|
||||
};
|
||||
|
||||
struct ScenarioSnapshot {
|
||||
std::string example_name;
|
||||
EngineLayout layout{EngineLayout::Inline};
|
||||
float bank_angle{};
|
||||
bool v8_headers{};
|
||||
PipeSolver pipe_solver{PipeSolver::Rusanov};
|
||||
int cylinder_count{2};
|
||||
float rev_limit_rpm{10000};
|
||||
std::array<float, 12> cylinder_angles{};
|
||||
std::array<float, 12> cylinder_pressures{}, piston_fractions{}, intake_areas{}, exhaust_areas{};
|
||||
std::uint64_t solver_substeps{}, boundary_iterations{}, pipe_positivity_repairs{};
|
||||
float rpm{}, speed_kmh{}, kinetic_energy{}, average_power{}, average_torque{};
|
||||
float cylinder_pressure1{}, cylinder_pressure2{};
|
||||
float cylinder_temperature1{}, cylinder_temperature2{};
|
||||
float cylinder_piston_fraction1{}, cylinder_piston_fraction2{};
|
||||
float valve1_intake_area{}, valve1_exhaust_area{}, valve2_intake_area{}, valve2_exhaust_area{};
|
||||
float intake_pressure{}, exhaust_pressure{}, second_bank_pressure{};
|
||||
std::array<float,2> crossover_pressures{};
|
||||
float throttle{}, pedal{}, target_afr{}, clutch{}, intake_flow{}, exhaust_flow{};
|
||||
bool idle_control{}, fuel_cut{}, two_stroke{}, reed_open{};
|
||||
bool racing_exhaust{};
|
||||
bool silencers{}, h_pipe{};
|
||||
ExhaustConfig exhaust_config{};
|
||||
float crankcase_pressure{};
|
||||
OutdoorScene audio_scene{OutdoorScene::Wall};
|
||||
int gear{};
|
||||
bool rev_limit1{}, rev_limit2{};
|
||||
std::array<std::vector<GasState>, 32> pipe_states;
|
||||
std::array<std::vector<float>, 32> pipe_areas, pipe_lengths;
|
||||
void capture_pipes(const std::vector<Pipe1D>& pipes) {
|
||||
for (std::size_t k = 0; k < pipes.size(); ++k) {
|
||||
pipe_positivity_repairs += pipes[k].positivity_repairs;
|
||||
pipe_areas[k] = pipes[k].cell_areas;
|
||||
pipe_lengths[k] = pipes[k].cell_lengths;
|
||||
pipe_states[k].reserve(pipes[k].cells.size());
|
||||
for (std::size_t i = 0; i < pipes[k].cells.size(); ++i)
|
||||
pipe_states[k].push_back(pipes[k].state(static_cast<int>(i)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Shared four-stroke topology: independent runners/headers and shared plenums.
|
||||
// Presets include a 600 cc twin, B205-sized inline-four and American 5.0 V8.
|
||||
// Calibrated four-stroke assembly with independent runners and shared plenums.
|
||||
class FourStrokeScenario : public MotorcycleTest {
|
||||
public:
|
||||
explicit FourStrokeScenario(int sample_rate = 44100, FourStrokePreset preset = FourStrokePreset::Twin, ExhaustConfig exhaust = {}, V8ExhaustConfig v8_exhaust = {}, EngineConfig engine = {});
|
||||
const FourStrokePreset preset;
|
||||
explicit FourStrokeScenario(int sample_rate = 44100, FourStrokePreset preset = FourStrokePreset::Twin);
|
||||
FourStrokeScenario(int sample_rate, const ScenarioExample& definition);
|
||||
FourStrokeScenario(int sample_rate, EngineConfig engine, ExhaustConfig exhaust = {}, JunctionConfig junctions = {});
|
||||
const ExhaustConfig exhaust_config;
|
||||
const V8ExhaustConfig v8_exhaust_config;
|
||||
const JunctionConfig junction_config;
|
||||
const EngineConfig engine_config;
|
||||
int cylinder_count() const { return engine_config.cylinders ? engine_config.cylinders : preset == FourStrokePreset::AmericanV8 ? 8 : preset == FourStrokePreset::SaabB205 ? 4 : 2; }
|
||||
int cylinder_count() const { return engine_config.cylinders; }
|
||||
std::array<FourStrokeCylinder*, 12> cylinders() { return {&cylinder1, &cylinder2, &cylinder3, &cylinder4,
|
||||
&cylinder5, &cylinder6, &cylinder7, &cylinder8,&cylinder9,&cylinder10,&cylinder11,&cylinder12}; }
|
||||
AudioFrame process();
|
||||
void set_controls(float throttle_value, float load_value, float clutch_value);
|
||||
void set_audio_scene(OutdoorScene scene);
|
||||
void set_listener(AudioListener listener);
|
||||
void set_pipe_solver(PipeSolver solver);
|
||||
void set_monitor_filter(bool enabled);
|
||||
void set_starter(bool engaged);
|
||||
@@ -115,8 +56,8 @@ public:
|
||||
FourStrokeCylinder cylinder1{}, cylinder2{}, cylinder3{}, cylinder4{};
|
||||
FourStrokeCylinder cylinder5{}, cylinder6{}, cylinder7{}, cylinder8{};
|
||||
FourStrokeCylinder cylinder9{}, cylinder10{}, cylinder11{}, cylinder12{};
|
||||
Volume0D intake_plenum{1.0e-3f, 101325.0f, 300.0f};
|
||||
Junction exhaust_collector{0.15e-3f, 101325.0f, 700.0f};
|
||||
Volume0D intake_plenum;
|
||||
Junction exhaust_collector;
|
||||
std::vector<Pipe1D> pipes;
|
||||
float intake_flow{}, exhaust_flow{};
|
||||
std::uint64_t sample_count{};
|
||||
@@ -134,7 +75,7 @@ private:
|
||||
Port* throttle_port_{};
|
||||
|
||||
static float area_for_diameter(float diameter);
|
||||
static std::vector<Pipe1D> create_pipes(FourStrokePreset preset, const ExhaustConfig& exhaust, const V8ExhaustConfig& v8_exhaust, const EngineConfig& engine);
|
||||
static std::vector<Pipe1D> create_pipes(const EngineConfig& engine, const ExhaustConfig& exhaust);
|
||||
};
|
||||
|
||||
using FourStrokeTwinScenario = FourStrokeScenario;
|
||||
|
||||
@@ -1,83 +1,28 @@
|
||||
#pragma once
|
||||
#include "FluidSim/Scenario.hpp"
|
||||
#include "FluidSim/EngineConfig.hpp"
|
||||
#include "FluidSim/Silencer.hpp"
|
||||
#include "FluidSim/TwoStrokeConfig.hpp"
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
struct ScenarioExample {
|
||||
std::string id,title,description;
|
||||
ScenarioKind kind;
|
||||
ScenarioKind kind{ScenarioKind::Twin};
|
||||
TwoStrokeConfig two{};
|
||||
ExhaustConfig exhaust{};
|
||||
V8ExhaustConfig v8{};
|
||||
JunctionConfig junctions{};
|
||||
EngineConfig engine{};
|
||||
std::string category;
|
||||
int order{};
|
||||
};
|
||||
inline std::vector<ScenarioExample> scenario_examples() {
|
||||
std::vector<ScenarioExample> examples;
|
||||
auto add=[&](const char* id,const char* title,const char* description,ScenarioKind kind)->ScenarioExample& {
|
||||
examples.push_back({id,title,description,kind,{},{},{},{},{}});return examples.back();
|
||||
};
|
||||
add("twin","600 cc parallel twin","EFI motorcycle baseline",ScenarioKind::Twin);
|
||||
add("yz-racing","YZ125 / expansion chamber","Racing pipe, fixed 4.5% carb idle",ScenarioKind::TwoStroke);
|
||||
add("yz-straight","YZ125 / straight exhaust","Same engine and carb, straight exhaust comparison",ScenarioKind::TwoStroke).two.exhaust=TwoStrokeExhaust::Straight;
|
||||
add("saab-straight","Saab B205 / straight-through","6 L resonator and 18 L straight-through rear muffler",ScenarioKind::SaabB205);
|
||||
auto& chambered=add("saab-chambered","Saab B205 / chambered","Expansion resonator and two-chamber rear muffler",ScenarioKind::SaabB205);
|
||||
chambered.exhaust.resonator.type=SilencerType::ExpansionResonator;
|
||||
chambered.exhaust.muffler.type=SilencerType::Chambered;chambered.exhaust.muffler.chambers=2;
|
||||
auto& resonator=add("saab-expansion","Saab B205 / expansion silencers","Two single expansion chambers, same body volumes",ScenarioKind::SaabB205);
|
||||
resonator.exhaust.resonator.type=SilencerType::ExpansionResonator;resonator.exhaust.muffler.type=SilencerType::ExpansionResonator;
|
||||
add("saab-open","Saab B205 / open exhaust","Silencers bypassed; naturally aspirated baseline",ScenarioKind::SaabB205).exhaust.enabled=false;
|
||||
add("v8-h","American 5.0 / 4-2-1 + H","750 mm primaries, 50 cc junctions, H crossover",ScenarioKind::AmericanV8);
|
||||
add("v8-dual","American 5.0 / independent banks","Same 4-2-1 headers with H crossover disconnected",ScenarioKind::AmericanV8).v8.h_pipe=false;
|
||||
add("v8-short","American 5.0 / short headers","600 mm primaries with H crossover",ScenarioKind::AmericanV8).v8.primary_length=.60f;
|
||||
add("v8-long","American 5.0 / long headers","900 mm primaries with H crossover",ScenarioKind::AmericanV8).v8.primary_length=.90f;
|
||||
add("v8-large","American 5.0 / larger primaries","42 mm x 750 mm primaries with H crossover",ScenarioKind::AmericanV8).v8.primary_diameter=.042f;
|
||||
for(auto& e:examples) e.category=e.kind==ScenarioKind::TwoStroke?"Two-stroke / 125 cc":e.kind==ScenarioKind::SaabB205?"Inline four / Saab":e.kind==ScenarioKind::AmericanV8?"V8 / American 5.0":"Twins / parallel";
|
||||
auto chamber=[&](const char* id,const char* title,float length,float width) {
|
||||
auto& e=add(id,title,"Estimated chamber geometry / same 125 cc engine",ScenarioKind::TwoStroke);
|
||||
e.category="Two-stroke / 125 cc";
|
||||
for(auto& section:e.two.racing_pipe) {section.length*=length;section.inlet_diameter*=width;section.outlet_diameter*=width;}
|
||||
};
|
||||
chamber("yz-torque","125 / long torque chamber",1.18f,1.f);
|
||||
chamber("yz-short","125 / short high-RPM chamber",.82f,1.f);
|
||||
chamber("yz-fat","125 / wide belly chamber",1.f,1.18f);
|
||||
auto two=[&](const char* id,const char* title,const char* category,float bore,float stroke,float carb) {
|
||||
auto& e=add(id,title,"Concept single / transported carb fuel",ScenarioKind::TwoStroke);e.category=category;
|
||||
const float scale=std::cbrt((bore*bore*stroke)/(.054f*.054f*.0545f));
|
||||
e.two.jet_scale=bore<.05f?.5f:1.f;
|
||||
e.two.bore=bore;e.two.stroke=stroke;e.two.rod*=scale;e.two.carburetor_diameter=carb;
|
||||
e.two.transfer_width*=scale;e.two.exhaust_width*=scale;e.two.crankcase_clearance*=scale*scale*scale;
|
||||
e.two.minimum_burn_seconds*=scale;
|
||||
for(auto& section:e.two.racing_pipe) {section.length*=scale;section.inlet_diameter*=scale;section.outlet_diameter*=scale;}
|
||||
e.two.exhaust_cell_length=.015f;
|
||||
};
|
||||
two("fifty","50 cc / little screamer","Two-stroke / 50 cc",.040f,.0398f,.019f);
|
||||
two("fivehundred","500 cc / big single","Two-stroke / 500 cc",.086f,.086f,.041f);
|
||||
auto custom=[&](const char* id,const char* title,const char* category,int count,EngineLayout layout,float bore,float stroke)->ScenarioExample& {
|
||||
auto& e=add(id,title,"Estimated concept calibration / EFI",ScenarioKind::Twin);e.category=category;
|
||||
e.engine.cylinders=count;e.engine.layout=layout;e.engine.bore=bore;e.engine.stroke=stroke;
|
||||
e.engine.rod=std::max(.11f,stroke*1.8f);return e;
|
||||
};
|
||||
custom("single-450","450 cc / four-stroke thumper","Singles / four-stroke",1,EngineLayout::Inline,.096f,.062f);
|
||||
custom("single-650","650 cc / big thumper","Singles / four-stroke",1,EngineLayout::Inline,.100f,.083f).engine.rev_limit=6500;
|
||||
auto& twin270=custom("twin-270","700 cc / 270-degree parallel twin","Twins / parallel",2,EngineLayout::Inline,.080f,.068f);
|
||||
twin270.engine.custom_phases=true;twin270.engine.phase_degrees[1]=450;
|
||||
auto& ducati=custom("ltwin","916-inspired / 90-degree V twin","Twins / V",2,EngineLayout::V,.094f,.066f);
|
||||
ducati.engine.bank_angle=90;ducati.engine.custom_phases=true;ducati.engine.phase_degrees[1]=450;
|
||||
custom("triple","1.0 litre / inline triple","Inline three",3,EngineLayout::Inline,.079f,.068f);
|
||||
auto& four=custom("f20","F20C-inspired / 2.0 high-rev four","Inline four / specials",4,EngineLayout::Inline,.087f,.084f);four.engine.rev_limit=9000;
|
||||
custom("five","Audi-inspired / 2.2 inline five","Inline five",5,EngineLayout::Inline,.081f,.0864f);
|
||||
custom("rb26","RB26-inspired / 2.6 inline six NA","Inline six",6,EngineLayout::Inline,.086f,.0737f);
|
||||
custom("six-long","3.0 litre / long-stroke inline six","Inline six",6,EngineLayout::Inline,.086f,.086f);
|
||||
custom("busso","Busso-inspired / 3.0 V6","V6",6,EngineLayout::V,.093f,.0726f);
|
||||
auto& flat4=custom("ej20","EJ20-inspired / 2.0 flat four NA","Boxer / flat four",4,EngineLayout::Boxer,.092f,.075f);
|
||||
flat4.engine.bank_angle=180;
|
||||
custom("flat6","911-inspired / 3.0 flat six","Boxer / flat six",6,EngineLayout::Boxer,.095f,.0704f).engine.bank_angle=180;
|
||||
auto& big=custom("big-carb","8.0 litre / carbureted big-block","V8 / big-block",8,EngineLayout::V,.1143f,.098f);
|
||||
big.engine.carb_pilot_scale=.4f;big.engine.carb_jet_scale=.47f;big.engine.bank_angle=90;big.engine.compression=9;big.engine.carbureted=true;big.engine.idle_rpm=800;big.engine.idle_opening=.08f;
|
||||
big.description="Carb fuel transport / fixed configurable idle stop";
|
||||
custom("v10","5.0 litre / even-fire V10","V10",10,EngineLayout::V,.092f,.0752f).engine.bank_angle=72;
|
||||
custom("v12","250-inspired / 3.0 V12","V12",12,EngineLayout::V,.073f,.0588f).engine.bank_angle=60;
|
||||
return examples;
|
||||
}
|
||||
ScenarioExample load_engine_file(const std::filesystem::path& path);
|
||||
std::vector<ScenarioExample> load_engine_directory(const std::filesystem::path& directory);
|
||||
// FLUIDSIM_ENGINE_DIR overrides discovery. Otherwise search the working directory,
|
||||
// beside the executable, then the source tree for developer tools.
|
||||
std::filesystem::path engine_directory();
|
||||
const std::vector<ScenarioExample>& scenario_examples();
|
||||
const ScenarioExample& scenario_example(const std::string& id);
|
||||
const ScenarioExample& preset_engine_definition(FourStrokePreset preset);
|
||||
}
|
||||
|
||||
16
include/FluidSim/ScenarioGeometry.hpp
Normal file
16
include/FluidSim/ScenarioGeometry.hpp
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
#include "FluidSim/AudioProcessing.hpp"
|
||||
#include <array>
|
||||
|
||||
namespace fluidsim::port {
|
||||
struct ScenarioSnapshot;
|
||||
// Fractions refer to simulated pipe length; positions are world metres.
|
||||
// Both the pipe mesh and its acoustic mouths use these same routes.
|
||||
struct PipeRoute {
|
||||
std::array<Vec3, 5> points{};
|
||||
std::array<float, 5> fractions{};
|
||||
unsigned count{};
|
||||
};
|
||||
std::array<PipeRoute, 32> scenario_pipe_routes(const ScenarioSnapshot& state);
|
||||
std::array<Vec3, 3> scenario_sound_sources(const ScenarioSnapshot& state);
|
||||
}
|
||||
54
include/FluidSim/ScenarioSnapshot.hpp
Normal file
54
include/FluidSim/ScenarioSnapshot.hpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
#include "FluidSim/EngineConfig.hpp"
|
||||
#include "FluidSim/Fluid.hpp"
|
||||
#include "FluidSim/OutdoorAudio.hpp"
|
||||
#include "FluidSim/Silencer.hpp"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
struct ScenarioSnapshot {
|
||||
std::string example_name;
|
||||
EngineLayout layout{EngineLayout::Inline};
|
||||
float bank_angle{};
|
||||
bool v8_headers{};
|
||||
bool staged_headers{}, dual_exhaust{};
|
||||
PipeSolver pipe_solver{PipeSolver::Rusanov};
|
||||
int cylinder_count{2};
|
||||
float rev_limit_rpm{10000};
|
||||
std::array<float, 12> cylinder_angles{};
|
||||
std::array<float, 12> cylinder_pressures{}, piston_fractions{}, intake_areas{}, exhaust_areas{};
|
||||
std::uint64_t solver_substeps{}, boundary_iterations{}, pipe_positivity_repairs{};
|
||||
float rpm{}, speed_kmh{}, kinetic_energy{}, average_power{}, average_torque{};
|
||||
float cylinder_pressure1{}, cylinder_pressure2{};
|
||||
float cylinder_temperature1{}, cylinder_temperature2{};
|
||||
float cylinder_piston_fraction1{}, cylinder_piston_fraction2{};
|
||||
float valve1_intake_area{}, valve1_exhaust_area{}, valve2_intake_area{}, valve2_exhaust_area{};
|
||||
float intake_pressure{}, exhaust_pressure{}, second_bank_pressure{};
|
||||
std::array<float,2> crossover_pressures{};
|
||||
float throttle{}, pedal{}, target_afr{}, clutch{}, intake_flow{}, exhaust_flow{};
|
||||
bool idle_control{}, fuel_cut{}, ignition_cut{}, two_stroke{}, reed_open{};
|
||||
bool racing_exhaust{};
|
||||
bool silencers{}, h_pipe{};
|
||||
ExhaustConfig exhaust_config{};
|
||||
float crankcase_pressure{};
|
||||
OutdoorScene audio_scene{OutdoorScene::Wall};
|
||||
int gear{};
|
||||
bool rev_limit1{}, rev_limit2{};
|
||||
std::array<std::vector<GasState>, 32> pipe_states;
|
||||
std::array<std::vector<float>, 32> pipe_areas, pipe_lengths;
|
||||
void capture_pipes(const std::vector<Pipe1D>& pipes) {
|
||||
for (std::size_t k = 0; k < pipes.size(); ++k) {
|
||||
pipe_positivity_repairs += pipes[k].positivity_repairs;
|
||||
pipe_areas[k] = pipes[k].cell_areas;
|
||||
pipe_lengths[k] = pipes[k].cell_lengths;
|
||||
pipe_states[k].reserve(pipes[k].cells.size());
|
||||
for (std::size_t i = 0; i < pipes[k].cells.size(); ++i)
|
||||
pipe_states[k].push_back(pipes[k].state(static_cast<int>(i)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
#include "FluidSim/PipeProfile.hpp"
|
||||
#include "FluidSim/Ecu.hpp"
|
||||
#include "FluidSim/Vehicle.hpp"
|
||||
#include <array>
|
||||
|
||||
namespace fluidsim::port {
|
||||
@@ -8,14 +10,40 @@ enum class TwoStrokeExhaust { Racing, Straight };
|
||||
// YZ125-sized baseline. Port/chamber/combustion dimensions are explicit design
|
||||
// estimates, not a measured Yamaha cylinder or factory exhaust/ignition map.
|
||||
struct TwoStrokeConfig {
|
||||
EcuConfig ecu=[] {
|
||||
EcuConfig e;
|
||||
e.idle_control=EcuConfig::IdleControl::FixedOpening;
|
||||
e.throttle_time_constant=.0002f;e.throttle_rate=5000;
|
||||
e.ignition_map={{0,10},{3000,18},{6000,24},{9000,18},{11000,12},{13000,6},{15000,2},{18000,0}};
|
||||
e.idle_rpm=3000;e.idle_max_opening=.3f;e.rev_limit_enabled=false;
|
||||
e.overrun_cut_rpm=5000;e.overrun_resume_rpm=3500;
|
||||
e.rev_limit_fuel_cut_enabled=false;e.overrun_fuel_cut_enabled=false;e.overrun_ignition_cut_enabled=true;
|
||||
return e;
|
||||
}();
|
||||
VehicleConfig vehicle=[] {
|
||||
VehicleConfig v;
|
||||
v.primary_reduction=64.f/19;v.final_drive_ratio=49.f/13;
|
||||
v.gear_ratios={31.f/13,29.f/15,27.f/17,23.f/17,20.f/17,19.f/18};
|
||||
v.mass=170;v.wheel_radius=.3313f;
|
||||
return v;
|
||||
}();
|
||||
float initial_rpm{3000}, starter_torque{8}, maximum_load_torque{10};
|
||||
float crankshaft_inertia{.01f};
|
||||
TwoStrokeExhaust exhaust{TwoStrokeExhaust::Racing};
|
||||
float bore{.054f}, stroke{.0545f}, rod{.110f};
|
||||
float trapped_compression{8.2f}; // exhaust-closure volume / clearance volume
|
||||
float transfer_duration{130}, exhaust_duration{190};
|
||||
float transfer_width{.050f}, exhaust_width{.036f}; // summed effective window widths
|
||||
float pilot_jet_scale{.75f}, main_jet_scale{1.6f}, needle_jet_scale{1.f};
|
||||
float needle_low_scale{2.72f}, needle_mid_scale{.8f};
|
||||
float needle_off_idle_scale{12.f}, needle_transition_scale{9.f}, needle_tip_scale{5.f};
|
||||
float needle_quarter_scale{1.4f}, needle_upper_scale{.76f};
|
||||
float pilot_transition_scale{1.f}, pilot_tip_scale{1.f};
|
||||
float jet_scale{1.f}; // multiplier on effective jet diameters
|
||||
float crankcase_clearance{150e-6f};
|
||||
float carburetor_diameter{.0381f};
|
||||
float idle_spark_gain{.05f}; // spark degrees per RPM error at the fixed carb stop
|
||||
float idle_minimum_advance{-10.f};
|
||||
float idle_slide_opening{.045f}; // fixed mechanical stop, normalized 0..1 (4.5%)
|
||||
float minimum_burn_seconds{.0008f};
|
||||
// Empirical mechanical FMEP = A + B*mean_piston_speed + C*speed^2.
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace fluidsim::port {
|
||||
struct TwoStrokeSnapshot {
|
||||
float rpm{}, cylinder_pressure{}, crankcase_pressure{}, intake_pressure{};
|
||||
float throttle{}, transfer_area{}, exhaust_area{};
|
||||
bool reed_open{}, fuel_cut{};
|
||||
bool reed_open{}, fuel_cut{}, ignition_cut{};
|
||||
float pedal{};
|
||||
OutdoorScene audio_scene{OutdoorScene::Wall};
|
||||
};
|
||||
@@ -28,6 +28,7 @@ struct TwoStrokeSnapshot {
|
||||
// the plenum / reed / crankcase / transfer / cylinder / exhaust topology.
|
||||
class TwoStrokeScenario : public MotorcycleTest {
|
||||
public:
|
||||
void set_listener(AudioListener listener) { audio_.set_listener(listener); }
|
||||
explicit TwoStrokeScenario(int sample_rate = 44100, TwoStrokeConfig config = {});
|
||||
AudioFrame process();
|
||||
void set_throttle(float value);
|
||||
|
||||
@@ -5,16 +5,19 @@
|
||||
namespace fluidsim::port {
|
||||
|
||||
// Longitudinal vehicle model and the friction clutch coupling it to the crank.
|
||||
class Vehicle {
|
||||
public:
|
||||
int current_gear{};
|
||||
struct VehicleConfig {
|
||||
std::vector<float> gear_ratios{4.1f, 2.7f, 2.0f, 1.55f, 1.25f, 0.98f};
|
||||
float final_drive_ratio{2.7f}, primary_reduction{1.8f};
|
||||
float clutch_input{1.0f}, clutch_capacity{80.0f}, clutch_engagement_rate{8.0f};
|
||||
float clutch_capacity{80.0f}, clutch_engagement_rate{8.0f};
|
||||
float mass{160.0f}, wheel_radius{0.32f}, rotational_inertia{1.2f};
|
||||
float rolling_resistance{0.015f}, drag_coefficient{0.7f}, frontal_area{0.65f};
|
||||
float air_density{1.225f}, gravity{9.80665f};
|
||||
float speed{}, engagement{};
|
||||
};
|
||||
|
||||
class Vehicle : public VehicleConfig {
|
||||
public:
|
||||
int current_gear{};
|
||||
float clutch_input{1.0f}, speed{}, engagement{};
|
||||
|
||||
float total_gear_ratio() const;
|
||||
float kinetic_energy() const;
|
||||
|
||||
Reference in New Issue
Block a user