759 lines
48 KiB
C++
759 lines
48 KiB
C++
#include "FluidSim/ScenarioCatalog.hpp"
|
|
#include <toml.hpp>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#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)) {}
|
|
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<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,double scale=1) const {
|
|
auto value=n.value<double>();
|
|
if(!value || !std::isfinite(*value) || std::abs(*value*scale)>std::numeric_limits<float>::max())fail("array entries must be finite numbers");
|
|
return static_cast<float>(*value*scale);
|
|
}
|
|
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);
|
|
}
|
|
|
|
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;i<geometry.pipes.size();++i){toml::array points;for(auto p:geometry.pipes[i].points)points.push_back(xyz(p));pipes.push_back(toml::table{{"index",int(i)},{"points_mm",std::move(points)}});}
|
|
for(const auto& v:geometry.volumes)volumes.push_back(toml::table{{"id",v.id},{"center_mm",xyz(v.box.center)},{"size_mm",xyz(v.box.size)}});
|
|
for(const auto& c:geometry.cylinders)cylinders.push_back(toml::table{{"origin_mm",xyz(c.origin)},{"tilt_deg",double(c.tilt_degrees)}});
|
|
return toml::table{{"pipes",std::move(pipes)},{"volumes",std::move(volumes)},{"cylinders",std::move(cylinders)}};
|
|
}
|
|
|
|
// 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_valve_count",c.exhaust_valve_count);
|
|
r.field("exhaust_valve_diameter_mm",c.exhaust_valve_diameter,0.001);
|
|
r.field("fuel_lhv_j_per_kg",c.fuel_lhv);
|
|
r.field("heat_transfer",c.heat_transfer);
|
|
r.field("intake_valve_count",c.intake_valve_count);
|
|
r.field("intake_valve_diameter_mm",c.intake_valve_diameter,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_cam(Reader r,FourStrokeCylinder::CamProfile& cam) {
|
|
const auto* curve=r.array("points");
|
|
if(curve) for(const auto& node:*curve) {
|
|
const auto* point=node.as_array();
|
|
if(!point || point->size()!=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;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.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<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 && (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<<error;throw std::runtime_error(message.str());}
|
|
Reader root(table,path.string());
|
|
int version=0;root.field("schema_version",version);
|
|
if(version!=1&&version!=2)root.fail("schema_version must be 1 or 2");
|
|
if(version==2&&!table.contains("geometry"))root.fail("schema_version 2 requires geometry");
|
|
ScenarioExample e;
|
|
e.source_path=std::filesystem::absolute(path);
|
|
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 _");
|
|
auto simulation=root.section("simulation");
|
|
float density=0;
|
|
if(simulation.node("cells_per_meter")) {
|
|
simulation.field("cells_per_meter",density);
|
|
if(density<1 || density>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<float> 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<int>(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;i<geometry.pipes.size();++i) {
|
|
toml::array points;for(auto p:geometry.pipes[i].points)points.push_back(xyz(p));
|
|
pipes.push_back(toml::table{{"index",int(i)},{"points_mm",std::move(points)}});
|
|
}
|
|
for(const auto& v:geometry.volumes)volumes.push_back(toml::table{{"id",v.id},{"center_mm",xyz(v.box.center)},{"size_mm",xyz(v.box.size)}});
|
|
for(const auto& c:geometry.cylinders)cylinders.push_back(toml::table{{"origin_mm",xyz(c.origin)},{"tilt_deg",double(c.tilt_degrees)}});
|
|
table.insert_or_assign("geometry",toml::table{{"pipes",std::move(pipes)},{"volumes",std::move(volumes)},{"cylinders",std::move(cylinders)}});
|
|
table.insert_or_assign("schema_version",2);
|
|
if(definition.kind==ScenarioKind::TwoStroke) {
|
|
auto& two=*table["two_stroke"].as_table();two.erase("crankcase_clearance_cc");
|
|
two.insert_or_assign("reed_pipe_diameter_mm",double(definition.two.reed_pipe_diameter)*1000);
|
|
two.insert_or_assign("transfer_pipe_diameter_mm",double(definition.two.transfer_pipe_diameter)*1000);
|
|
} else {
|
|
auto& engine=*table["engine"].as_table();engine.erase("plenum_volume_litres");engine.erase("collector_volume_litres");
|
|
for(auto& pipe:*table["pipes"].as_array())pipe.as_table()->erase("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 = "<<std::setprecision(10)<<double(cfg.diameter())*1000<<'\n';continue;
|
|
}
|
|
preserved<<line<<'\n';
|
|
if(text=="[two_stroke]"&&!already_geometry)preserved<<"reed_pipe_diameter_mm = "<<definition.two.reed_pipe_diameter*1000<<"\ntransfer_pipe_diameter_mm = "<<definition.two.transfer_pipe_diameter*1000<<'\n';
|
|
}
|
|
{std::ofstream output(temporary);output.exceptions(std::ios::failbit|std::ios::badbit);output<<preserved.str()<<'\n'<<toml::table{{"geometry",*table["geometry"].as_table()}}<<'\n';}
|
|
load_engine_file(temporary);
|
|
std::filesystem::copy_file(temporary,path,std::filesystem::copy_options::overwrite_existing);
|
|
std::filesystem::remove(temporary);
|
|
}
|
|
|
|
std::filesystem::path save_tuned_engine(const ScenarioExample& source,const EngineConfig& requested,
|
|
const std::filesystem::path& directory) {
|
|
if(source.kind==ScenarioKind::TwoStroke)throw std::invalid_argument("Live tuning export requires a four-stroke engine");
|
|
const auto tuning=resolve_engine_geometry(requested,source.exhaust,source.junctions);
|
|
validate_engine_config(tuning,"export");
|
|
auto table=toml::parse_file((source.source_path.empty()?engine_directory()/(source.id+".toml"):source.source_path).string());
|
|
auto destination=directory/(source.id+"-tuned.toml");
|
|
for(int suffix=2;std::filesystem::exists(destination);++suffix)
|
|
destination=directory/(source.id+"-tuned-"+std::to_string(suffix)+".toml");
|
|
table.insert_or_assign("id",destination.stem().string());
|
|
table.insert_or_assign("name",source.title+" / tuned");
|
|
table.insert_or_assign("geometry",geometry_table(tuning.geometry));table.insert_or_assign("schema_version",2);
|
|
if(tuning.cells_per_meter>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<tuning.pipes.size();++i)if(!tuning.pipes[i].silenced) {
|
|
auto& pipe=*pipes[i].as_table();const auto& p=tuning.pipes[i];
|
|
pipe.insert_or_assign("diameter_mm",double(p.diameter)*1000);
|
|
if(tuning.geometry.empty())pipe.insert_or_assign("length_mm",double(p.length)*1000);else pipe.erase("length_mm");
|
|
pipe.insert_or_assign("cells",p.cells);
|
|
}
|
|
auto temporary=destination;temporary+=".tmp";
|
|
try {
|
|
{std::ofstream output(temporary);output.exceptions(std::ios::failbit|std::ios::badbit);output<<table<<'\n';}
|
|
load_engine_file(temporary); // Use the exact same parser and validation as startup.
|
|
std::filesystem::rename(temporary,destination);
|
|
} catch(...) {std::error_code ignored;std::filesystem::remove(temporary,ignored);throw;}
|
|
return destination;
|
|
}
|
|
|
|
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
|