Add machine workshop, typed logic, mechanics and sensor-driven ECUs
This commit is contained in:
55
tests/speed_density_tests.cpp
Normal file
55
tests/speed_density_tests.cpp
Normal file
@@ -0,0 +1,55 @@
|
||||
#include "FluidSim/Machine.hpp"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
using namespace fluidsim::port;
|
||||
namespace {
|
||||
void check(bool condition,const char* why){if(!condition)throw std::runtime_error(why);}
|
||||
bool close(double a,double b){return std::abs(a-b)<=1e-8*std::max({1e-12,std::abs(a),std::abs(b)});}
|
||||
template<class F>void rejects(F action,const char* why){bool rejected=false;try{action();}catch(const std::exception&){rejected=true;}check(rejected,why);}
|
||||
SensorEcuMeasurements measurements(double pressure=100000,double temperature=300,double rpm=3000){
|
||||
SensorEcuMeasurements m;m.speed=SignalValue::number(rpm*2*double(pi)/60);m.angle=SignalValue::number(.125);m.speed.timing=m.angle.timing=SignalTiming{1,1,0,.125,std::get<double>(m.speed.value)};m.map=SignalValue::number(pressure);m.iat=SignalValue::number(temperature);return m;
|
||||
}
|
||||
SensorEcuState estimate(SensorEcuConfig config,SensorEcuMeasurements m){SensorEcu ecu(std::move(config));return ecu.sample(m,0);}
|
||||
}
|
||||
int main(int argc,char** argv)try{
|
||||
if(argc==3&&std::string(argv[1])=="--write-example"){save_machine(machine_speed_density_example(),argv[2]);return 0;}
|
||||
for(int i=0;i<sensor_ecu_strategy_count;++i){const auto strategy=SensorEcuConfig::Strategy(i);check(sensor_ecu_strategy_from_key(sensor_ecu_strategy_key(strategy))==strategy,"ECU strategy key failed round-trip");}
|
||||
rejects([]{sensor_ecu_strategy_from_key("unknown_future_strategy");},"Unknown ECU strategy silently fell back");
|
||||
SensorEcuConfig config;config.strategy=SensorEcuConfig::Strategy::SpeedDensity;config.displacement=.0005;config.target_afr=14.7;config.volumetric_efficiency={1,1,1,1};
|
||||
const auto full=estimate(config,measurements());const double air_mass=100000*.0005/(287*300.);
|
||||
check(!full.sensor_fault&&close(full.estimated_air_per_cycle,air_mass)&&close(full.fuel_per_cycle,air_mass/14.7),"Speed-density disagrees with the dry ideal-gas charge at 100 kPa/300 K/500 cc");
|
||||
check(close(full.fuel_mass_flow,air_mass/14.7*25),"Speed-density four-stroke mass-per-cycle conversion failed");
|
||||
const auto half=estimate(config,measurements(50000)),hot=estimate(config,measurements(100000,600));
|
||||
check(close(half.fuel_mass_flow,full.fuel_mass_flow*.5)&&close(hot.fuel_mass_flow,full.fuel_mass_flow*.5),"Fuel estimate did not respond to wired MAP/IAT");
|
||||
check(close(estimate(config,measurements(1e308,1e308)).fuel_mass_flow,full.fuel_mass_flow*300/100000),"Finite high P/T ratio overflowed into zero fueling");
|
||||
auto large=config;large.displacement*=2;check(close(estimate(large,measurements()).fuel_per_cycle,full.fuel_per_cycle*2),"Displacement calibration was ignored");
|
||||
auto table=config;table.volumetric_efficiency={.5,.7,.9,1.1};const auto interpolated=estimate(table,measurements(60000));check(close(interpolated.volumetric_efficiency,.8),"RPM/MAP VE table did not interpolate both axes");
|
||||
const auto stopped=estimate(config,measurements(100000,300,0));check(!stopped.sensor_fault&&stopped.fuel_mass_flow==0,"Stopped crank was a fault or delivered fuel");
|
||||
for(int failure=0;failure<7;++failure){auto m=measurements();switch(failure){case 0:m.map={};break;case 1:m.iat={};break;case 2:m.map=SignalValue::number(0);break;case 3:m.iat=SignalValue::number(0);break;case 4:m.map.timestamp=-1;break;case 5:m.iat=SignalValue::boolean(true);break;case 6:m.iat.validity=SignalValidity::Fault;break;}const auto state=estimate(config,m);check(state.sensor_fault&&!state.spark_events&&state.fuel_mass_flow==0,"Invalid required MAP/IAT retained ECU outputs");}
|
||||
auto bad=config;bad.ve_pressure={100000,20000};rejects([&]{SensorEcu invalid(bad);},"Reversed VE pressure axis accepted");bad=config;bad.volumetric_efficiency.pop_back();rejects([&]{SensorEcu invalid(bad);},"Mismatched VE dimensions accepted");bad=config;bad.displacement=0;rejects([&]{SensorEcu invalid(bad);},"Zero displacement accepted");
|
||||
LogicNode schema;schema.kind=LogicNodeKind::Ecu;schema.ecu_strategy=SensorEcuConfig::Strategy::SpeedDensity;const auto ports=logic_node_ports(schema);check(ports.size()==4&&ports[2].key=="map"&&ports[2].quantity==SignalQuantity::Pressure&&ports[2].pressure_reference==PressureReference::Absolute&&ports[3].key=="iat"&&ports[3].quantity==SignalQuantity::Temperature,"Speed-density connector schema lost MAP/IAT quantities");
|
||||
for(const auto& port:ports)check(port.required&&port.direction==PortDirection::Input&&port.domain==PortDomain::Number,"Speed-density required inputs are not typed Number rings");
|
||||
auto gauge=ports[2];gauge.direction=PortDirection::Output;gauge.pressure_reference=PressureReference::Gauge;check(!port_compatibility(gauge,ports[2]).allowed,"MAP accepted gauge pressure without a conversion");
|
||||
|
||||
auto doc=machine_speed_density_example();std::uint32_t ecu=0,host=0,cylinder=0,connector=0,pressure=0,temperature=0;
|
||||
for(const auto& p:doc.parts){if(p.kind==MachinePartKind::Ecu)ecu=p.id;if(p.injector)host=p.id;if(p.kind==MachinePartKind::Cylinder)cylinder=p.id;}
|
||||
for(const auto& n:doc.logic.nodes){if(n.kind==LogicNodeKind::Ecu)connector=n.id;if(n.kind==LogicNodeKind::Sensor&&n.sensor_kind==MachineSensorKind::Pressure)pressure=n.id;if(n.kind==LogicNodeKind::Sensor&&n.sensor_kind==MachineSensorKind::GasTemperature)temperature=n.id;}
|
||||
check(doc.sensors.size()==3&&doc.logic.nodes.size()==4,"Basic speed-density example demanded extra sensors");
|
||||
MachineSimulation sim(doc,22050);bool fuel=false,fire=false;for(int i=0;i<22050;++i){sim.process();if(i%16==0)for(const auto& p:sim.snapshot().parts)if(p.id==cylinder){fuel|=p.fuel_mass>0;fire|=p.combustion;}}
|
||||
check(fuel&&fire,"Wired speed-density example failed actual fuel transport/combustion");
|
||||
auto missing=doc;missing.logic.disconnect({connector,"iat"});rejects([&]{MachineSimulation invalid(missing);},"Unwired required IAT was silently defaulted");
|
||||
auto gauge_doc=doc;gauge_doc.logic.find(pressure)->sensor_kind=MachineSensorKind::DifferentialPressure;check(!logic_connection_preview(gauge_doc.logic,{pressure,"value"},{connector,"map"}).allowed,"Differential-pressure source accepted as MAP");
|
||||
auto calibrated=doc;calibrated.find(ecu)->sensor_ecu.displacement*=2;rejects([&]{sim.apply_tuning(calibrated);},"Displacement changed during Test without Build");
|
||||
const auto path=std::filesystem::temp_directory_path()/("fluidsim-sd-"+std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())+".machine.toml");save_machine(doc,path);const auto loaded=load_machine(path);std::filesystem::remove(path);
|
||||
check(loaded.logic==doc.logic&&loaded.find(ecu)->sensor_ecu==doc.find(ecu)->sensor_ecu&&loaded.find(host)->injector==doc.find(host)->injector,"Speed-density calibration, schema or injector failed save/load");
|
||||
std::vector<std::uint32_t> ids;for(const auto& p:doc.parts)ids.push_back(p.id);const auto assembly=machine_extract_assembly(doc,ids,"Speed-density engine");MachineDocument pasted;machine_insert_assembly(pasted,assembly,{});MachineSimulation copied(pasted,22050);copied.process();
|
||||
auto routed=doc;const auto manual=routed.logic.add(LogicNodeKind::Switch).id,zero=routed.logic.add(LogicNodeKind::NumberConstant).id,select=routed.logic.add(LogicNodeKind::Select).id;routed.logic.find(zero)->quantity=routed.logic.find(select)->quantity=SignalQuantity::Pressure;routed.logic.find(zero)->pressure_reference=routed.logic.find(select)->pressure_reference=PressureReference::Absolute;
|
||||
routed.logic.disconnect({connector,"map"});routed.logic.connect({pressure,"value"},{select,"a"});routed.logic.connect({zero,"out"},{select,"b"});routed.logic.connect({manual,"out"},{select,"select"});routed.logic.connect({select,"out"},{connector,"map"});
|
||||
MachineSimulation switched(routed,22050);for(int i=0;i<10000;++i)switched.process();switched.set_logic_control(manual,true);for(int i=0;i<1000;++i)switched.process();for(const auto& p:switched.snapshot().parts){if(p.id==ecu)check(p.sensor_fault&&p.ignition_cut,"Invalid MAP failed to cut ECU outputs");if(p.id==host)check(p.fuel_flow==0,"Invalid MAP retained injector delivery");if(p.id==cylinder)check(!p.combustion,"Invalid MAP retained controlled combustion");}
|
||||
switched.set_logic_control(manual,false);bool recovered=false;for(int i=0;i<10000;++i){switched.process();if(i%16==0)for(const auto& p:switched.snapshot().parts)if(p.id==cylinder)recovered|=p.combustion;}check(recovered,"Restored MAP failed to recover physical combustion");
|
||||
auto no_host=doc;no_host.find_sensor(no_host.logic.find(temperature)->sensor)->target.part=0;MachineSimulation absent(no_host,22050);absent.process();for(const auto& p:absent.snapshot().parts)if(p.id==host)check(p.injector_delivered==0,"Missing IAT host used hidden gas temperature");
|
||||
std::cout<<"Speed-density ideal-gas/VE calibration, MAP/IAT typing, live faults, combustion and persistence passed\n";return 0;
|
||||
}catch(const std::exception& error){std::cerr<<error.what()<<'\n';return 1;}
|
||||
Reference in New Issue
Block a user