Improve physical geometry, fluid solvers, audio, and profiling
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
@@ -22,6 +24,7 @@ class Reader {
|
||||
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+"'");
|
||||
@@ -67,10 +70,10 @@ public:
|
||||
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 {
|
||||
float number(const toml::node& n,double scale=1) 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);
|
||||
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));}
|
||||
@@ -84,6 +87,51 @@ 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"});
|
||||
@@ -94,18 +142,12 @@ void read_cylinder(Reader r,CylinderConfig& c) {
|
||||
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);
|
||||
@@ -114,6 +156,21 @@ void read_cylinder(Reader r,CylinderConfig& c) {
|
||||
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);
|
||||
@@ -183,6 +240,8 @@ void read_silencer(Reader r,SilencerConfig& c) {
|
||||
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;
|
||||
@@ -210,6 +269,8 @@ void read_two_stroke(Reader r,TwoStrokeConfig& c) {
|
||||
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);
|
||||
@@ -335,7 +396,7 @@ void validate_two(const TwoStrokeConfig& c,const std::string& path) {
|
||||
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 &&
|
||||
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,
|
||||
@@ -354,6 +415,7 @@ void validate_two(const TwoStrokeConfig& c,const std::string& 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;
|
||||
@@ -362,11 +424,17 @@ void validate_engine_config(const EngineConfig& c,const std::string& path) {
|
||||
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.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 &&
|
||||
@@ -392,9 +460,9 @@ void validate_engine_config(const EngineConfig& c,const std::string& path) {
|
||||
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,
|
||||
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+=p.cells*p.count;
|
||||
cells+=(c.cells_per_meter>0?mesh_cell_count(p.length,c.cells_per_meter):p.cells)*p.count;
|
||||
}
|
||||
count+=p.count;
|
||||
}
|
||||
@@ -419,22 +487,45 @@ ScenarioExample load_engine_file(const std::filesystem::path& path) {
|
||||
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");
|
||||
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);
|
||||
@@ -442,6 +533,21 @@ ScenarioExample load_engine_file(const std::filesystem::path& path) {
|
||||
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");
|
||||
@@ -453,6 +559,11 @@ ScenarioExample load_engine_file(const std::filesystem::path& path) {
|
||||
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()) {
|
||||
@@ -476,6 +587,127 @@ ScenarioExample load_engine_file(const std::filesystem::path& path) {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user