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:
526
src/engine_config.cpp
Normal file
526
src/engine_config.cpp
Normal file
@@ -0,0 +1,526 @@
|
||||
#include "FluidSim/ScenarioCatalog.hpp"
|
||||
#include <toml.hpp>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
namespace fluidsim::port {
|
||||
namespace {
|
||||
class Reader {
|
||||
const toml::table& table_;
|
||||
std::set<std::string> used_;
|
||||
std::string path_;
|
||||
public:
|
||||
Reader(const toml::table& table,std::string path):table_(table),path_(std::move(path)) {}
|
||||
[[noreturn]] void fail(const std::string& message) const { throw std::runtime_error(path_+": "+message); }
|
||||
void required(std::initializer_list<const char*> keys) const {
|
||||
for(const auto* key:keys)if(!table_.contains(key))fail(std::string("missing required field '")+key+"'");
|
||||
}
|
||||
const toml::node* node(const std::string& key) { used_.insert(key);return table_.get(key); }
|
||||
void field(const char* key,float& value,double scale=1) {
|
||||
if(const auto* n=node(key)) {
|
||||
auto number=n->value<double>();
|
||||
if(!number || !std::isfinite(*number) || std::abs(*number*scale)>std::numeric_limits<float>::max())
|
||||
fail(std::string(key)+" must be a finite number");
|
||||
value=static_cast<float>(*number*scale);
|
||||
}
|
||||
}
|
||||
void field(const char* key,int& value) {
|
||||
if(const auto* n=node(key)) {
|
||||
auto number=n->value<int64_t>();
|
||||
if(!n->is_integer() || !number || *number<std::numeric_limits<int>::min() || *number>std::numeric_limits<int>::max())
|
||||
fail(std::string(key)+" must be an integer");
|
||||
value=static_cast<int>(*number);
|
||||
}
|
||||
}
|
||||
void field(const char* key,bool& value) {
|
||||
if(const auto* n=node(key)) {
|
||||
auto b=n->value<bool>();if(!b)fail(std::string(key)+" must be true or false");value=*b;
|
||||
}
|
||||
}
|
||||
void field(const char* key,std::string& value) {
|
||||
if(const auto* n=node(key)) {
|
||||
auto s=n->value<std::string>();if(!s)fail(std::string(key)+" must be a string");value=*s;
|
||||
}
|
||||
}
|
||||
std::string text(const char* key,std::string fallback={}) {field(key,fallback);return fallback;}
|
||||
Reader section(const char* key,bool required=false) {
|
||||
static const toml::table empty;
|
||||
const auto* n=node(key);
|
||||
if(n && !n->is_table())fail(std::string(key)+" must be a table");
|
||||
if(!n && required)fail(std::string("missing [")+key+"]");
|
||||
return Reader(n?*n->as_table():empty,path_+"."+key);
|
||||
}
|
||||
const toml::array* array(const char* key) {
|
||||
const auto* n=node(key);
|
||||
if(n && !n->is_array())fail(std::string(key)+" must be an array");
|
||||
if(n && n->as_array()->size()>4096)fail(std::string(key)+" has too many entries");
|
||||
return n?n->as_array():nullptr;
|
||||
}
|
||||
float number(const toml::node& n) const {
|
||||
auto value=n.value<double>();
|
||||
if(!value || !std::isfinite(*value) || std::abs(*value)>std::numeric_limits<float>::max())fail("array entries must be finite numbers");
|
||||
return static_cast<float>(*value);
|
||||
}
|
||||
void field(const char* key,std::vector<float>& values) {
|
||||
if(const auto* a=array(key)) {values.clear();for(const auto& n:*a)values.push_back(number(n));}
|
||||
}
|
||||
void finish() const {
|
||||
for(const auto& [key,value]:table_)if(!used_.contains(std::string(key.str())))fail("unknown field '"+std::string(key.str())+"'");
|
||||
}
|
||||
};
|
||||
|
||||
template<class T> void require(bool condition,const T& where,const char* message) {
|
||||
if(!condition)throw std::runtime_error(std::string(where)+": "+message);
|
||||
}
|
||||
|
||||
// Scalar readers below keep the file units explicit at the conversion boundary.
|
||||
void read_cylinder(Reader r,CylinderConfig& c) {
|
||||
r.required({"bore_mm","stroke_mm","rod_mm","compression"});
|
||||
r.field("afr",c.afr);
|
||||
r.field("ambient_temperature_k",c.ambient_temperature);
|
||||
r.field("bore_mm",c.bore,0.001);
|
||||
r.field("burn_duration_deg",c.burn_duration);
|
||||
r.field("burn_start_deg",c.burn_start);
|
||||
r.field("compression",c.compression);
|
||||
r.field("energy_variation",c.energy_variation);
|
||||
r.field("exhaust_close_deg",c.exhaust_close);
|
||||
r.field("exhaust_open_deg",c.exhaust_open);
|
||||
r.field("exhaust_valve_count",c.exhaust_valve_count);
|
||||
r.field("exhaust_valve_diameter_mm",c.exhaust_valve_diameter,0.001);
|
||||
r.field("exhaust_valve_lift_mm",c.exhaust_valve_lift,0.001);
|
||||
r.field("fuel_lhv_j_per_kg",c.fuel_lhv);
|
||||
r.field("heat_transfer",c.heat_transfer);
|
||||
r.field("intake_close_deg",c.intake_close);
|
||||
r.field("intake_open_deg",c.intake_open);
|
||||
r.field("intake_valve_count",c.intake_valve_count);
|
||||
r.field("intake_valve_diameter_mm",c.intake_valve_diameter,0.001);
|
||||
r.field("intake_valve_lift_mm",c.intake_valve_lift,0.001);
|
||||
r.field("misfire_probability",c.misfire_probability);
|
||||
r.field("rod_mm",c.rod,0.001);
|
||||
r.field("stroke_mm",c.stroke,0.001);
|
||||
r.field("wall_area",c.wall_area);
|
||||
r.field("wiebe_a",c.wiebe_a);
|
||||
r.field("wiebe_m",c.wiebe_m);
|
||||
r.finish();
|
||||
}
|
||||
void read_crankshaft(Reader r,CrankshaftConfig& c) {
|
||||
r.field("friction_constant_nm",c.friction_constant);
|
||||
r.field("friction_quadratic_nm_s2",c.friction_quadratic);
|
||||
r.field("friction_viscous_nm_s",c.friction_viscous);
|
||||
r.field("inertia_kg_m2",c.inertia);
|
||||
r.finish();
|
||||
}
|
||||
void read_ecu(Reader r,EcuConfig& c) {
|
||||
r.field("idle_base_opening",c.idle_base_opening);
|
||||
r.field("idle_ki",c.idle_ki);
|
||||
r.field("idle_kp",c.idle_kp);
|
||||
r.field("idle_max_opening",c.idle_max_opening);
|
||||
r.field("idle_minimum_advance_deg",c.idle_minimum_advance);
|
||||
r.field("idle_rpm",c.idle_rpm);
|
||||
r.field("idle_spark_gain",c.idle_spark_gain);
|
||||
r.field("overrun_cut_rpm",c.overrun_cut_rpm);
|
||||
r.field("overrun_fuel_cut_enabled",c.overrun_fuel_cut_enabled);
|
||||
r.field("overrun_ignition_cut_enabled",c.overrun_ignition_cut_enabled);
|
||||
r.field("overrun_resume_rpm",c.overrun_resume_rpm);
|
||||
r.field("power_afr",c.power_afr);
|
||||
r.field("rev_limit_enabled",c.rev_limit_enabled);
|
||||
r.field("rev_limit_fuel_cut_enabled",c.rev_limit_fuel_cut_enabled);
|
||||
r.field("rev_limit_rpm",c.rev_limit_rpm);
|
||||
r.field("rev_resume_rpm",c.rev_resume_rpm);
|
||||
r.field("stoichiometric_afr",c.stoichiometric_afr);
|
||||
r.field("throttle_closing_rate",c.throttle_closing_rate);
|
||||
r.field("throttle_closing_time_constant_s",c.throttle_closing_time_constant);
|
||||
r.field("throttle_rate",c.throttle_rate);
|
||||
r.field("throttle_time_constant_s",c.throttle_time_constant);
|
||||
const auto mode=r.text("idle_control","feedback");
|
||||
if(mode!="fixed" && mode!="feedback")r.fail("idle_control must be fixed or feedback");
|
||||
c.idle_control=mode=="fixed"?EcuConfig::IdleControl::FixedOpening:EcuConfig::IdleControl::Feedback;
|
||||
if(const auto* curve=r.array("ignition_map")) {
|
||||
c.ignition_map.clear();float previous=-1;
|
||||
for(const auto& n:*curve) {
|
||||
const auto* point=n.as_array();
|
||||
if(!point || point->size()!=2)r.fail("ignition_map entries must be [RPM, degrees BTDC]");
|
||||
const float rpm=r.number(*point->get(0)),advance=r.number(*point->get(1));
|
||||
if(rpm<0 || rpm<=previous || advance < -90 || advance>90)r.fail("ignition_map needs increasing nonnegative RPM and advances within +/-90 degrees");
|
||||
c.ignition_map.push_back({rpm,advance});previous=rpm;
|
||||
}
|
||||
if(c.ignition_map.empty())r.fail("ignition_map must not be empty");
|
||||
}
|
||||
r.finish();
|
||||
}
|
||||
void read_vehicle(Reader r,VehicleConfig& c) {
|
||||
r.field("air_density",c.air_density);
|
||||
r.field("clutch_capacity",c.clutch_capacity);
|
||||
r.field("clutch_engagement_rate",c.clutch_engagement_rate);
|
||||
r.field("drag_coefficient",c.drag_coefficient);
|
||||
r.field("final_drive_ratio",c.final_drive_ratio);
|
||||
r.field("frontal_area",c.frontal_area);
|
||||
r.field("gear_ratios",c.gear_ratios);
|
||||
r.field("gravity",c.gravity);
|
||||
r.field("mass_kg",c.mass);
|
||||
r.field("primary_reduction",c.primary_reduction);
|
||||
r.field("rolling_resistance",c.rolling_resistance);
|
||||
r.field("rotational_inertia_kg_m2",c.rotational_inertia);
|
||||
r.field("wheel_radius_mm",c.wheel_radius,0.001);
|
||||
r.finish();
|
||||
}
|
||||
void read_silencer(Reader r,SilencerConfig& c) {
|
||||
r.field("chambers",c.chambers);
|
||||
r.field("core_diameter_mm",c.core_diameter,0.001);
|
||||
r.field("length_mm",c.length,0.001);
|
||||
r.field("loss_coefficient",c.loss_coefficient);
|
||||
r.field("passage_diameter_mm",c.passage_diameter,0.001);
|
||||
r.field("resistance",c.resistance);
|
||||
r.field("volume_litres",c.volume,0.001);
|
||||
const auto type=r.text("type","straight_through");
|
||||
if(type=="straight_through")c.type=SilencerType::StraightThrough;
|
||||
else if(type=="chambered")c.type=SilencerType::Chambered;
|
||||
else if(type=="expansion")c.type=SilencerType::ExpansionResonator;
|
||||
else r.fail("unknown silencer type '"+type+"'");
|
||||
r.finish();
|
||||
}
|
||||
void read_silencers(Reader r,ExhaustConfig& c) {
|
||||
r.field("cell_length_mm",c.cell_length,0.001);
|
||||
r.field("connecting_length_mm",c.connecting_length,0.001);
|
||||
r.field("diameter_mm",c.diameter,0.001);
|
||||
r.field("enabled",c.enabled);
|
||||
r.field("inlet_length_mm",c.inlet_length,0.001);
|
||||
r.field("outlet_length_mm",c.outlet_length,0.001);
|
||||
read_silencer(r.section("resonator"),c.resonator);
|
||||
read_silencer(r.section("muffler"),c.muffler);
|
||||
r.finish();
|
||||
}
|
||||
void read_two_stroke(Reader r,TwoStrokeConfig& c) {
|
||||
r.required({"bore_mm","stroke_mm","rod_mm","trapped_compression","carburetor_diameter_mm"});
|
||||
r.field("initial_rpm",c.initial_rpm);
|
||||
r.field("starter_torque_nm",c.starter_torque);
|
||||
r.field("maximum_load_torque_nm",c.maximum_load_torque);
|
||||
r.field("crankshaft_inertia_kg_m2",c.crankshaft_inertia);
|
||||
r.field("bore_mm",c.bore,0.001);
|
||||
r.field("carburetor_diameter_mm",c.carburetor_diameter,0.001);
|
||||
r.field("crankcase_clearance_cc",c.crankcase_clearance,1e-06);
|
||||
r.field("exhaust_cell_length_mm",c.exhaust_cell_length,0.001);
|
||||
r.field("exhaust_duration_deg",c.exhaust_duration);
|
||||
r.field("exhaust_width_mm",c.exhaust_width,0.001);
|
||||
r.field("friction_bar",c.friction_bar);
|
||||
r.field("friction_bar_per_mps",c.friction_bar_per_mps);
|
||||
r.field("friction_bar_per_mps2",c.friction_bar_per_mps2);
|
||||
r.field("idle_minimum_advance_deg",c.idle_minimum_advance);
|
||||
r.field("idle_slide_opening",c.idle_slide_opening);
|
||||
r.field("idle_spark_gain",c.idle_spark_gain);
|
||||
r.field("jet_scale",c.jet_scale);
|
||||
r.field("main_jet_scale",c.main_jet_scale);
|
||||
r.field("minimum_burn_s",c.minimum_burn_seconds);
|
||||
r.field("needle_jet_scale",c.needle_jet_scale);
|
||||
r.field("needle_low_scale",c.needle_low_scale);
|
||||
r.field("needle_mid_scale",c.needle_mid_scale);
|
||||
r.field("needle_off_idle_scale",c.needle_off_idle_scale);
|
||||
r.field("needle_quarter_scale",c.needle_quarter_scale);
|
||||
r.field("needle_tip_scale",c.needle_tip_scale);
|
||||
r.field("needle_transition_scale",c.needle_transition_scale);
|
||||
r.field("needle_upper_scale",c.needle_upper_scale);
|
||||
r.field("pilot_jet_scale",c.pilot_jet_scale);
|
||||
r.field("pilot_tip_scale",c.pilot_tip_scale);
|
||||
r.field("pilot_transition_scale",c.pilot_transition_scale);
|
||||
r.field("rod_mm",c.rod,0.001);
|
||||
r.field("stroke_mm",c.stroke,0.001);
|
||||
r.field("transfer_duration_deg",c.transfer_duration);
|
||||
r.field("transfer_width_mm",c.transfer_width,0.001);
|
||||
r.field("trapped_compression",c.trapped_compression);
|
||||
const auto exhaust=r.text("exhaust","racing");
|
||||
if(exhaust!="racing" && exhaust!="straight")r.fail("exhaust must be racing or straight");
|
||||
c.exhaust=exhaust=="racing"?TwoStrokeExhaust::Racing:TwoStrokeExhaust::Straight;
|
||||
if(const auto* sections=r.array("chamber_sections_mm")) {
|
||||
if(sections->size()!=c.racing_pipe.size())r.fail("chamber_sections_mm must have six sections");
|
||||
for(std::size_t i=0;i<sections->size();++i) {
|
||||
const auto* section=sections->get(i)->as_array();
|
||||
if(!section || section->size()!=3)r.fail("chamber sections must be [length, inlet diameter, outlet diameter] in mm");
|
||||
auto& p=c.racing_pipe[i];
|
||||
p.length=r.number(*section->get(0))*.001f;
|
||||
p.inlet_diameter=r.number(*section->get(1))*.001f;
|
||||
p.outlet_diameter=r.number(*section->get(2))*.001f;
|
||||
}
|
||||
}
|
||||
r.finish();
|
||||
}
|
||||
void read_engine(Reader r,EngineConfig& c) {
|
||||
r.required({"cylinders","layout","topology","phase_degrees"});
|
||||
r.field("bank_angle_deg",c.bank_angle);
|
||||
r.field("collector_volume_litres",c.collector_volume,0.001);
|
||||
r.field("cylinders",c.cylinders);
|
||||
r.field("initial_rpm",c.initial_rpm);
|
||||
r.field("initialize_phased_charge",c.initialize_phased_charge);
|
||||
r.field("maximum_load_torque_nm",c.maximum_load_torque);
|
||||
r.field("minimum_substeps",c.minimum_substeps);
|
||||
r.field("phase_degrees",c.phase_degrees);
|
||||
r.field("pipe_damping",c.pipe_damping);
|
||||
r.field("plenum_volume_litres",c.plenum_volume,0.001);
|
||||
r.field("starter_torque_nm",c.starter_torque);
|
||||
r.field("throttle_diameter_mm",c.throttle_diameter,0.001);
|
||||
const auto layout=r.text("layout","inline"),topology=r.text("topology","collector");
|
||||
if(layout=="inline")c.layout=EngineLayout::Inline;
|
||||
else if(layout=="v")c.layout=EngineLayout::V;
|
||||
else if(layout=="boxer")c.layout=EngineLayout::Boxer;
|
||||
else r.fail("layout must be inline, v or boxer");
|
||||
if(topology=="collector")c.topology=ExhaustTopology::Collector;
|
||||
else if(topology=="staged")c.topology=ExhaustTopology::Staged;
|
||||
else if(topology=="american_v8")c.topology=ExhaustTopology::AmericanV8;
|
||||
else r.fail("unknown exhaust topology '"+topology+"'");
|
||||
r.finish();
|
||||
}
|
||||
void read_fuel(Reader r,EngineConfig& c) {
|
||||
const auto system=r.text("system","injection");
|
||||
if(system!="injection" && system!="carburetor")r.fail("system must be injection or carburetor");
|
||||
c.carbureted=system=="carburetor";
|
||||
r.field("pilot_jet_diameter_mm",c.fuel.pilot_jet_diameter,0.001);
|
||||
r.field("main_jet_diameter_mm",c.fuel.main_jet_diameter,0.001);
|
||||
r.field("needle_jet_diameter_mm",c.fuel.needle_jet_diameter,0.001);
|
||||
r.field("needle_low_scale",c.fuel.needle_low_scale);
|
||||
r.field("needle_mid_scale",c.fuel.needle_mid_scale);
|
||||
r.field("needle_off_idle_scale",c.fuel.needle_off_idle_scale);
|
||||
r.field("needle_transition_scale",c.fuel.needle_transition_scale);
|
||||
r.field("needle_tip_scale",c.fuel.needle_tip_scale);
|
||||
r.field("needle_quarter_scale",c.fuel.needle_quarter_scale);
|
||||
r.field("needle_upper_scale",c.fuel.needle_upper_scale);
|
||||
r.field("pilot_transition_scale",c.fuel.pilot_transition_scale);
|
||||
r.field("pilot_tip_scale",c.fuel.pilot_tip_scale);
|
||||
r.field("fuel_density",c.fuel.fuel_density);
|
||||
r.field("pilot_discharge_coefficient",c.fuel.pilot_discharge_coefficient);
|
||||
r.field("main_discharge_coefficient",c.fuel.main_discharge_coefficient);
|
||||
r.field("bowl_head_pressure",c.fuel.bowl_head_pressure);
|
||||
r.field("film_fraction",c.fuel.film_fraction);
|
||||
r.field("film_time_constant",c.fuel.film_time_constant);
|
||||
r.field("overrun_rpm",c.fuel.overrun_rpm);
|
||||
r.field("overrun_exit_rpm",c.fuel.overrun_exit_rpm);
|
||||
r.field("bowl_capacity_kg",c.fuel.bowl_capacity);
|
||||
r.field("bowl_refill_kg_per_s",c.fuel.bowl_refill_rate);
|
||||
r.finish();
|
||||
}
|
||||
|
||||
void validate_ecu(const EcuConfig& c,const std::string& path) {
|
||||
require(c.idle_rpm>0 && c.idle_base_opening>=0 && c.idle_base_opening<=1 &&
|
||||
c.idle_max_opening>=c.idle_base_opening && c.idle_max_opening<=1,path,"ecu: invalid idle calibration");
|
||||
require(c.rev_limit_rpm>c.rev_resume_rpm && c.rev_resume_rpm>0 &&
|
||||
c.overrun_cut_rpm>c.overrun_resume_rpm && c.overrun_resume_rpm>0,path,"ecu: invalid cut/resume RPM");
|
||||
require(c.throttle_time_constant>0 && c.throttle_rate>0 && c.idle_spark_gain>=0 &&
|
||||
c.throttle_closing_time_constant>=0 && c.throttle_closing_rate>=0,path,"ecu: invalid actuator calibration");
|
||||
require(c.stoichiometric_afr>0 && c.power_afr>0,path,"ecu: AFR targets must be positive");
|
||||
}
|
||||
void validate_vehicle(const VehicleConfig& c,const std::string& path) {
|
||||
require(c.mass>0 && c.wheel_radius>0 && c.rotational_inertia>=0 &&
|
||||
c.primary_reduction>0 && c.final_drive_ratio>0 && c.clutch_capacity>0 &&
|
||||
c.clutch_engagement_rate>0 && c.rolling_resistance>=0 &&
|
||||
c.drag_coefficient>=0 && c.frontal_area>0 && c.air_density>0 && c.gravity>0,
|
||||
path,"vehicle: invalid dimensions, ratios or resistance");
|
||||
require(!c.gear_ratios.empty() && c.gear_ratios.size()<=16,path,"vehicle: expected 1..16 gear ratios");
|
||||
for(float ratio:c.gear_ratios)require(ratio>0,path,"vehicle: gear ratios must be positive");
|
||||
}
|
||||
void validate_two(const TwoStrokeConfig& c,const std::string& path) {
|
||||
require(c.bore>0 && c.bore<=1 && c.stroke>0 && c.stroke<=1 && c.rod>c.stroke*.5f &&
|
||||
c.trapped_compression>1 && c.trapped_compression<=50,path,"two_stroke: invalid cylinder geometry");
|
||||
require(c.transfer_duration>0 && c.transfer_duration<c.exhaust_duration && c.exhaust_duration<360 &&
|
||||
c.transfer_width>0 && c.exhaust_width>0 && c.crankcase_clearance>0,
|
||||
path,"two_stroke: invalid port timing or crankcase geometry");
|
||||
require(c.idle_slide_opening>=0 && c.idle_slide_opening<=1 && c.initial_rpm>0 && c.initial_rpm<=30000 &&
|
||||
c.crankshaft_inertia>0 && c.starter_torque>=0 && c.maximum_load_torque>=0 && c.idle_spark_gain>=0,
|
||||
path,"two_stroke: invalid idle or crankshaft calibration");
|
||||
require(c.carburetor_diameter>0 && c.minimum_burn_seconds>0 &&
|
||||
c.friction_bar>=0 && c.friction_bar_per_mps>=0 && c.friction_bar_per_mps2>=0,
|
||||
path,"two_stroke: invalid carburetor, burn time or friction");
|
||||
for(float value:{c.jet_scale,c.pilot_jet_scale,c.main_jet_scale,c.needle_jet_scale,
|
||||
c.needle_low_scale,c.needle_mid_scale,c.needle_off_idle_scale,c.needle_transition_scale,
|
||||
c.needle_tip_scale,c.needle_quarter_scale,c.needle_upper_scale,c.pilot_transition_scale,c.pilot_tip_scale})
|
||||
require(value>0,path,"two_stroke: jet and needle scales must be positive");
|
||||
require(c.exhaust_cell_length>=.002f && c.exhaust_cell_length<=1,path,"two_stroke: exhaust cell length outside 2..1000 mm");
|
||||
float total=0;
|
||||
for(auto p:c.racing_pipe) {
|
||||
require(p.length>0 && p.inlet_diameter>0 && p.outlet_diameter>0,path,"two_stroke: invalid chamber section");
|
||||
total+=p.length;
|
||||
}
|
||||
require(total/c.exhaust_cell_length<=4096,path,"two_stroke: chamber exceeds 4096 cells");
|
||||
validate_ecu(c.ecu,path);validate_vehicle(c.vehicle,path);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void validate_engine_config(const EngineConfig& c,const std::string& path) {
|
||||
require(c.cylinders>=1 && c.cylinders<=12 && (c.layout==EngineLayout::Inline || c.cylinders%2==0),
|
||||
path,"engine: expected 1..12 cylinders and even banked cylinder counts");
|
||||
const auto& cylinder=c.cylinder;
|
||||
require(cylinder.bore>0 && cylinder.bore<=1 && cylinder.stroke>0 && cylinder.stroke<=1 &&
|
||||
cylinder.rod>cylinder.stroke*.5f && cylinder.compression>1 && cylinder.compression<=50,
|
||||
path,"cylinder: invalid bore, stroke, rod or compression");
|
||||
require(cylinder.intake_valve_count>=1 && cylinder.intake_valve_count<=8 &&
|
||||
cylinder.exhaust_valve_count>=1 && cylinder.exhaust_valve_count<=8 &&
|
||||
cylinder.intake_valve_diameter>0 && cylinder.exhaust_valve_diameter>0 &&
|
||||
cylinder.intake_valve_lift>0 && cylinder.exhaust_valve_lift>0,path,"cylinder: invalid valve geometry");
|
||||
for(float angle:{cylinder.intake_open,cylinder.intake_close,cylinder.exhaust_open,cylinder.exhaust_close})
|
||||
require(angle>=0 && angle<720,path,"cylinder: valve events must be in [0,720) degrees");
|
||||
require(cylinder.intake_open!=cylinder.intake_close && cylinder.exhaust_open!=cylinder.exhaust_close &&
|
||||
cylinder.burn_duration>0 && cylinder.wiebe_a>0 && cylinder.wiebe_m>=0 &&
|
||||
cylinder.energy_variation>=0 && cylinder.energy_variation<=1 &&
|
||||
cylinder.misfire_probability>=0 && cylinder.misfire_probability<=1 &&
|
||||
cylinder.fuel_lhv>0 && cylinder.wall_area>0 && cylinder.heat_transfer>=0 && cylinder.ambient_temperature>0,
|
||||
path,"cylinder: invalid timing or combustion calibration");
|
||||
require(c.crankshaft.inertia>0 && c.crankshaft.friction_constant>=0 && c.crankshaft.friction_viscous>=0 &&
|
||||
c.crankshaft.friction_quadratic>=0,path,"crankshaft: invalid inertia or friction");
|
||||
require(c.initial_rpm>0 && c.initial_rpm<=30000 && c.starter_torque>=0 && c.maximum_load_torque>=0 &&
|
||||
c.plenum_volume>0 && c.collector_volume>0 && c.throttle_diameter>0 &&
|
||||
c.pipe_damping>=0 && c.minimum_substeps>=1 && c.minimum_substeps<=64 &&
|
||||
c.bank_angle>0 && c.bank_angle<=180,path,"engine: invalid startup, intake or solver calibration");
|
||||
require(c.phase_degrees.size()==static_cast<std::size_t>(c.cylinders),path,"engine: phase_degrees needs one value per cylinder");
|
||||
for(float phase:c.phase_degrees)require(std::isfinite(phase) && phase>=0 && phase<720,path,"engine: invalid cylinder phase");
|
||||
require(!c.american_headers() || (c.cylinders==8 && c.layout==EngineLayout::V),path,"engine: american_v8 topology requires a V8");
|
||||
require(!c.staged_headers() || c.cylinders>=3,path,"engine: staged topology requires at least three cylinders");
|
||||
const int expected=c.american_headers()?26:1+2*c.cylinders+(c.dual_outlets()?2:1)+(c.staged_headers()?c.header_groups():0);
|
||||
int count=0,cells=0,silenced=0;std::set<std::string> ids;
|
||||
for(const auto& p:c.pipes) {
|
||||
require(!p.id.empty() && ids.insert(p.id).second,path,"pipes: missing or duplicate pipe group id");
|
||||
require(p.count>=1 && p.count<=12,path,"pipes: count must be in 1..12");
|
||||
if(p.silenced) {
|
||||
require(c.has_silenced_exhaust() && c.topology==ExhaustTopology::Collector &&
|
||||
count==1+2*c.cylinders && p.count==1,path,"pipes: silenced profile must be the collector's final outlet");
|
||||
++silenced;
|
||||
} else {
|
||||
require(p.diameter>0 && p.diameter<=2 && p.length>0 && p.length<=100 && p.cells>=1 && p.cells<=4096,
|
||||
path,"pipes: invalid diameter, length or cell count");
|
||||
cells+=p.cells*p.count;
|
||||
}
|
||||
count+=p.count;
|
||||
}
|
||||
require(count==expected && count<=32 && cells<=32768,path,"pipes: groups do not match the selected topology or exceed mesh limits");
|
||||
require(silenced<=1,path,"pipes: only one silenced outlet is supported");
|
||||
if(c.carbureted) {
|
||||
const auto& f=c.fuel;
|
||||
for(float value:{f.pilot_jet_diameter,f.main_jet_diameter,f.needle_jet_diameter,f.needle_low_scale,
|
||||
f.needle_mid_scale,f.needle_off_idle_scale,f.needle_transition_scale,f.needle_tip_scale,
|
||||
f.needle_quarter_scale,f.needle_upper_scale,f.pilot_transition_scale,f.pilot_tip_scale,
|
||||
f.fuel_density,f.pilot_discharge_coefficient,f.main_discharge_coefficient,f.bowl_capacity,f.bowl_refill_rate,f.film_time_constant})
|
||||
require(value>0,path,"fuel: jets, scales, density and time constants must be positive");
|
||||
require(f.film_fraction>=0 && f.film_fraction<=.8f && f.bowl_head_pressure>=0 &&
|
||||
f.overrun_rpm>f.overrun_exit_rpm && f.overrun_exit_rpm>0,path,"fuel: invalid film or overrun calibration");
|
||||
}
|
||||
validate_ecu(c.ecu,path);validate_vehicle(c.vehicle,path);
|
||||
}
|
||||
|
||||
ScenarioExample load_engine_file(const std::filesystem::path& path) {
|
||||
toml::table table;
|
||||
try { table=toml::parse_file(path.string()); }
|
||||
catch(const toml::parse_error& error) {std::ostringstream message;message<<error;throw std::runtime_error(message.str());}
|
||||
Reader root(table,path.string());
|
||||
int version=0;root.field("schema_version",version);
|
||||
if(version!=1)root.fail("schema_version must be 1");
|
||||
ScenarioExample e;
|
||||
root.field("id",e.id);root.field("name",e.title);root.field("category",e.category);
|
||||
root.field("description",e.description);root.field("order",e.order);
|
||||
if(e.id.empty() || e.title.empty() || e.category.empty())root.fail("id, name and category are required");
|
||||
if(e.id.find_first_not_of("abcdefghijklmnopqrstuvwxyz0123456789-_")!=std::string::npos)root.fail("id must use lowercase letters, digits, - or _");
|
||||
const auto cycle=root.text("cycle");
|
||||
if(cycle=="two_stroke") {
|
||||
e.kind=ScenarioKind::TwoStroke;
|
||||
read_two_stroke(root.section("two_stroke",true),e.two);
|
||||
read_ecu(root.section("ecu"),e.two.ecu);
|
||||
read_vehicle(root.section("vehicle"),e.two.vehicle);
|
||||
validate_two(e.two,path.string());
|
||||
} else if(cycle=="four_stroke") {
|
||||
read_engine(root.section("engine",true),e.engine);
|
||||
read_cylinder(root.section("cylinder",true),e.engine.cylinder);
|
||||
read_crankshaft(root.section("crankshaft",true),e.engine.crankshaft);
|
||||
read_ecu(root.section("ecu",true),e.engine.ecu);
|
||||
read_vehicle(root.section("vehicle",true),e.engine.vehicle);
|
||||
read_fuel(root.section("fuel",true),e.engine);
|
||||
read_silencers(root.section("silencers"),e.exhaust);
|
||||
auto junctions=root.section("junctions");
|
||||
junctions.field("h_pipe",e.junctions.h_pipe);junctions.field("junction_volume_cc",e.junctions.junction_volume,1e-6);
|
||||
junctions.finish();
|
||||
if(const auto* pipes=root.array("pipes"))for(const auto& value:*pipes) {
|
||||
if(!value.is_table())root.fail("pipes entries must be tables");
|
||||
Reader r(*value.as_table(),path.string()+".pipes");PipeConfig p;
|
||||
r.field("id",p.id);r.field("count",p.count);
|
||||
const auto profile=r.text("profile");
|
||||
if(!profile.empty() && profile!="silenced")r.fail("unknown pipe profile '"+profile+"'");
|
||||
p.silenced=profile=="silenced";
|
||||
r.field("diameter_mm",p.diameter,.001);r.field("length_mm",p.length,.001);r.field("cells",p.cells);
|
||||
r.finish();e.engine.pipes.push_back(std::move(p));
|
||||
}
|
||||
validate_engine_config(e.engine,path.string());
|
||||
require(e.junctions.junction_volume>0 && e.junctions.junction_volume<=1,path.string(),"junction_volume_cc outside (0,1000000]");
|
||||
if(e.engine.has_silenced_exhaust()) {
|
||||
require(e.exhaust.cell_length>=.002f && e.exhaust.cell_length<=1,path.string(),"silencers: cell length outside 2..1000 mm");
|
||||
require(e.exhaust.diameter>0 && e.exhaust.diameter<=2 && e.exhaust.inlet_length>0 &&
|
||||
e.exhaust.outlet_length>0 && e.exhaust.connecting_length>0,
|
||||
path.string(),"silencers: invalid connecting pipe geometry");
|
||||
float length=e.exhaust.inlet_length+e.exhaust.outlet_length+e.exhaust.connecting_length;
|
||||
for(const auto& s:{e.exhaust.resonator,e.exhaust.muffler}) {
|
||||
require(s.volume>0 && s.volume<=1 && s.length>0 && s.core_diameter>0 && s.core_diameter<=2 &&
|
||||
s.passage_diameter>0 && s.passage_diameter<=2 && s.chambers>=1 && s.chambers<=16 &&
|
||||
s.resistance>=0 && s.loss_coefficient>=0,path.string(),"silencers: invalid chamber geometry or resistance");
|
||||
length+=s.length;
|
||||
}
|
||||
require(length/e.exhaust.cell_length+6*(e.exhaust.resonator.chambers+e.exhaust.muffler.chambers)+6<=4096,
|
||||
path.string(),"silencers: mesh exceeds 4096 cells");
|
||||
try {const auto pipe=make_silenced_exhaust(e.exhaust);require(pipe.cells.size()<=4096,path.string(),"silencers: mesh exceeds 4096 cells");}
|
||||
catch(const std::exception& error) {root.fail(std::string("silencers: ")+error.what());}
|
||||
}
|
||||
} else root.fail("cycle must be two_stroke or four_stroke");
|
||||
root.finish();return e;
|
||||
}
|
||||
|
||||
std::vector<ScenarioExample> load_engine_directory(const std::filesystem::path& directory) {
|
||||
require(std::filesystem::is_directory(directory),directory.string(),"engine directory does not exist");
|
||||
std::vector<std::filesystem::path> files;
|
||||
for(const auto& entry:std::filesystem::directory_iterator(directory))
|
||||
if(entry.is_regular_file() && entry.path().extension()==".toml")files.push_back(entry.path());
|
||||
std::sort(files.begin(),files.end());
|
||||
std::vector<ScenarioExample> engines;std::set<std::string> ids;
|
||||
for(const auto& file:files) {
|
||||
auto engine=load_engine_file(file);
|
||||
require(ids.insert(engine.id).second,file.string(),"duplicate engine id");
|
||||
engines.push_back(std::move(engine));
|
||||
}
|
||||
require(!engines.empty(),directory.string(),"no .toml engine definitions found");
|
||||
std::sort(engines.begin(),engines.end(),[](const auto& a,const auto& b) {
|
||||
return a.order==b.order?a.id<b.id:a.order<b.order;
|
||||
});
|
||||
return engines;
|
||||
}
|
||||
|
||||
std::filesystem::path engine_directory() {
|
||||
if(const char* override_path=std::getenv("FLUIDSIM_ENGINE_DIR");override_path && *override_path)return override_path;
|
||||
std::vector<std::filesystem::path> candidates{std::filesystem::current_path()/"engines"};
|
||||
#ifdef _WIN32
|
||||
std::wstring executable(32768,L'\0');
|
||||
const auto size=GetModuleFileNameW(nullptr,executable.data(),static_cast<DWORD>(executable.size()));
|
||||
if(size>0 && size<executable.size()) {executable.resize(size);candidates.push_back(std::filesystem::path(executable).parent_path()/"engines");}
|
||||
#elif defined(__linux__)
|
||||
std::error_code error;
|
||||
const auto executable=std::filesystem::read_symlink("/proc/self/exe",error);
|
||||
if(!error)candidates.push_back(executable.parent_path()/"engines");
|
||||
#endif
|
||||
candidates.emplace_back(FLUIDSIM_ENGINE_SOURCE_DIR);
|
||||
for(const auto& candidate:candidates)if(std::filesystem::is_directory(candidate))return candidate;
|
||||
throw std::runtime_error("Cannot find engines/. Set FLUIDSIM_ENGINE_DIR to the engine definitions directory.");
|
||||
}
|
||||
const std::vector<ScenarioExample>& scenario_examples() {
|
||||
static const auto engines=load_engine_directory(engine_directory());return engines;
|
||||
}
|
||||
const ScenarioExample& scenario_example(const std::string& id) {
|
||||
const auto& engines=scenario_examples();
|
||||
const auto found=std::find_if(engines.begin(),engines.end(),[&](const auto& e) {return e.id==id;});
|
||||
if(found==engines.end())throw std::runtime_error("Unknown engine id '"+id+"' in "+engine_directory().string());
|
||||
return *found;
|
||||
}
|
||||
const ScenarioExample& preset_engine_definition(FourStrokePreset preset) {
|
||||
return scenario_example(preset==FourStrokePreset::AmericanV8?"v8-h":preset==FourStrokePreset::SaabB205?"saab-straight":"twin");
|
||||
}
|
||||
} // namespace fluidsim::port
|
||||
Reference in New Issue
Block a user