Add machine workshop, typed logic, mechanics and sensor-driven ECUs
This commit is contained in:
70
tests/machine_logic_tests.cpp
Normal file
70
tests/machine_logic_tests.cpp
Normal file
@@ -0,0 +1,70 @@
|
||||
#include "FluidSim/Machine.hpp"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
using namespace fluidsim::port;
|
||||
namespace {
|
||||
void check(bool value,const char* why){if(!value)throw std::runtime_error(why);}
|
||||
bool close(double a,double b,double tolerance=1e-5){return std::abs(a-b)<=tolerance*std::max({1.,std::abs(a),std::abs(b)});}
|
||||
template<class F>void rejects(F&& f,const char* why){bool caught=false;try{f();}catch(const std::exception&){caught=true;}check(caught,why);}
|
||||
void run(MachineSimulation& sim,int count){for(int i=0;i<count;++i){const auto a=sim.process();check(std::isfinite(a.left)&&std::isfinite(a.right),"Logic-controlled machine generated invalid audio");}}
|
||||
SignalValue value(const MachineSnapshot& snapshot,const MachineDocument& doc,std::uint32_t id,const char* key){const auto ports=logic_node_ports(*doc.logic.find(id));for(const auto& n:snapshot.logic)if(n.id==id)for(std::size_t p=0;p<ports.size();++p)if(ports[p].key==key)return n.values[p];throw std::runtime_error("Missing live logic port");}
|
||||
const MachinePartState& part(const MachineSnapshot& snapshot,std::uint32_t id){for(const auto& p:snapshot.parts)if(p.id==id)return p;throw std::runtime_error("Missing physical part");}
|
||||
std::uint32_t node(const MachineDocument& d,LogicNodeKind kind){for(const auto& n:d.logic.nodes)if(n.kind==kind)return n.id;throw std::runtime_error("Missing logic node kind");}
|
||||
}
|
||||
int main(int argc,char** argv)try{
|
||||
MachineDocument controlled;const auto shaft=controlled.add(MachinePartKind::Shaft).id,motor=controlled.add(MachinePartKind::DriveMotor).id;controlled.find(shaft)->shaft.inertia=1;controlled.find(motor)->drive={0,10,false,true};controlled.connect({shaft,0},{motor,0});
|
||||
const auto enable=controlled.logic.add(LogicNodeKind::Command).id;controlled.logic.find(enable)->part=motor;
|
||||
{MachineSimulation inactive(controlled,1000);check(!part(inactive.snapshot(),motor).actuator_enabled,"Signal-owned enable started in its manual state before the first sample");run(inactive,10);check(part(inactive.snapshot(),shaft).shaft_speed==0,"Unwired optional command used the manual enabled state instead of inactive default");}
|
||||
const auto toggle=controlled.logic.add(LogicNodeKind::Switch).id;controlled.logic.connect({toggle,"out"},{enable,"in"});
|
||||
const auto target=controlled.logic.add(LogicNodeKind::Command).id;controlled.logic.find(target)->part=motor;controlled.logic.find(target)->command=LogicCommand::MotorTorque;
|
||||
const auto amount=controlled.logic.add(LogicNodeKind::NumberConstant).id;controlled.logic.find(amount)->quantity=SignalQuantity::Torque;controlled.logic.find(amount)->value=3;controlled.logic.connect({amount,"out"},{target,"in"});
|
||||
MachineSimulation sim(controlled,1000);sim.set_logic_control(toggle,true);run(sim,10);check(close(part(sim.snapshot(),shaft).shaft_speed,.03),"Signal enable / torque target did not own the declared drive fields");
|
||||
sim.set_logic_control(toggle,false);run(sim,10);check(close(part(sim.snapshot(),shaft).shaft_speed,.03)&&part(sim.snapshot(),motor).torque==0,"Disabling a signal-owned motor continued to add drive work");
|
||||
auto tuned=controlled;tuned.logic.find(amount)->value=6;const auto time=sim.snapshot().time;sim.apply_tuning(tuned);check(sim.snapshot().time==time,"Constant tuning reset simulation time");sim.set_logic_control(toggle,true);run(sim,10);check(close(part(sim.snapshot(),shaft).shaft_speed,.09),"Live constant tuning did not update latched drive commands");
|
||||
auto retyped=tuned;retyped.logic.find(amount)->quantity=SignalQuantity::Power;rejects([&]{sim.apply_tuning(retyped);},"A live quantity edit changed compiled pin meaning");
|
||||
auto duplicate=controlled;const auto second=duplicate.logic.add(LogicNodeKind::Command).id;duplicate.logic.find(second)->part=motor;rejects([&]{validate_machine(duplicate);},"Two signal owners claimed the same motor field");
|
||||
{
|
||||
auto bad=controlled;bad.logic.disconnect({target,"in"});const auto divider=bad.logic.add(LogicNodeKind::Divide).id;bad.logic.find(divider)->quantity=SignalQuantity::Torque;const auto zero=bad.logic.add(LogicNodeKind::NumberConstant).id;bad.logic.connect({amount,"out"},{divider,"a"});bad.logic.connect({zero,"out"},{divider,"b"});bad.logic.connect({divider,"out"},{target,"in"});bad.logic.find(toggle)->boolean=true;MachineSimulation fault(bad,1000);run(fault,20);check(part(fault.snapshot(),shaft).shaft_speed==0&&value(fault.snapshot(),bad,target,"in").validity==SignalValidity::Fault,"Invalid numeric command drove the machine or hid its fault");
|
||||
}
|
||||
{
|
||||
auto engine=machine_engine_example();std::uint32_t throttle=0;for(const auto& p:engine.parts)if(p.kind==MachinePartKind::Throttle)throttle=p.id;const auto owner=engine.logic.add(LogicNodeKind::Command).id;engine.logic.find(owner)->command=LogicCommand::ThrottleOpening;engine.logic.find(owner)->part=throttle;rejects([&]{MachineSimulation conflicting(engine);},"Signal and legacy ECU simultaneously owned throttle opening");
|
||||
}
|
||||
{
|
||||
auto engine=machine_engine_example();const auto crank=machine_primary_shaft(engine),drive=engine.add(MachinePartKind::DriveMotor).id;engine.connect({crank,0},{drive,0});const auto command=engine.logic.add(LogicNodeKind::Command).id,speed=engine.logic.add(LogicNodeKind::NumberConstant).id;engine.logic.find(command)->part=drive;engine.logic.find(command)->command=LogicCommand::MotorSpeed;engine.logic.find(speed)->quantity=SignalQuantity::AngularSpeed;engine.logic.find(speed)->value=-10;engine.logic.connect({speed,"out"},{command,"in"});check(logic_node_ports(*engine.logic.find(command),&engine)[0].minimum==0,"Engine command metadata does not expose its forward-only range");MachineSimulation forward(engine);run(forward,16);check(value(forward.snapshot(),engine,command,"in").validity==SignalValidity::Fault&&part(forward.snapshot(),drive).command_target==0,"Negative speed on a forward-only engine stopped Test instead of applying its inactive fault fallback");engine.logic.find(speed)->value=20;forward.apply_tuning(engine);run(forward,16);check(close(part(forward.snapshot(),crank).shaft_speed,20),"A recovered speed command remained faulted");
|
||||
}
|
||||
{
|
||||
MachineDocument valve;const auto duct=valve.add(MachinePartKind::Pipe).id,body=valve.add(MachinePartKind::Throttle).id;valve.connect({duct,1},{body,0});const auto tps=valve.add_sensor(MachineSensorKind::ThrottlePosition,body).id;
|
||||
const auto opening=valve.logic.add(LogicNodeKind::Command).id;valve.logic.find(opening)->command=LogicCommand::ThrottleOpening;valve.logic.find(opening)->part=body;const auto slider=valve.logic.add(LogicNodeKind::Slider).id;valve.logic.find(slider)->quantity=SignalQuantity::Fraction;valve.logic.connect({slider,"out"},{opening,"in"});MachineSimulation commanded(valve);commanded.set_controls(.95f);commanded.set_logic_control(slider,.4);run(commanded,1);check(close(std::get<double>(commanded.sense(tps).outputs[0].value),.4),"TPS or valve ignored signal ownership and followed the pedal");
|
||||
}
|
||||
{
|
||||
MachineDocument pure;const auto button=pure.logic.add(LogicNodeKind::Button).id,lamp=pure.logic.add(LogicNodeKind::Lamp).id;pure.logic.connect({button,"out"},{lamp,"in"});MachineSimulation logic_only(pure,1000);logic_only.set_logic_control(button,true);logic_only.set_logic_control(button,false);run(logic_only,1);check(std::get<bool>(value(logic_only.snapshot(),pure,lamp,"in").value),"A short momentary press vanished before a physics boundary");run(logic_only,1);check(!std::get<bool>(value(logic_only.snapshot(),pure,lamp,"in").value),"A short momentary press remained true");check(logic_only.snapshot().cells==0&&logic_only.snapshot().parts.empty(),"Logic-only test invented physical gas parts");
|
||||
}
|
||||
{
|
||||
MachineDocument start;const auto crank=start.add(MachinePartKind::Shaft).id;start.find(crank)->shaft.inertia=1;start.find(crank)->shaft.starter_torque=5;const auto command=start.logic.add(LogicNodeKind::Command).id,button=start.logic.add(LogicNodeKind::BooleanConstant).id;start.logic.find(command)->part=crank;start.logic.find(command)->command=LogicCommand::Starter;start.logic.find(button)->boolean=true;start.logic.connect({button,"out"},{command,"in"});MachineSimulation starter(start,1000);run(starter,100);check(close(part(starter.snapshot(),crank).shaft_speed,.5),"Starter command did not supply its declared shaft torque");start.logic.find(button)->boolean=false;starter.apply_tuning(start);starter.set_controls(0,true);run(starter,100);check(close(part(starter.snapshot(),crank).shaft_speed,.5),"Keyboard starter competed with signal ownership");
|
||||
}
|
||||
{
|
||||
MachineDocument braking;const auto crank=braking.add(MachinePartKind::Shaft).id,drive=braking.add(MachinePartKind::DriveMotor).id,brake=braking.add(MachinePartKind::Brake).id;braking.find(crank)->shaft.inertia=1;braking.find(drive)->drive={0,4,false,true};braking.find(brake)->brake.constant=5;braking.connect({crank,0},{drive,0});braking.connect({crank,0},{brake,0});const auto command=braking.logic.add(LogicNodeKind::Command).id,toggle=braking.logic.add(LogicNodeKind::Switch).id;braking.logic.find(command)->part=brake;braking.logic.find(command)->command=LogicCommand::BrakeEnable;braking.logic.find(toggle)->boolean=true;braking.logic.connect({toggle,"out"},{command,"in"});MachineSimulation load(braking,1000);run(load,100);check(part(load.snapshot(),crank).shaft_speed==0,"Signal-enabled brake drove through a static hold");load.set_logic_control(toggle,false);run(load,100);check(close(part(load.snapshot(),crank).shaft_speed,.4),"Signal disable did not release the brake");
|
||||
}
|
||||
auto bench=machine_logic_example();const auto master=node(bench,LogicNodeKind::Switch),hysteresis=node(bench,LogicNodeKind::Hysteresis),pressure=node(bench,LogicNodeKind::Sensor);std::uint32_t bench_motor=0,tank=bench.sensors[0].target.part;for(const auto& p:bench.parts)if(p.kind==MachinePartKind::DriveMotor)bench_motor=p.id;
|
||||
MachineSimulation compressor(bench);bool stopped=false;double first_stop=0;for(int i=0;i<44100;++i){compressor.process();if(i%32==0){const auto snapshot=compressor.snapshot();const auto demand=value(snapshot,bench,hysteresis,"out");if(demand.validity==SignalValidity::Valid&&!std::get<bool>(demand.value)){check(part(snapshot,tank).pressure>=149000,"Compressor threshold switched off before its upper pressure");check(std::abs(part(snapshot,bench_motor).torque)<1e-8,"Compressor drive still supplied torque after demand switched off");stopped=true;first_stop=snapshot.time;break;}}}
|
||||
check(stopped&&first_stop>0,"Pressure feedback did not stop the compressor at its upper threshold");
|
||||
{
|
||||
auto counted=bench;counted.cells_per_meter=500;const auto memory=counted.logic.add(LogicNodeKind::NumberDelay).id,sum=counted.logic.add(LogicNodeKind::Add).id,one=counted.logic.add(LogicNodeKind::NumberConstant).id;counted.logic.find(one)->value=1;counted.logic.connect({memory,"out"},{sum,"a"});counted.logic.connect({one,"out"},{sum,"b"});counted.logic.connect({sum,"out"},{memory,"in"});MachineSimulation ticks(counted);run(ticks,10);const auto snapshot=ticks.snapshot();check(snapshot.substeps>10&&std::get<double>(value(snapshot,counted,memory,"out").value)==9,"Logic advanced at fluid CFL frequency instead of the configured sample boundary");
|
||||
}
|
||||
MachineSimulation manual(bench);manual.set_logic_control(master,false);run(manual,64);check(part(manual.snapshot(),bench_motor).shaft_speed==0,"Master switch failed to inhibit automatic compressor demand");
|
||||
auto missing=bench;missing.remove_sensor(bench.logic.find(pressure)->sensor);check(missing.logic.find(pressure)->sensor==0,"Deleted sensor reference can silently match a later sensor ID");MachineSimulation safe(missing);run(safe,64);check(part(safe.snapshot(),bench_motor).shaft_speed==0&&value(safe.snapshot(),missing,pressure,"value").validity==SignalValidity::Missing,"Missing pressure sensor enabled compressor drive");
|
||||
auto removed_host=bench;removed_host.remove(tank);MachineSimulation detached_sensor(removed_host);run(detached_sensor,64);check(!part(detached_sensor.snapshot(),bench_motor).actuator_enabled,"Unresolved physical sensor host enabled the controlled motor");
|
||||
auto wrong=bench;wrong.sensors[0].kind=MachineSensorKind::GasTemperature;MachineSimulation wrong_units(wrong);run(wrong_units,64);check(part(wrong_units.snapshot(),bench_motor).shaft_speed==0,"Mismatched sensor schema fed temperature into pressure control");
|
||||
{
|
||||
std::vector<std::uint32_t> all;for(const auto& p:bench.parts)all.push_back(p.id);const auto assembly=machine_extract_assembly(bench,all,"Automatic compressor module");check(assembly.logic.nodes.size()==bench.logic.nodes.size(),"Assembly dropped a connected control harness");auto combined=bench;machine_insert_assembly(combined,assembly,{2,.25f,0});check(combined.logic.nodes.size()==2*bench.logic.nodes.size()&&combined.sensors.size()==2*bench.sensors.size(),"Copied logic or sensors were not independently remapped");
|
||||
MachineSimulation independent(combined);independent.set_logic_control(master,false);run(independent,64);check(part(independent.snapshot(),bench_motor).shaft_speed==0,"Original master switch was lost during assembly insertion");bool copy_running=false;for(const auto& p:independent.snapshot().parts)if(p.id!=bench_motor&&combined.find(p.id)->kind==MachinePartKind::DriveMotor)copy_running=p.shaft_speed>80;check(copy_running,"Copied compressor still used the original master switch or sensor/command IDs");
|
||||
const auto incomplete=machine_extract_assembly(bench,{bench_motor},"External sensor harness");check(!incomplete.logic.nodes.empty()&&!incomplete.sensors.empty()&&incomplete.sensors[0].target.part==0,"Partial assembly silently retained external physical sensor IDs");
|
||||
}
|
||||
const auto file=std::filesystem::temp_directory_path()/("fluidsim-machine-logic-"+std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())+".machine.toml");save_machine(bench,file);const auto restored=load_machine(file);check(restored.logic==bench.logic,"Saved compressor harness changed its topology or calibration");MachineSimulation roundtrip(restored);run(roundtrip,128);check(part(roundtrip.snapshot(),bench_motor).shaft_speed>80,"Saved graph did not operate its physical drive");std::filesystem::remove(file);
|
||||
if(argc==3&&std::string(argv[1])=="--write-example")save_machine(bench,argv[2]);
|
||||
std::cout<<"Per-sample logic, explicit actuator ownership/fallbacks, pressure-controlled compression, independent copied harnesses and persistence passed\n";return 0;
|
||||
}catch(const std::exception& e){std::cerr<<e.what()<<'\n';return 1;}
|
||||
Reference in New Issue
Block a user