#include "FluidSim/ScenarioCatalog.hpp" #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX #endif #include #endif namespace fluidsim::port { namespace { class Reader { const toml::table& table_; std::set used_; std::string path_; public: Reader(const toml::table& table,std::string path):table_(table),path_(std::move(path)) {} const std::string& context() const {return path_;} [[noreturn]] void fail(const std::string& message) const { throw std::runtime_error(path_+": "+message); } void required(std::initializer_list 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(); if(!number || !std::isfinite(*number) || std::abs(*number*scale)>std::numeric_limits::max()) fail(std::string(key)+" must be a finite number"); value=static_cast(*number*scale); } } void field(const char* key,int& value) { if(const auto* n=node(key)) { auto number=n->value(); if(!n->is_integer() || !number || *number::min() || *number>std::numeric_limits::max()) fail(std::string(key)+" must be an integer"); value=static_cast(*number); } } void field(const char* key,bool& value) { if(const auto* n=node(key)) { auto b=n->value();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();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,double scale=1) const { auto value=n.value(); if(!value || !std::isfinite(*value) || std::abs(*value*scale)>std::numeric_limits::max())fail("array entries must be finite numbers"); return static_cast(*value*scale); } void field(const char* key,std::vector& 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 void require(bool condition,const T& where,const char* message) { if(!condition)throw std::runtime_error(std::string(where)+": "+message); } Vec3 read_xyz(Reader& r,const toml::node& node) { const auto* a=node.as_array();if(!a||a->size()!=3)r.fail("coordinates require three numbers in mm"); return {r.number(*a->get(0),.001),r.number(*a->get(1),.001),r.number(*a->get(2),.001)}; } void read_geometry(Reader r,PhysicalGeometry& g) { if(const auto* pipes=r.array("pipes"))for(const auto& value:*pipes) { if(!value.is_table())r.fail("geometry pipes must be tables"); Reader p(*value.as_table(),r.context()+".pipes");int index=-1;p.field("index",index); if(index!=int(g.pipes.size()))p.fail("pipe indices must be consecutive, starting at zero"); const auto* points=p.array("points_mm");if(!points)p.fail("points_mm is required"); GeometryPath path;for(const auto& point:*points)path.points.push_back(read_xyz(p,point)); p.finish();g.pipes.push_back(std::move(path)); } if(const auto* volumes=r.array("volumes"))for(const auto& value:*volumes) { if(!value.is_table())r.fail("geometry volumes must be tables"); Reader v(*value.as_table(),r.context()+".volumes");GeometryVolume volume;v.field("id",volume.id); const auto* size=v.node("size_mm");const auto* center=v.node("center_mm");const auto* corners=v.array("corners_mm"); if(corners) { if(size||center||corners->size()!=2)v.fail("use two corners_mm or size_mm with center_mm"); auto a=read_xyz(v,*corners->get(0)),b=read_xyz(v,*corners->get(1)); volume.box.center={(a.x+b.x)*.5f,(a.y+b.y)*.5f,(a.z+b.z)*.5f}; volume.box.size={std::abs(a.x-b.x),std::abs(a.y-b.y),std::abs(a.z-b.z)}; } else { if(!size)v.fail("volume requires size_mm or corners_mm"); volume.box.size=read_xyz(v,*size);if(center)volume.box.center=read_xyz(v,*center); } v.finish();g.volumes.push_back(std::move(volume)); } if(const auto* cylinders=r.array("cylinders"))for(const auto& value:*cylinders) { if(!value.is_table())r.fail("geometry cylinders must be tables"); Reader c(*value.as_table(),r.context()+".cylinders");CylinderPlacement placement; const auto* origin=c.node("origin_mm");if(!origin)c.fail("origin_mm is required"); placement.origin=read_xyz(c,*origin);c.field("tilt_deg",placement.tilt_degrees);c.finish();g.cylinders.push_back(placement); } r.finish(); } toml::table geometry_table(const PhysicalGeometry& geometry) { const auto xyz=[](Vec3 p){const auto mm=[](float x){return std::round(double(x)*1e7)/1e4;};return toml::array{mm(p.x),mm(p.y),mm(p.z)};}; toml::array pipes,volumes,cylinders; for(std::size_t i=0;isize()!=2)r.fail("cam points must be [angle_deg, lift_mm]"); cam.points.push_back({r.number(*point->get(0)),r.number(*point->get(1),0.001)}); } r.finish(); } void read_valve(Reader r,FourStrokeCylinder::ValveDynamics& v) { r.field("mass_g",v.mass_kg,0.001);r.field("spring_rate_n_per_mm",v.spring_rate_n_per_m,1000); r.field("damping_n_s_per_m",v.damping_n_s_per_m);r.field("seat_force_n",v.seat_force_n); r.field("maximum_lift_mm",v.maximum_lift_m,0.001);r.field("seat_restitution",v.seat_restitution); r.field("cam_restitution",v.cam_restitution);r.field("stop_restitution",v.stop_restitution);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); float diameter=0;r.field("diameter_mm",diameter,.001); if(diameter>0)c.volume=pi*.25f*diameter*diameter*c.length; 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("reed_pipe_diameter_mm",c.reed_pipe_diameter,.001); r.field("transfer_pipe_diameter_mm",c.transfer_pipe_diameter,.001); 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;isize();++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_duration0 && 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.reed_pipe_diameter>0&&c.reed_pipe_diameter<=2&&c.transfer_pipe_diameter>0&&c.transfer_pipe_diameter<=2&&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(std::isfinite(c.cells_per_meter) && (c.cells_per_meter==0 || (c.cells_per_meter>=1 && c.cells_per_meter<=500)),path,"invalid cells_per_meter"); 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,path,"cylinder: invalid valve geometry"); auto valid_cam=[&](const FourStrokeCylinder::CamProfile& cam,const FourStrokeCylinder::ValveDynamics& valve) { require(cam.points.size()>=3 && cam.points.front().angle_degrees==0 && cam.points.back().angle_degrees==720,path,"cam: profile must cover 0..720 degrees"); float previous=-1; for(const auto& point:cam.points) {require(point.angle_degrees>previous && point.angle_degrees<=720 && point.lift_metres>=0 && point.lift_metres<=valve.maximum_lift_m,path,"cam: points must increase and lifts fit valve travel");previous=point.angle_degrees;} require(cam.points.front().lift_metres==0 && cam.points.back().lift_metres==0,path,"cam: both cycle endpoints must have zero lift"); require(valve.mass_kg>0 && valve.spring_rate_n_per_m>0 && valve.damping_n_s_per_m>=0 && valve.seat_force_n>0 && valve.maximum_lift_m>0 && valve.seat_restitution>=0 && valve.seat_restitution<=1 && valve.cam_restitution>=0 && valve.cam_restitution<=1 && valve.stop_restitution>=0 && valve.stop_restitution<=1,path,"valves: invalid mass, spring or restitution"); }; valid_cam(c.intake_cam,c.intake_valve);valid_cam(c.exhaust_cam,c.exhaust_valve); require(c.valve_sound.mode_hz>0 && c.valve_sound.effective_mass_kg>0 && c.valve_sound.radiation_area>=0 && c.valve_sound.damping_ratio>=0,path,"valve_sound: invalid resonance calibration"); require( 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(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 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 && (c.cells_per_meter>0 || (p.cells>=1 && p.cells<=4096)), path,"pipes: invalid diameter, length or cell count"); cells+=(c.cells_per_meter>0?mesh_cell_count(p.length,c.cells_per_meter):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<500) simulation.fail("cells_per_meter must be in 1..500"); } simulation.finish(); e.engine.cells_per_meter=e.two.cells_per_meter=density; if(density>0) { e.two.exhaust_cell_length=1/density;e.exhaust.cell_length=1/density; } const auto cycle=root.text("cycle"); if(cycle=="two_stroke") { e.kind=ScenarioKind::TwoStroke; read_two_stroke(root.section("two_stroke",true),e.two); if(density>0) e.two.exhaust_cell_length=1/density; read_ecu(root.section("ecu"),e.two.ecu); read_vehicle(root.section("vehicle"),e.two.vehicle); read_geometry(root.section("geometry"),e.two.geometry); if(version==2&&e.two.geometry.empty())root.fail("geometry requires explicit pipe paths"); try {e.two=resolve_two_stroke_geometry(std::move(e.two));}catch(const std::exception& error){root.fail(error.what());} 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); auto cams=root.section("cam",true); read_cam(cams.section("intake",true),e.engine.intake_cam);read_cam(cams.section("exhaust",true),e.engine.exhaust_cam);cams.finish(); auto valves=root.section("valves",true); auto intake_valve=valves.section("intake",true),exhaust_valve=valves.section("exhaust",true); read_valve(intake_valve,e.engine.intake_valve);read_valve(exhaust_valve,e.engine.exhaust_valve);valves.finish(); auto sound=root.section("valve_sound",true); sound.field("mode_hz",e.engine.valve_sound.mode_hz);sound.field("effective_mass_g",e.engine.valve_sound.effective_mass_kg,0.001); sound.field("radiation_area_cm2",e.engine.valve_sound.radiation_area,0.0001);sound.field("damping_ratio",e.engine.valve_sound.damping_ratio);sound.finish(); 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.field("model",e.junctions.model);junctions.field("turning_loss",e.junctions.turning_loss); if(e.junctions.model!="volume" && e.junctions.model!="characteristic" && e.junctions.model!="legacy") junctions.fail("model must be volume, characteristic, or legacy"); if(e.junctions.turning_loss<0 || e.junctions.turning_loss>10) junctions.fail("turning_loss outside 0..10"); if(const auto* branches=junctions.array("branches")) for(const auto& value:*branches) { if(!value.is_table()) junctions.fail("branches entries must be tables"); Reader r(*value.as_table(),path.string()+".junctions.branches"); r.required({"pipe","left","direction"}); JunctionBranchConfig branch; r.field("pipe",branch.pipe);r.field("left",branch.left); std::vector direction;r.field("direction",direction); if(branch.pipe<0 || direction.size()!=3) r.fail("branch requires nonnegative pipe and three direction coordinates"); double norm=0;for(int i=0;i<3;++i) { branch.direction[i]=direction[i];norm+=double(direction[i])*direction[i]; } if(!(norm>0)) r.fail("branch direction must be nonzero"); for(const auto& previous:e.junctions.branches) if(previous.pipe==branch.pipe && previous.left==branch.left) r.fail("duplicate branch endpoint"); r.finish();e.junctions.branches.push_back(branch); } 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)); } read_geometry(root.section("geometry"),e.engine.geometry); if(version==2&&e.engine.geometry.empty())root.fail("geometry requires explicit pipe paths"); try {e.engine=resolve_engine_geometry(std::move(e.engine),e.exhaust,e.junctions);}catch(const std::exception& error){root.fail(error.what());} if(density>0)for(auto& p:e.engine.pipes)if(!p.silenced)p.cells=mesh_cell_count(p.length,density); if(density>0) e.exhaust.cell_length=1/density; 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; } void migrate_engine_geometry(const std::filesystem::path& path,bool relayout) { const auto definition=load_engine_file(path); auto geometry=definition.kind==ScenarioKind::TwoStroke?definition.two.geometry:definition.engine.geometry; if(relayout)geometry=definition.kind==ScenarioKind::TwoStroke?default_geometry(definition.two):default_geometry(definition.engine,definition.exhaust,definition.junctions); auto table=toml::parse_file(path.string()); const bool already_geometry=table["schema_version"].value_or(1)==2; if(already_geometry&&!relayout)return; auto xyz=[](Vec3 p){const auto mm=[](float x){return std::round(double(x)*1e7)/1e4;};return toml::array{mm(p.x),mm(p.y),mm(p.z)};}; toml::array pipes,volumes,cylinders; for(std::size_t i=0;ierase("length_mm"); if(auto* junction=table["junctions"].as_table())junction->erase("junction_volume_cc"); if(auto* silencers=table["silencers"].as_table())for(const auto* name:{"resonator","muffler"})if(auto* s=(*silencers)[name].as_table()) { const auto& cfg=std::string(name)=="resonator"?definition.exhaust.resonator:definition.exhaust.muffler; s->erase("volume_litres");s->insert_or_assign("diameter_mm",double(cfg.diameter())*1000); } } auto temporary=path;temporary+=".geometry.tmp"; std::ifstream original(path);std::ostringstream preserved;std::string line,section; const auto trim=[](std::string s){const auto a=s.find_first_not_of(" \t\r");return a==std::string::npos?std::string{}:s.substr(a,s.find_last_not_of(" \t\r")-a+1);}; while(std::getline(original,line)) { const auto text=trim(line);const auto key=trim(text.substr(0,text.find('='))); if(text.starts_with("[geometry")||text.starts_with("[[geometry"))break; if(text.starts_with("["))section=text; if(key=="schema_version"){preserved<<"schema_version = 2\n";continue;} if(section=="[engine]"&&(key=="plenum_volume_litres"||key=="collector_volume_litres"))continue; if(section=="[[pipes]]"&&key=="length_mm")continue; if(section=="[junctions]"&&key=="junction_volume_cc")continue; if(section=="[two_stroke]"&&key=="crankcase_clearance_cc")continue; if((section=="[silencers.resonator]"||section=="[silencers.muffler]")&&key=="volume_litres") { const auto& cfg=section=="[silencers.resonator]"?definition.exhaust.resonator:definition.exhaust.muffler; preserved<<"diameter_mm = "<0) table.insert_or_assign("simulation",toml::table{{"cells_per_meter",double(tuning.cells_per_meter)}}); auto write_cam=[&](const char* name,const FourStrokeCylinder::CamProfile& cam) { toml::array points; for(const auto& p:cam.points)points.push_back(toml::array{double(p.angle_degrees),double(p.lift_metres)*1000}); table["cam"][name].as_table()->insert_or_assign("points",std::move(points)); }; write_cam("intake",tuning.intake_cam);write_cam("exhaust",tuning.exhaust_cam); auto write_valve=[&](const char* name,const FourStrokeCylinder::ValveDynamics& v) { auto& t=*table["valves"][name].as_table(); t.insert_or_assign("mass_g",double(v.mass_kg)*1000); t.insert_or_assign("spring_rate_n_per_mm",double(v.spring_rate_n_per_m)/1000); t.insert_or_assign("damping_n_s_per_m",double(v.damping_n_s_per_m)); t.insert_or_assign("seat_force_n",double(v.seat_force_n)); t.insert_or_assign("maximum_lift_mm",double(v.maximum_lift_m)*1000); t.insert_or_assign("seat_restitution",double(v.seat_restitution)); t.insert_or_assign("cam_restitution",double(v.cam_restitution)); t.insert_or_assign("stop_restitution",double(v.stop_restitution)); }; write_valve("intake",tuning.intake_valve);write_valve("exhaust",tuning.exhaust_valve); auto& sound=*table["valve_sound"].as_table(); sound.insert_or_assign("mode_hz",double(tuning.valve_sound.mode_hz)); sound.insert_or_assign("effective_mass_g",double(tuning.valve_sound.effective_mass_kg)*1000); sound.insert_or_assign("radiation_area_cm2",double(tuning.valve_sound.radiation_area)*10000); sound.insert_or_assign("damping_ratio",double(tuning.valve_sound.damping_ratio)); auto& ecu=*table["ecu"].as_table();toml::array ignition; for(const auto& p:tuning.ecu.ignition_map)ignition.push_back(toml::array{double(p.rpm),double(p.degrees_btdc)}); ecu.insert_or_assign("ignition_map",std::move(ignition)); ecu.insert_or_assign("idle_base_opening",double(tuning.ecu.idle_base_opening)); ecu.insert_or_assign("idle_max_opening",double(tuning.ecu.idle_max_opening)); ecu.insert_or_assign("stoichiometric_afr",double(tuning.ecu.stoichiometric_afr)); ecu.insert_or_assign("power_afr",double(tuning.ecu.power_afr)); if(tuning.carbureted) { auto& fuel=*table["fuel"].as_table(); fuel.insert_or_assign("pilot_jet_diameter_mm",double(tuning.fuel.pilot_jet_diameter)*1000); fuel.insert_or_assign("main_jet_diameter_mm",double(tuning.fuel.main_jet_diameter)*1000); fuel.insert_or_assign("needle_jet_diameter_mm",double(tuning.fuel.needle_jet_diameter)*1000); } auto& pipes=*table["pipes"].as_array(); if(pipes.size()!=tuning.pipes.size())throw std::invalid_argument("Pipe groups changed since the engine was opened"); for(std::size_t i=0;i load_engine_directory(const std::filesystem::path& directory) { require(std::filesystem::is_directory(directory),directory.string(),"engine directory does not exist"); std::vector 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 engines;std::set 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 candidates{std::filesystem::current_path()/"engines"}; #ifdef _WIN32 std::wstring executable(32768,L'\0'); const auto size=GetModuleFileNameW(nullptr,executable.data(),static_cast(executable.size())); if(size>0 && size& 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