Add machine workshop, typed logic, mechanics and sensor-driven ECUs

This commit is contained in:
max
2026-11-02 07:27:13 +01:00
parent eb2b6366c2
commit abfcce0f46
99 changed files with 28835 additions and 145 deletions

382
src/machine_simulation.cpp Normal file
View File

@@ -0,0 +1,382 @@
#include "FluidSim/Machine.hpp"
#include "FluidSim/Fluid.hpp"
#include "FluidSim/PipeProfile.hpp"
#include "FluidSim/Solver.hpp"
#include "FluidSim/Ecu.hpp"
#include <algorithm>
#include <deque>
#include <functional>
#include <unordered_map>
#include <tuple>
namespace fluidsim::port {
using namespace machine_math;
namespace {
struct Actuator final:Component {
std::function<void(float)> advance;
std::vector<Port*> ports()override{return {};}
void prepare_step(float dt)override{advance(dt);}
void update_state(float)override{}
};
FourStrokeCylinder::CamProfile cam(float open,float close,float lift){
FourStrokeCylinder::CamProfile c;c.points.push_back({0,0});
if(open>0)c.points.push_back({open,0});
for(int i=1;i<32;++i){const float f=float(i)/32,s=std::sin(pi*f);c.points.push_back({std::lerp(open,close,f),lift*s*s});}
c.points.push_back({close,0});if(close<720)c.points.push_back({720,0});return c;
}
float bore_area(const MachinePart& p){return pi*.25f*p.cylinder.bore*p.cylinder.bore;}
float pump_volume(const MachinePart& p,float angle){
const float radius=p.cylinder.stroke*.5f,rod=p.cylinder.rod,s=std::sin(angle);
const float travel=radius*(1-std::cos(angle))+rod-std::sqrt(rod*rod-radius*radius*s*s);
return bore_area(p)*(p.cylinder.stroke/(p.cylinder.compression-1)+travel);
}
}
struct MachineSimulation::Impl {
struct Gate;
struct ShaftRuntime {std::uint32_t id{};std::unique_ptr<Crankshaft> timing;double previous_speed{};};
struct Part {
const MachinePart* definition{};
int pipe{-1};
std::unique_ptr<Volume0D> volume;
std::unique_ptr<FourStrokeCylinder> cylinder;
Ecu ecu;
std::unique_ptr<SensorEcu> sensor_ecu;
std::unique_ptr<FuelInjector> injector;
Part* injector_ecu{};
CrankIgnitionClock ignition_clock;
Part* controller{};
ShaftRuntime* shaft{};double gas_torque{};
std::unique_ptr<Carburetor> carburetor;
bool fuel_source{};
std::optional<bool> signal_enable,signal_starter;
std::optional<double> signal_target;
std::optional<float> signal_opening;
Gate* inlet{};
FourStrokeCylinder::CamProfile intake_cam,exhaust_cam;
};
struct Gate {Part* part;unsigned port;int pipe;bool left;float area{},maximum{};Port* component_port{};float throat_area{};};
struct Mouth {std::size_t index;std::unique_ptr<OutdoorAudio> audio;};
struct SensorTap {Part* host{};int cell{};};
struct Sensor {const MachineSensor* definition{};SensorTap target,reference;MachineSensorState state;std::array<MachinePortDescriptor,3> outputs;};
MachineDocument document;
std::vector<Pipe1D> pipes;
BoundarySystem boundaries{pipes};
Solver solver{pipes,boundaries};
Actuator actuator;
MechanicalSystem mechanics;
std::vector<std::unique_ptr<ShaftRuntime>> shafts;
std::unordered_map<std::uint32_t,ShaftRuntime*> shaft_by_id;
std::vector<std::unique_ptr<Part>> parts;
std::unordered_map<std::uint32_t,Part*> by_id;
std::deque<Gate> gates;
std::vector<Part*> cylinders,controllers,pumps,carburetors;
std::vector<Part*> motors,brakes;
std::vector<Gate*> cylinder_gates,throttle_gates,check_gates;
std::vector<Mouth> mouths;
std::vector<Sensor> sensors;
std::unordered_map<std::uint32_t,std::size_t> sensor_by_id;
std::unique_ptr<MachineLogicRuntime> logic;
float dt{},pedal{.2f};bool starter{};
double time{};
explicit Impl(const MachineDocument& d,int rate):document(d),dt(1.f/std::max(1,rate)){
validate_machine(document);if(document.parts.empty()&&document.logic.nodes.empty())throw std::invalid_argument("Place some parts or logic nodes before testing");
for(const auto& p:document.parts)if(p.kind==MachinePartKind::Shaft){
bool engine=false;for(const auto& c:document.parts)if(c.kind==MachinePartKind::Cylinder&&machine_shaft_for(document,c.id)==p.id)engine=true;
const auto& q=p.shaft;mechanics.add_shaft(p.id,{q.inertia,q.initial_speed,q.initial_angle,q.friction_constant,q.friction_viscous,q.friction_quadratic,engine});
auto runtime=std::make_unique<ShaftRuntime>();runtime->id=p.id;runtime->timing=std::make_unique<Crankshaft>();runtime->timing->angle=runtime->timing->previous_angle=std::fmod(q.initial_angle,4*pi);runtime->timing->omega=q.initial_speed;runtime->previous_speed=q.initial_speed;shaft_by_id[p.id]=runtime.get();shafts.push_back(std::move(runtime));
}
for(const auto& p:document.parts){const auto shaft=machine_shaft_for(document,p.id);
if((machine_rotational_part(p.kind)&&p.kind!=MachinePartKind::Shaft)||p.kind==MachinePartKind::Cylinder||p.kind==MachinePartKind::PistonPump){if(!shaft)throw std::invalid_argument(p.name+": connect its mechanical port to a shaft");}
if(p.kind==MachinePartKind::Flywheel)mechanics.add_flywheel(p.id,shaft,p.shaft.inertia);
else if(p.kind==MachinePartKind::DriveMotor)mechanics.add_drive(p.id,shaft,{p.drive.speed_control?DriveMode::Speed:DriveMode::Torque,p.drive.speed_control?p.drive.target_speed:p.drive.torque,p.drive.enabled});
else if(p.kind==MachinePartKind::Brake)mechanics.add_brake(p.id,shaft,{p.brake.constant,p.brake.viscous,p.brake.quadratic,p.brake.enabled});
}
actuator.advance=[this](float){update_check_valves();};solver.minimum_substeps=1;
parts.reserve(d.parts.size());
for(auto& p:document.parts){
auto runtime=std::make_unique<Part>();auto& r=*runtime;r.definition=&p;const auto shaft_id=machine_shaft_for(document,p.id);if(shaft_id)r.shaft=shaft_by_id.at(shaft_id);
if(p.kind==MachinePartKind::Pipe){
// Integrate the per-point profile over uniform arclength cells.
// Edited route knots do not force tiny fluid substeps.
r.pipe=int(pipes.size());const auto profile=machine_pipe_sections(p);
pipes.push_back(make_uniform_profiled_pipe(profile,1/d.cells_per_meter));auto& pipe=pipes.back();pipe.solver=d.solver;
for(auto& cell:pipe.cells){cell.rho=p.pressure/(gas_constant*p.temperature);cell.energy=p.pressure/pipe_gm1;}
}else if(p.kind==MachinePartKind::Cylinder){
r.cylinder=std::make_unique<FourStrokeCylinder>();auto& c=*r.cylinder;static_cast<CylinderConfig&>(c)=p.cylinder;
c.phase=p.phase_degrees*pi/180;r.intake_cam=p.intake_profile.points.empty()?cam(p.intake_open,p.intake_close,p.maximum_lift):p.intake_profile;r.exhaust_cam=p.exhaust_profile.points.empty()?cam(p.exhaust_open,p.exhaust_close,p.maximum_lift):p.exhaust_profile;
c.set_valve_profiles(r.intake_cam,p.valve,r.exhaust_cam,p.exhaust_valve);
r.ecu.ignition_map={{0,p.cylinder.spark_advance},{20000,p.cylinder.spark_advance}};r.ecu.stoichiometric_afr=r.ecu.power_afr=p.cylinder.afr;
r.ecu.rev_limit_enabled=false;r.ecu.overrun_fuel_cut_enabled=false;r.ecu.overrun_ignition_cut_enabled=false;c.ecu=&r.ecu;
c.transported_fuel=true; // No implicit injector. Only an assigned injection ECU can enable dosing.
c.initialize(*r.shaft->timing);c.volume=c.compute_volume(r.shaft->timing->angle+c.phase);c.air_mass=p.pressure*c.volume/(gas_constant*p.temperature);c.energy=p.pressure*c.volume/(gamma-1);c.update_state(0);solver.add_component(c);
}else if(p.kind==MachinePartKind::Ecu){static_cast<EcuConfig&>(r.ecu)=p.ecu;if(p.sensor_ecu.enabled)r.sensor_ecu=std::make_unique<SensorEcu>(machine_sensor_ecu_config(p));}
else if(!machine_rotational_part(p.kind)&&p.kind!=MachinePartKind::Reservoir&&p.kind!=MachinePartKind::Cap&&!(p.kind==MachinePartKind::Junction&&p.junction_method)){
const float v=p.kind==MachinePartKind::PistonPump?pump_volume(p,r.shaft->timing->angle+p.phase_degrees*pi/180):machine_part_volume(p);
r.volume=std::make_unique<Volume0D>(v,p.pressure,p.temperature);solver.add_component(*r.volume);
if(p.kind==MachinePartKind::Carburetor)r.carburetor=std::make_unique<Carburetor>(p.diameter,p.carburetor);
}
if(p.injector){const auto& c=*p.injector;FuelInjectorConfig config;config.supply=c.finite_supply?FuelInjectorConfig::Supply::FiniteRail:FuelInjectorConfig::Supply::IdealRail;config.maximum_mass_flow=c.maximum_flow;config.initial_fuel_mass=c.initial_mass;config.fuel_temperature=c.temperature;r.injector=std::make_unique<FuelInjector>(config);}
if(r.cylinder)cylinders.push_back(&r);
if(r.carburetor)carburetors.push_back(&r);
if(p.kind==MachinePartKind::Ecu)controllers.push_back(&r);
if(p.kind==MachinePartKind::PistonPump)pumps.push_back(&r);
if(p.kind==MachinePartKind::DriveMotor)motors.push_back(&r);
if(p.kind==MachinePartKind::Brake)brakes.push_back(&r);
by_id[p.id]=&r;parts.push_back(std::move(runtime));
}
if(pipes.empty()&&shafts.empty()&&document.logic.nodes.empty())throw std::invalid_argument("Add a pipe, mechanical shaft or logic graph to test");
for(auto& link:document.links)if(machine_link_domain(document,link)==PortDomain::EcuAssignment){auto target=by_id.at(link.b.part),control=by_id.at(link.a.part);target->controller=control;
if(target->cylinder){if(control->shaft&&control->shaft!=target->shaft)throw std::invalid_argument(control->definition->name+": controlled cylinders must share a shaft");control->shaft=target->shaft;target->cylinder->ecu=&control->ecu;target->cylinder->transported_fuel=bool(control->sensor_ecu)||!control->definition->injection_enabled;target->fuel_source=!control->sensor_ecu&&control->definition->injection_enabled;if(control->sensor_ecu)target->cylinder->ignition_command=FourStrokeCylinder::IgnitionCommand{};}
}
// Legacy carb metering can use the unique shaft reached through its
// intake network; a sensor-driven ECU will replace this ideal reference.
const auto gas_reference=[&](Part* source){std::vector<std::uint32_t> visited{source->definition->id};ShaftRuntime* found=nullptr;for(std::size_t i=0;i<visited.size();++i)for(const auto& link:document.links)if(machine_link_domain(document,link)==PortDomain::Gas){const auto other=link.a.part==visited[i]?link.b:link.b.part==visited[i]?link.a:MachinePort{};if(!other.part||std::find(visited.begin(),visited.end(),other.part)!=visited.end())continue;auto target=by_id.at(other.part);if(target->cylinder){if(other.port==0){if(found&&found!=target->shaft)return static_cast<ShaftRuntime*>(nullptr);found=target->shaft;}continue;}visited.push_back(other.part);}return found;};
for(auto controller:controllers)if(!controller->shaft){for(const auto& link:document.links)if(link.kind==MachineConnectionKind::Control&&link.a.part==controller->definition->id){auto reference=gas_reference(by_id.at(link.b.part));if(reference){if(controller->shaft&&controller->shaft!=reference)throw std::invalid_argument("ECU targets refer to multiple shafts");controller->shaft=reference;}}}
for(auto& part:parts)if(part->injector){const auto found=by_id.find(part->definition->injector->ecu);if(found==by_id.end()||!found->second->sensor_ecu||found->second->definition->sensor_ecu.strategy==SensorEcuConfig::Strategy::Ignition)throw std::invalid_argument(part->definition->name+": bind its injector to a sensor-driven EFI ECU");part->injector_ecu=found->second;}
for(auto controller:controllers)if(controller->sensor_ecu){unsigned targets=0,doses=0;for(auto target:cylinders)if(target->controller==controller)++targets;for(const auto& part:parts)if(part->injector_ecu==controller)doses+=part->definition->injector->cylinders;if(!targets)throw std::invalid_argument(controller->definition->name+": assign controlled cylinders");if(controller->definition->sensor_ecu.strategy!=SensorEcuConfig::Strategy::Ignition&&doses!=targets)throw std::invalid_argument(controller->definition->name+": injector cylinder doses must match assigned cylinders");}
for(auto carb:carburetors)carb->shaft=carb->controller?carb->controller->shaft:gas_reference(carb);
// This reachability flag is diagnostic only; carburetor fuel still has to travel through the gas solver.
for(auto r:cylinders)r->fuel_source=machine_has_fuel_source(document,r->definition->id);
std::vector<std::array<bool,2>> claimed(pipes.size(),{false,false});
std::unordered_map<std::uint32_t,std::vector<BoundarySystem::JunctionFace>> junctions;
const auto face=[&](MachinePort e){auto& r=*by_id.at(e.part);const auto path=machine_pipe_path(*r.definition);const auto axis=e.port?subtract(path[path.size()-2],path.back()):subtract(path[1],path.front());const auto dir=unit(axis);return BoundarySystem::JunctionFace{r.pipe,e.port==0,{dir.x,dir.y,dir.z}};};
for(auto& link:document.links){
if(machine_link_domain(document,link)!=PortDomain::Gas)continue;
MachinePort a=link.a,b=link.b;if(by_id.at(a.part)->pipe<0)std::swap(a,b);
auto& pipe_part=*by_id.at(a.part);auto& other=*by_id.at(b.part);const auto& p=*other.definition;
const auto& pipe=pipes[pipe_part.pipe];const bool left=a.port==0;
const float area=left?pipe.cell_areas.front():pipe.cell_areas.back();claimed[pipe_part.pipe][a.port]=true;
if(other.pipe>=0){
claimed[other.pipe][b.port]=true;
auto ends={face(a),face(b)};
boundaries.add_junction(ends,link.kind==MachineConnectionKind::Legacy?BoundarySystem::JunctionMethod::LegacyRusanov:BoundarySystem::JunctionMethod::Characteristic,link.loss);
}else if(p.kind==MachinePartKind::Cap)boundaries.add_closed_end(pipe_part.pipe,left);
else if(p.kind==MachinePartKind::Reservoir){
boundaries.add_radiating_open_end(pipe_part.pipe,left,p.pressure,area,p.temperature);
OutdoorAudioConfig config;config.intake_position=machine_port_position(p,b.port);config.ground_y=-.02f;config.listener={1,.7f,1.5f};
auto audio=std::make_unique<OutdoorAudio>(rate,config);audio->set_scene(d.scene);audio->set_source_enabled(1,false);audio->set_source_enabled(2,false);audio->set_source_enabled(3,false);
mouths.push_back({mouths.size(),std::move(audio)});
}else if(p.kind==MachinePartKind::Junction&&p.junction_method)junctions[p.id].push_back(face(a));
else{
Port* component_port=other.cylinder?(b.port?&other.cylinder->exhaust:&other.cylinder->intake):&other.volume->create_port();
gates.push_back({&other,b.port,pipe_part.pipe,left,area,area,component_port});auto& gate=gates.back();
if(other.cylinder)cylinder_gates.push_back(&gate);
else if((p.kind==MachinePartKind::Throttle||p.kind==MachinePartKind::Carburetor)&&b.port==0){gate.throat_area=std::min(area,pi*.25f*p.diameter*p.diameter);throttle_gates.push_back(&gate);if(other.carburetor)other.inlet=&gate;}
else if(p.kind==MachinePartKind::CheckValve||p.kind==MachinePartKind::PistonPump){const float diameter=p.kind==MachinePartKind::PistonPump?(b.port?p.cylinder.exhaust_valve_diameter:p.cylinder.intake_valve_diameter):p.diameter;
const int count=p.kind==MachinePartKind::PistonPump?(b.port?p.cylinder.exhaust_valve_count:p.cylinder.intake_valve_count):1;gate.throat_area=std::min(area,pi*.25f*diameter*diameter*count);check_gates.push_back(&gate);}
const float cd=link.discharge*((p.kind==MachinePartKind::Throttle||p.kind==MachinePartKind::Carburetor||p.kind==MachinePartKind::CheckValve||other.cylinder||p.kind==MachinePartKind::PistonPump)?p.discharge:1.f);
if(link.kind==MachineConnectionKind::Inertance)boundaries.add_orifice_with_inertance(*component_port,pipe_part.pipe,left,gate.area,cd,link.inertance_length,link.loss);
else boundaries.add_orifice(*component_port,pipe_part.pipe,left,gate.area,cd);
}
}
for(auto& [id,faces]:junctions){auto& p=*by_id.at(id)->definition;
if(faces.size()==1)boundaries.add_closed_end(faces.front().pipe,faces.front().left);
else boundaries.add_junction(faces,p.junction_method==2?BoundarySystem::JunctionMethod::LegacyRusanov:BoundarySystem::JunctionMethod::Characteristic,p.turning_loss);
}
for(std::size_t i=0;i<pipes.size();++i)for(unsigned end=0;end<2;++end)if(!claimed[i][end])boundaries.add_closed_end(int(i),end==0);
boundaries.validate_topology();
if(!check_gates.empty())solver.add_component(actuator);
const auto sensor_tap=[&](const MachineSensorTap& tap){
SensorTap result;if(const auto found=by_id.find(tap.part);found!=by_id.end())result.host=found->second;
if(result.host&&result.host->pipe>=0){const auto& pipe=pipes[result.host->pipe];double length=0;for(float dx:pipe.cell_lengths)length+=dx;const auto position=length*tap.position;double end=0;
result.cell=int(pipe.cells.size())-1;for(std::size_t i=0;i<pipe.cells.size();++i){end+=pipe.cell_lengths[i];if(position<end){result.cell=int(i);break;}}
}return result;
};
sensors.reserve(document.sensors.size());for(const auto& s:document.sensors){Sensor sensor;sensor.definition=&s;sensor.target=sensor_tap(s.target);sensor.reference=sensor_tap(s.reference);sensor.state.id=s.id;sensor.state.count=machine_sensor_output_count(s.kind);sensor.state.status=machine_sensor_binding(document,s);for(unsigned i=0;i<sensor.state.count;++i)sensor.outputs[i]=machine_sensor_output(s,i);sensors.push_back(std::move(sensor));}
sample_sensors();
for(std::size_t i=0;i<sensors.size();++i)sensor_by_id.emplace(sensors[i].state.id,i);
logic=std::make_unique<MachineLogicRuntime>(document,[this](std::uint32_t id){const auto found=sensor_by_id.find(id);return found==sensor_by_id.end()?MachineSensorState{}:sensors[found->second].state;});
for(const auto controller:controllers)if(controller->sensor_ecu&&!std::any_of(logic->ecu_inputs().begin(),logic->ecu_inputs().end(),[&](const auto& input){return input.part==controller->definition->id;}))throw std::invalid_argument(controller->definition->name+": add its ECU connector in Logic");
latch_logic(false);sample_sensors();
}
float throttle_opening(const Part& part)const{const auto& p=*part.definition;return part.signal_opening?*part.signal_opening:part.controller?part.controller->ecu.throttle_opening():p.pedal_control?pedal:p.opening;}
void latch_logic(bool evaluate=true){
if(evaluate)logic->evaluate(time,dt);
for(const auto& command:logic->commands()){auto& target=*by_id.at(command.part);const bool valid=command.value.validity==SignalValidity::Valid;
switch(command.command){
case LogicCommand::MotorEnable:case LogicCommand::BrakeEnable:target.signal_enable=valid&&std::get<bool>(command.value.value);break;
case LogicCommand::MotorSpeed:case LogicCommand::MotorTorque:target.signal_target=valid?std::get<double>(command.value.value):0.;break;
case LogicCommand::ThrottleOpening:target.signal_opening=valid?float(std::get<double>(command.value.value)):0.f;break;
case LogicCommand::Starter:target.signal_starter=valid&&std::get<bool>(command.value.value);break;
}
}
for(auto motor:motors)if(motor->signal_enable||motor->signal_target){const auto& p=*motor->definition;mechanics.set_drive(p.id,{p.drive.speed_control?DriveMode::Speed:DriveMode::Torque,motor->signal_target.value_or(p.drive.speed_control?p.drive.target_speed:p.drive.torque),motor->signal_enable.value_or(p.drive.enabled)});}
for(auto brake:brakes)if(brake->signal_enable){const auto& p=*brake->definition;mechanics.set_brake(p.id,{p.brake.constant,p.brake.viscous,p.brake.quadratic,*brake->signal_enable});}
if(evaluate)for(const auto& input:logic->ecu_inputs()){
auto& controller=*by_id.at(input.part);auto speed=input.speed;const auto measured=ecu_crank_sample(speed,input.angle,time);if(!measured||!controller.shaft||measured->shaft!=controller.shaft->id)speed.validity=SignalValidity::Fault;const auto& state=controller.sensor_ecu->sample(SensorEcuMeasurements{speed,input.angle,input.tps,input.map,input.iat},time);
for(auto target:cylinders)if(target->controller==&controller){
const auto crank=ecu_crank_sample(input.speed,input.angle,time);
const bool bound=crank&&crank->shaft==target->shaft->id;
CrankIgnitionClock::Result event;
if(bound&&!state.sensor_fault)event=target->ignition_clock.sample(input.speed,input.angle,time,state.spark_advance,target->definition->ignition_tdc_degrees);else target->ignition_clock.reset();
const bool cut=!bound||state.sensor_fault||state.rev_limited||!event.valid;
target->cylinder->ignition_command=FourStrokeCylinder::IgnitionCommand{event.valid&&event.sparks>0,cut};
}
}
}
void deliver_injectors(){for(auto& target:parts)if(target->injector){const auto& state=target->injector_ecu->sensor_ecu->state();auto command=SignalValue::number(state.fuel_mass_flow*target->definition->injector->cylinders,time);if(state.sensor_fault)command.validity=SignalValidity::Fault;target->injector->deliver(*target->volume,command,time,dt);}}
double gas_sensor_value(const SensorTap& tap,MachineSensorKind kind)const{
const auto& r=*tap.host;
if(r.pipe>=0){const auto& pipe=pipes[r.pipe];const auto cell=pipe.state(tap.cell);if(kind==MachineSensorKind::MassFlow)return double(cell.rho)*cell.velocity*pipe.cell_areas[tap.cell];if(kind==MachineSensorKind::GasTemperature)return cell.pressure/(cell.rho*gas_constant);return cell.pressure;}
if(r.cylinder)return kind==MachineSensorKind::GasTemperature?r.cylinder->temperature():r.cylinder->pressure;
if(r.volume)return kind==MachineSensorKind::GasTemperature?r.volume->temperature():r.volume->pressure();
return kind==MachineSensorKind::GasTemperature?r.definition->temperature:r.definition->pressure;
}
void sample_sensors(){
for(auto& sensor:sensors){const auto& definition=*sensor.definition;auto& state=sensor.state;
// Bindings cannot change during Test; a numeric fault may recover.
const bool bound=state.status==MachineSensorStatus::Valid||state.status==MachineSensorStatus::Fault;
state.timing_shaft=0;state.unwrapped_angle=0;
for(unsigned i=0;i<state.count;++i){state.outputs[i]=definition.kind==MachineSensorKind::Crank&&i==2?SignalValue::boolean(false,time):SignalValue::number(0,time);state.outputs[i].validity=SignalValidity::Missing;}
if(!bound)continue;
state.status=MachineSensorStatus::Valid;
const auto& host=*sensor.target.host;
if(definition.kind==MachineSensorKind::Crank){const auto& shaft=mechanics.shaft(host.definition->id);state.timing_shaft=shaft.id;state.unwrapped_angle=shaft.angle+definition.angle_offset;double angle=std::fmod(state.unwrapped_angle,2*double(pi));if(angle<0)angle+=2*double(pi);
state.outputs[0]=SignalValue::number(shaft.speed,time);state.outputs[1]=SignalValue::number(angle,time);state.outputs[2]=SignalValue::boolean(true,time);
const SignalTiming timing{definition.id,shaft.id,time,state.unwrapped_angle,shaft.speed};
for(auto& output:state.outputs)output.timing=timing;
}else if(definition.kind==MachineSensorKind::ThrottlePosition)state.outputs[0]=SignalValue::number(throttle_opening(host),time);
else{auto value=gas_sensor_value(sensor.target,definition.kind);if(definition.kind==MachineSensorKind::DifferentialPressure)value-=gas_sensor_value(sensor.reference,MachineSensorKind::Pressure);state.outputs[0]=SignalValue::number(value,time);}
for(unsigned i=0;i<state.count;++i)if(!signal_value_valid_for(state.outputs[i],sensor.outputs[i]))state.status=MachineSensorStatus::Fault;
if(state.status==MachineSensorStatus::Fault){state.timing_shaft=0;for(unsigned i=0;i<state.count;++i)state.outputs[i].validity=SignalValidity::Fault;}
}
}
// Shaft, ECU, cams and combustion advance at the audio sample rate, as in
// FourStrokeScenario. Gas transport and pressure-operated checks retain CFL substeps.
void advance_controls(float sub_dt){
const auto primary=machine_primary_shaft(document);
for(const auto& shaft:shafts){const auto& part=*by_id.at(shaft->id);const bool enabled=part.signal_starter.value_or(starter&&shaft->id==primary);if(enabled){const auto& state=mechanics.shaft(shaft->id);if(state.speed>=0&&state.speed<200)mechanics.add_torque(shaft->id,part.definition->shaft.starter_torque);}}
mechanics.step(sub_dt);
for(auto& shaft:shafts){auto& crank=*shaft->timing;shaft->previous_speed=crank.omega;const auto& state=mechanics.shaft(shaft->id);crank.previous_angle=crank.angle;crank.angle=std::fmod(state.angle,4*pi);crank.omega=float(state.speed);crank.inertia=float(state.inertia);const float output=float(state.external_torque+state.friction_torque);crank.record_output(output,output*crank.omega);}
const auto rpm_of=[](const Part* p){return p->shaft?float(p->shaft->timing->omega*60/(2*pi)):0.f;};
for(auto r:controllers)if(!r->sensor_ecu)r->ecu.step(sub_dt,pedal,rpm_of(r));
for(auto r:cylinders){if(!r->controller)r->ecu.step(sub_dt,pedal,rpm_of(r));r->cylinder->pre_step(*r->shaft->timing,sub_dt,pedal);r->gas_torque=r->shaft->timing->take_torque();mechanics.add_torque(r->shaft->id,r->gas_torque);}
for(auto r:carburetors){const auto& p=*r->definition;float flow=0,upstream=p.pressure,density=ambient_density;if(auto g=r->inlet){const auto& pipe=pipes[g->pipe];const auto state=pipe.state(g->left?0:int(pipe.cells.size()-1));upstream=state.pressure;density=state.rho;flow=std::max(0.f,g->component_port->mass_flow);}
const float opening=throttle_opening(*r);
r->carburetor->step(sub_dt,pedal,opening,rpm_of(r),flow,density,upstream,r->volume->pressure());r->volume->add_fuel_mass(r->carburetor->fuel_flow()*sub_dt);
}
for(auto r:pumps){const auto& p=*r->definition;
const float theta=r->shaft->timing->angle+p.phase_degrees*pi/180,radius=p.cylinder.stroke*.5f,sine=std::sin(theta),cosine=std::cos(theta),rod=p.cylinder.rod;
const float travel_derivative=radius*sine+radius*radius*sine*cosine/std::sqrt(std::max(1e-12f,rod*rod-radius*radius*sine*sine));
r->gas_torque=(r->volume->pressure()-101325)*bore_area(p)*travel_derivative;mechanics.add_torque(r->shaft->id,r->gas_torque);
r->volume->volume_change_rate=(pump_volume(p,theta)-r->volume->volume())/sub_dt;
}
for(auto g:cylinder_gates)g->area=std::min(g->maximum,g->part->cylinder->valve_area(0,g->port==0));
for(auto g:throttle_gates){const float opening=throttle_opening(*g->part);g->area=g->throat_area*(1-std::cos(opening*pi*.5f));}
}
void update_check_valves(){
for(auto g:check_gates){const auto& p=*g->part->definition;const auto& pipe=pipes[g->pipe];const auto state=pipe.state(g->left?0:int(pipe.cells.size()-1));
const float internal=g->part->volume->pressure(),delta=g->port==0?state.pressure-internal:internal-state.pressure;
g->area=delta>p.cracking_pressure?g->throat_area:0;
}
}
AudioFrame process(){latch_logic();advance_controls(dt);deliver_injectors();solver.step(dt);time+=dt;sample_sensors();AudioFrame result;
for(auto& mouth:mouths){const auto s=mouth.audio->process(ambient_density*boundaries.open_end_volume_flow(mouth.index),0);result.left+=s.left;result.right+=s.right;}
return result;
}
double rotational_reading(std::uint32_t part,MachineQuantity quantity)const{
if(!part)part=machine_primary_shaft(document);
if(!part)return std::numeric_limits<double>::quiet_NaN();
const auto& r=*by_id.at(part);if(!r.shaft)return std::numeric_limits<double>::quiet_NaN();const auto& shaft=mechanics.shaft(r.shaft->id);if(quantity==MachineQuantity::Rpm)return shaft.speed*60/(2*pi);
double torque=shaft.net_torque,power=torque*(shaft.speed+r.shaft->previous_speed)*.5;
if(r.definition->kind==MachinePartKind::DriveMotor){const auto& drive=mechanics.drive(part);torque=drive.torque;power=drive.power;}
else if(r.definition->kind==MachinePartKind::Brake){const auto& brake=mechanics.brake(part);torque=brake.torque;power=brake.power;}
else if(r.definition->kind==MachinePartKind::Flywheel){torque*=r.definition->shaft.inertia/shaft.inertia;power*=r.definition->shaft.inertia/shaft.inertia;}
else if(r.cylinder||r.definition->kind==MachinePartKind::PistonPump){torque=r.gas_torque;power=torque*(shaft.angle-shaft.previous_angle)/dt;}
return quantity==MachineQuantity::Torque?torque:power;
}
MachineMeasurement measure()const{
MachineMeasurement result;result.time=time;result.count=document.probes.size();result.values.fill(std::numeric_limits<float>::quiet_NaN());
for(std::size_t i=0;i<document.probes.size();++i){const auto& probe=document.probes[i];auto& value=result.values[i];auto& status=result.states[i];
if(probe.quantity>=MachineQuantity::Rpm){value=float(rotational_reading(probe.part,probe.quantity));if(probe.generated_output){const auto shaft=machine_shaft_for(document,probe.part?probe.part:machine_primary_shaft(document));if(shaft){const auto& timing=*shaft_by_id.at(shaft)->timing;value=probe.quantity==MachineQuantity::Torque?timing.average_torque:timing.average_power;}}continue;}
const auto& r=*by_id.at(probe.part);float pressure=r.definition->pressure,temperature=r.definition->temperature,flow=0,air=0,fuel=0;
if(r.pipe>=0){const auto& p=pipes[r.pipe];const int at=std::min(int(p.cells.size())-1,int(probe.position*p.cells.size()));const auto s=p.state(at);pressure=s.pressure;temperature=s.pressure/(s.rho*gas_constant);flow=s.rho*s.velocity*p.cell_areas[at];fuel=s.fuel_equivalent;air=1-fuel;}
else if(r.cylinder){pressure=r.cylinder->pressure;temperature=r.cylinder->temperature();flow=r.cylinder->intake.mass_flow+r.cylinder->exhaust.mass_flow;fuel=r.cylinder->fuel_equivalent_mass;air=r.cylinder->total_mass()-fuel;}
else if(r.volume){pressure=r.volume->pressure();temperature=r.volume->temperature();fuel=r.volume->fuel_equivalent_fraction();air=1-fuel;for(const auto& g:gates)if(g.part==&r)flow+=g.component_port->mass_flow;}
if(probe.quantity==MachineQuantity::Pressure)value=pressure;else if(probe.quantity==MachineQuantity::Temperature)value=temperature;else if(probe.quantity==MachineQuantity::MassFlow)value=flow;
else if(probe.quantity==MachineQuantity::AirFuelRatio||probe.quantity==MachineQuantity::Lambda){
if(fuel<=1e-12f)status=MachineReadingState::NoFuel;
else{const float afr=air/fuel;if(!std::isfinite(afr)||afr>machine_afr_reading_limit)status=MachineReadingState::LeanLimit;else value=probe.quantity==MachineQuantity::Lambda?afr/probe.reference_afr:afr;}
}else if(probe.quantity==MachineQuantity::ChargeAfr&&r.cylinder){const auto& cylinder=*r.cylinder;
if(!cylinder.transported_fuel&&cylinder.ecu&&cylinder.ecu->fuel_cut())status=MachineReadingState::FuelCut;
else if(!cylinder.charge_sampled)status=MachineReadingState::AwaitingCharge;
else if(!std::isfinite(cylinder.charge_afr))status=MachineReadingState::NoFuel;
else if(cylinder.charge_afr>machine_afr_reading_limit)status=MachineReadingState::LeanLimit;
else value=cylinder.charge_afr;
}
}return result;
}
void apply_tuning(const MachineDocument& proposed){
validate_machine(proposed);
if(proposed.driven!=document.driven||proposed.drive_rpm!=document.drive_rpm||proposed.shaft_inertia!=document.shaft_inertia||proposed.load_torque!=document.load_torque||proposed.friction_constant!=document.friction_constant||proposed.friction_viscous!=document.friction_viscous||proposed.friction_quadratic!=document.friction_quadratic||proposed.starter_torque!=document.starter_torque)throw std::invalid_argument("Shared-shaft settings are legacy: edit the explicit shaft, drive or brake part");
const auto frozen=[](bool match){if(!match)throw std::invalid_argument("This property requires returning to Build");};
frozen(proposed.parts.size()==document.parts.size()&&proposed.links.size()==document.links.size()&&proposed.cells_per_meter==document.cells_per_meter&&proposed.solver==document.solver&&proposed.scene==document.scene&&proposed.sensors==document.sensors&&logic_topology_equal(document.logic,proposed.logic));
for(std::size_t i=0;i<document.links.size();++i){const auto& a=document.links[i];const auto& b=proposed.links[i];frozen(a.a==b.a&&a.b==b.b&&a.kind==b.kind&&a.discharge==b.discharge&&a.inertance_length==b.inertance_length&&a.loss==b.loss);}
const auto cam_equal=[](const auto& a,const auto& b){return a.points.size()==b.points.size()&&std::equal(a.points.begin(),a.points.end(),b.points.begin(),[](const auto& x,const auto& y){return x.angle_degrees==y.angle_degrees&&x.lift_metres==y.lift_metres;});};
const auto valve=[](const auto& v){return std::tuple{v.mass_kg,v.spring_rate_n_per_m,v.damping_n_s_per_m,v.seat_force_n,v.maximum_lift_m,v.seat_restitution,v.cam_restitution,v.stop_restitution};};
const auto cylinder_geometry=[](const auto& c){return std::tuple{c.bore,c.stroke,c.rod,c.compression,c.intake_valve_diameter,c.exhaust_valve_diameter,c.intake_valve_count,c.exhaust_valve_count};};
for(std::size_t i=0;i<document.parts.size();++i){const auto& a=document.parts[i];const auto& b=proposed.parts[i];
frozen(a.id==b.id&&a.kind==b.kind&&a.position==b.position&&a.rotation==b.rotation&&a.size==b.size&&a.points==b.points&&machine_pipe_diameters(a)==machine_pipe_diameters(b)&&a.port_coordinates==b.port_coordinates&&a.port_keys==b.port_keys);
if(machine_rotational_part(a.kind))frozen(a.shaft.inertia==b.shaft.inertia&&a.shaft.initial_speed==b.shaft.initial_speed&&a.shaft.initial_angle==b.shaft.initial_angle);
frozen(a.diameter==b.diameter&&a.outlet_diameter==b.outlet_diameter&&a.pressure==b.pressure&&a.temperature==b.temperature&&a.discharge==b.discharge&&a.phase_degrees==b.phase_degrees&&a.junction_method==b.junction_method&&a.turning_loss==b.turning_loss);
frozen(a.sensor_ecu==b.sensor_ecu&&a.ignition_tdc_degrees==b.ignition_tdc_degrees);
frozen(a.injection_enabled==b.injection_enabled);
frozen(a.injector==b.injector);
frozen(cylinder_geometry(a.cylinder)==cylinder_geometry(b.cylinder)&&a.maximum_lift==b.maximum_lift&&a.intake_open==b.intake_open&&a.intake_close==b.intake_close&&a.exhaust_open==b.exhaust_open&&a.exhaust_close==b.exhaust_close&&valve(a.valve)==valve(b.valve)&&valve(a.exhaust_valve)==valve(b.exhaust_valve)&&cam_equal(a.intake_profile,b.intake_profile)&&cam_equal(a.exhaust_profile,b.exhaust_profile));
}
logic->apply_tuning(proposed);document.logic=proposed.logic;
document.primary_shaft=proposed.primary_shaft;document.probes=proposed.probes;
for(std::size_t i=0;i<document.parts.size();++i){auto& p=document.parts[i];const auto& q=proposed.parts[i];p.carburetor=q.carburetor;p.ecu=q.ecu;p.cylinder=q.cylinder;p.opening=q.opening;p.pedal_control=q.pedal_control;p.cracking_pressure=q.cracking_pressure;p.injection_enabled=q.injection_enabled;p.shaft=q.shaft;p.drive=q.drive;p.brake=q.brake;
if(p.kind==MachinePartKind::Shaft)mechanics.set_friction(p.id,p.shaft.friction_constant,p.shaft.friction_viscous,p.shaft.friction_quadratic);
if(p.kind==MachinePartKind::DriveMotor)mechanics.set_drive(p.id,{p.drive.speed_control?DriveMode::Speed:DriveMode::Torque,p.drive.speed_control?p.drive.target_speed:p.drive.torque,p.drive.enabled});
if(p.kind==MachinePartKind::Brake)mechanics.set_brake(p.id,{p.brake.constant,p.brake.viscous,p.brake.quadratic,p.brake.enabled});
auto& r=*by_id.at(p.id);if(r.carburetor)static_cast<CarburetorConfig&>(*r.carburetor)=p.carburetor;
if(p.kind==MachinePartKind::Ecu){static_cast<EcuConfig&>(r.ecu)=p.ecu;if(r.sensor_ecu)r.sensor_ecu->apply_calibration(machine_sensor_ecu_config(p));}
if(r.cylinder){static_cast<CylinderConfig&>(*r.cylinder)=p.cylinder;if(!r.controller){r.ecu.ignition_map={{0,p.cylinder.spark_advance},{20000,p.cylinder.spark_advance}};r.ecu.stoichiometric_afr=r.ecu.power_afr=p.cylinder.afr;}}
}
for(auto r:cylinders){r->cylinder->transported_fuel=!(r->controller&&r->controller->definition->injection_enabled);r->fuel_source=machine_has_fuel_source(document,r->definition->id);}
sample_sensors();
}
MachineSnapshot snapshot()const{
MachineSnapshot s;s.time=time;s.primary_shaft=machine_primary_shaft(document);s.rpm=float(rotational_reading(s.primary_shaft,MachineQuantity::Rpm));s.torque=float(rotational_reading(s.primary_shaft,MachineQuantity::Torque));s.power=float(rotational_reading(s.primary_shaft,MachineQuantity::Power));s.substeps=solver.substep_count();for(const auto& shaft:shafts)s.shafts.push_back(mechanics.shaft(shaft->id));
for(const auto& sensor:sensors)s.sensors.push_back(sensor.state);
s.logic=logic->snapshot();
for(auto& r:parts){const auto& p=*r->definition;MachinePartState v;v.id=p.id;v.pressure=p.pressure;v.temperature=p.temperature;v.volume=machine_part_volume(p);
v.signal_controlled=r->signal_enable.has_value()||r->signal_target.has_value()||r->signal_opening.has_value()||r->signal_starter.has_value();
if(p.kind==MachinePartKind::DriveMotor){v.actuator_enabled=r->signal_enable.value_or(p.drive.enabled);v.command_target=r->signal_target.value_or(p.drive.speed_control?p.drive.target_speed:p.drive.torque);}
else if(p.kind==MachinePartKind::Brake)v.actuator_enabled=r->signal_enable.value_or(p.brake.enabled);
if(r->shaft){const auto& shaft=mechanics.shaft(r->shaft->id);v.shaft=shaft.id;v.shaft_angle=shaft.angle;v.shaft_speed=shaft.speed;v.rotational_inertia=p.kind==MachinePartKind::Flywheel?p.shaft.inertia:shaft.inertia;v.kinetic_energy=.5*v.rotational_inertia*shaft.speed*shaft.speed;v.torque=rotational_reading(p.id,MachineQuantity::Torque);v.power=rotational_reading(p.id,MachineQuantity::Power);if(p.kind==MachinePartKind::DriveMotor)v.mechanical_work=mechanics.drive(p.id).work;else if(p.kind==MachinePartKind::Brake)v.mechanical_loss=mechanics.brake(p.id).loss;else if(p.kind==MachinePartKind::Shaft){v.mechanical_work=shaft.external_work+shaft.drive_work;v.mechanical_loss=shaft.friction_loss+shaft.brake_loss;}}
if(p.kind==MachinePartKind::Throttle||p.kind==MachinePartKind::Carburetor)v.opening=throttle_opening(*r);
if(p.kind==MachinePartKind::Ecu){v.fuel_cut=r->ecu.fuel_cut();v.ignition_cut=r->ecu.ignition_cut();v.idle_control=r->ecu.idle_active();v.afr=r->ecu.target_afr();v.opening=r->ecu.throttle_opening();}
if(r->sensor_ecu){const auto& state=r->sensor_ecu->state();v.ignition_cut=state.sensor_fault||state.rev_limited;v.fuel_cut=v.ignition_cut&&p.sensor_ecu.strategy!=SensorEcuConfig::Strategy::Ignition;v.sensor_fault=state.sensor_fault;v.rev_limited=state.rev_limited;v.spark_advance=float(state.spark_advance);v.idle_control=false;v.afr=0;v.opening=0;v.fuel_flow=float(state.fuel_mass_flow);}
if(r->carburetor){v.fuel_flow=r->carburetor->fuel_flow();v.afr=r->carburetor->indicated_afr();}
if(r->injector){const auto& state=r->injector->state();v.fuel_flow=float(state.mass_flow);v.fuel_cut=state.command_fault||state.empty;v.injector_fault=state.command_fault;v.injector_empty=state.empty;v.injector_remaining=state.remaining_mass;v.injector_delivered=state.delivered_mass;v.fuel_mass=r->volume->fuel_mass();}
if(r->pipe>=0){const auto& pipe=pipes[r->pipe];s.cells+=pipe.cells.size();s.positivity_repairs+=pipe.positivity_repairs;v.pressure=0;v.volume=0;const auto shortest=pipe.minimum_cell_length();if(s.minimum_cell_length==0||shortest<s.minimum_cell_length){s.minimum_cell_length=shortest;s.minimum_cell_part=p.id;}
for(std::size_t i=0;i<pipe.cells.size();++i){auto state=pipe.state(int(i));v.pipe_pressures.push_back(state.pressure);v.pressure+=state.pressure;v.flow+=state.rho*state.velocity*pipe.cell_areas[i];v.volume+=pipe.cell_areas[i]*pipe.cell_lengths[i];}
v.pressure/=pipe.cells.size();v.flow/=pipe.cells.size();
}else if(r->volume){v.pressure=r->volume->pressure();v.temperature=r->volume->temperature();v.volume=r->volume->volume();
if(p.kind==MachinePartKind::PistonPump)v.piston_fraction=(v.volume/bore_area(p)-p.cylinder.stroke/(p.cylinder.compression-1))/p.cylinder.stroke;
}else if(r->cylinder){const auto& c=*r->cylinder;v.pressure=c.pressure;v.temperature=c.temperature();v.volume=c.volume;v.piston_fraction=(c.volume-c.clearance_volume())/c.swept_volume();v.intake_lift=c.intake_lift();v.exhaust_lift=c.exhaust_lift();v.fuel_mass=c.fuel_mass;v.combustion=c.combustion_active;v.burn_fraction=c.burn_fraction;v.fuel_source=r->fuel_source;v.valve_float=c.valve_floating();v.fuel_cut=c.ecu->fuel_cut();v.ignition_cut=c.ecu->ignition_cut();}
if(r->cylinder&&r->cylinder->ignition_command)v.ignition_cut=r->cylinder->ignition_command->cut;
if(r->cylinder&&r->controller&&r->controller->sensor_ecu)v.fuel_cut=v.ignition_cut&&r->controller->definition->sensor_ecu.strategy!=SensorEcuConfig::Strategy::Ignition;
for(auto& g:gates)if(g.part==r.get()&&g.component_port){v.flow+=g.component_port->mass_flow;if(p.kind==MachinePartKind::CheckValve)v.opening=std::max(v.opening,g.area>0?1.f:0.f);}
s.parts.push_back(std::move(v));
}return s;
}
};
MachineSimulation::MachineSimulation(const MachineDocument& d,int rate):impl_(std::make_unique<Impl>(d,rate)){}
MachineSimulation::~MachineSimulation()=default;
AudioFrame MachineSimulation::process(){return impl_->process();}
void MachineSimulation::set_controls(float pedal,bool starter){impl_->pedal=std::clamp(pedal,0.f,1.f);impl_->starter=starter;}
void MachineSimulation::set_listener(AudioListener listener){for(auto& mouth:impl_->mouths)mouth.audio->set_listener(listener);}
MachineSnapshot MachineSimulation::snapshot()const{return impl_->snapshot();}
MachineMeasurement MachineSimulation::measure()const{return impl_->measure();}
MachineSensorState MachineSimulation::sense(std::uint32_t id)const{for(const auto& sensor:impl_->sensors)if(sensor.state.id==id)return sensor.state;throw std::out_of_range("Unknown machine sensor");}
void MachineSimulation::set_logic_control(std::uint32_t node,bool value){impl_->logic->set_manual(node,value);}
void MachineSimulation::set_logic_control(std::uint32_t node,double value){impl_->logic->set_manual(node,value);}
void MachineSimulation::release_logic_buttons(){impl_->logic->release_buttons();}
void MachineSimulation::apply_tuning(const MachineDocument& d){impl_->apply_tuning(d);}
}