#include "FluidSim/Machine.hpp" #include "FluidSim/Fluid.hpp" #include "FluidSim/PipeProfile.hpp" #include "FluidSim/Solver.hpp" #include "FluidSim/Ecu.hpp" #include #include #include #include #include namespace fluidsim::port { using namespace machine_math; namespace { struct Actuator final:Component { std::function advance; std::vector 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 timing;double previous_speed{};}; struct Part { const MachinePart* definition{}; int pipe{-1}; std::unique_ptr volume; std::unique_ptr cylinder; Ecu ecu; std::unique_ptr sensor_ecu; std::unique_ptr injector; Part* injector_ecu{}; CrankIgnitionClock ignition_clock; Part* controller{}; ShaftRuntime* shaft{};double gas_torque{}; std::unique_ptr carburetor; bool fuel_source{}; std::optional signal_enable,signal_starter; std::optional signal_target; std::optional 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 audio;}; struct SensorTap {Part* host{};int cell{};}; struct Sensor {const MachineSensor* definition{};SensorTap target,reference;MachineSensorState state;std::array outputs;}; MachineDocument document; std::vector pipes; BoundarySystem boundaries{pipes}; Solver solver{pipes,boundaries}; Actuator actuator; MechanicalSystem mechanics; std::vector> shafts; std::unordered_map shaft_by_id; std::vector> parts; std::unordered_map by_id; std::deque gates; std::vector cylinders,controllers,pumps,carburetors; std::vector motors,brakes; std::vector cylinder_gates,throttle_gates,check_gates; std::vector mouths; std::vector sensors; std::unordered_map sensor_by_id; std::unique_ptr 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();runtime->id=p.id;runtime->timing=std::make_unique();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();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();auto& c=*r.cylinder;static_cast(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(r.ecu)=p.ecu;if(p.sensor_ecu.enabled)r.sensor_ecu=std::make_unique(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(v,p.pressure,p.temperature);solver.add_component(*r.volume); if(p.kind==MachinePartKind::Carburetor)r.carburetor=std::make_unique(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(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 visited{source->definition->id};ShaftRuntime* found=nullptr;for(std::size_t i=0;icylinder){if(other.port==0){if(found&&found!=target->shaft)return static_cast(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> claimed(pipes.size(),{false,false}); std::unordered_map> 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(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;isecond; 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(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(command.value.value);break; case LogicCommand::MotorSpeed:case LogicCommand::MotorTorque:target.signal_target=valid?std::get(command.value.value):0.;break; case LogicCommand::ThrottleOpening:target.signal_opening=valid?float(std::get(command.value.value)):0.f;break; case LogicCommand::Starter:target.signal_starter=valid&&std::get(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;iid);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;iid);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::quiet_NaN(); const auto& r=*by_id.at(part);if(!r.shaft)return std::numeric_limits::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::quiet_NaN()); for(std::size_t i=0;i=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;iapply_tuning(proposed);document.logic=proposed.logic; document.primary_shaft=proposed.primary_shaft;document.probes=proposed.probes; for(std::size_t i=0;i(*r.carburetor)=p.carburetor; if(p.kind==MachinePartKind::Ecu){static_cast(r.ecu)=p.ecu;if(r.sensor_ecu)r.sensor_ecu->apply_calibration(machine_sensor_ecu_config(p));} if(r.cylinder){static_cast(*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||shortestvolume){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(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);} }