#include "FluidSim/Machine.hpp" #include "MachineTestControls.hpp" #include #include #include #include #include #include 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)});} templatevoid rejects(F&& f,const char* why){bool caught=false;try{f();}catch(const std::exception&){caught=true;}check(caught,why);} void run(MachineSimulation& s,int samples){for(int i=0;iid;} } int main()try{ MachineDocument bench;bench.name="Independent mechanical bench"; const auto a=bench.add(MachinePartKind::Shaft,{}).id,b=bench.add(MachinePartKind::Shaft,{0,0,.5f}).id; bench.find(a)->shaft.inertia=2;bench.find(b)->shaft.inertia=4; const auto wheel=bench.add(MachinePartKind::Flywheel,{.3f,0,0}).id;bench.find(wheel)->shaft.inertia=1;bench.connect({wheel,0},{a,0}); const auto motor=bench.add(MachinePartKind::DriveMotor,{-.3f,0,0}).id;bench.find(motor)->drive={0,6,false,true};bench.connect({a,0},{motor,0}); const auto reverse=bench.add(MachinePartKind::DriveMotor,{-.3f,0,.5f}).id;bench.find(reverse)->drive={0,-4,false,true};bench.connect({b,0},{reverse,0}); const auto brake=bench.add(MachinePartKind::Brake,{.3f,0,.2f}).id;bench.find(brake)->brake.constant=1;bench.connect({a,0},{brake,0}); bench.probes={{1,a,"First RPM",MachineQuantity::Rpm},{2,b,"Second RPM",MachineQuantity::Rpm},{3,motor,"Motor torque",MachineQuantity::Torque},{4,brake,"Absorbed power",MachineQuantity::Power}}; validate_machine(bench);check(machine_part_volume(*bench.find(a))==0&&machine_part_volume(*bench.find(motor))==0,"Mechanical parts added gas storage"); check(!machine_connection_preview(bench,{a,0},{b,0}).allowed,"Direct shaft-to-shaft rigid constraint was accepted without a coupling"); check(!machine_connection_preview(bench,{b,0},{wheel,0}).allowed,"Flywheel was attached to two independent shafts"); for(int rate:{1000,4000}){MachineSimulation sim(bench,rate);run(sim,rate);const auto state=sim.snapshot();check(state.cells==0&&state.shafts.size()==2,"Pure mechanical machine required a hidden gas network"); check(close(shaft(state,a).speed,5./3)&&close(shaft(state,b).speed,-1),"Compiled shafts share state or use the wrong effective inertia"); check(close(shaft(state,a).inertia,3),"Compiled flywheel inertia was counted incorrectly");const auto values=sim.measure();check(close(values.values[0],5./3*60/(2*pi))&&close(values.values[1],-60/(2*pi))&&values.values[2]==6&&values.values[3]>=0,"Rotational probes lost their named targets or sign conventions");balance(shaft(state,a));balance(shaft(state,b)); auto tuned=bench;tuned.find(motor)->drive.torque=9;const auto time=state.time;sim.apply_tuning(tuned);check(sim.snapshot().time==time,"Live motor tuning restarted the simulation");run(sim,rate);check(shaft(sim.snapshot(),a).speed>4,"Live motor tuning did not affect the running shaft"); tuned.find(wheel)->shaft.inertia=2;rejects([&]{sim.apply_tuning(tuned);},"Flywheel inertia was rebuilt during Test"); } auto detached=bench;detached.disconnect({wheel,0});rejects([&]{MachineSimulation invalid(detached);},"Unattached flywheel ran silently"); const auto detached_checks=machine_checks(detached);check(std::any_of(detached_checks.begin(),detached_checks.end(),[&](const auto& c){return c.part==wheel&&c.title=="No shaft attached";}),"Unattached flywheel lacks a navigable check"); auto first=machine_engine_example();machine_test::drive(first,1200);const auto first_cylinder=cylinder(first),first_shaft=machine_shaft_for(first,first_cylinder); auto second=machine_engine_example();machine_test::drive(second,2400);const auto second_cylinder=cylinder(second); std::vector selected;for(const auto& p:second.parts)selected.push_back(p.id);const auto assembly=machine_extract_assembly(second,selected,"Second engine"); const auto ids=machine_insert_assembly(first,assembly,{1.5f,.3f,0});std::uint32_t copied_cylinder=0,copied_motor=0; for(std::size_t i=0;idrive.target_speed=3600*2*pi/60;engines.apply_tuning(live);check(engines.snapshot().time==state.time,"Second engine tuning reset the first engine");run(engines,128);check(close(engines.measure().values[0],1200)&&close(engines.measure().values[1],3600),"Tuning one motor changed another engine");balance(shaft(engines.snapshot(),first_shaft));balance(shaft(engines.snapshot(),second_shaft)); auto mixed=first;std::uint32_t controller=0;for(const auto& link:mixed.links)if(link.kind==MachineConnectionKind::Control&&link.b.part==first_cylinder)controller=link.a.part;mixed.disconnect({copied_cylinder,2});mixed.connect({controller,0},{copied_cylinder,2});rejects([&]{MachineSimulation invalid(mixed);},"An ECU silently controlled cylinders on different shafts"); auto unbound=first;unbound.disconnect({first_cylinder,machine_port_index(*unbound.find(first_cylinder),"shaft")});rejects([&]{MachineSimulation invalid(unbound);},"Cylinder without a shaft ran on an implicit crank"); auto reverse_engine=first;reverse_engine.find(first_shaft)->shaft.initial_speed=-10;rejects([&]{MachineSimulation invalid(reverse_engine);},"Unsupported reverse engine timing was enabled"); auto moved=first;std::vector> paths;for(const auto& p:first.parts)if(p.kind==MachinePartKind::Pipe)paths.push_back(p.points);moved.transform_selection({first_shaft},{},{.2f,.1f,0},0);validate_machine(moved);std::size_t index=0;for(const auto& p:moved.parts)if(p.kind==MachinePartKind::Pipe)check(p.points==paths[index++],"Moving a shaft silently changed gas geometry"); const auto checks=machine_checks(unbound);check(std::any_of(checks.begin(),checks.end(),[&](const auto& c){return c.part==first_cylinder&&c.title=="No shaft attached";}),"Unattached cylinder lacks a navigable check"); { std::vector all;for(const auto& p:bench.parts)all.push_back(p.id);const auto module=machine_extract_assembly(bench,all,"Mechanical module");auto destination=bench;const auto copied=machine_insert_assembly(destination,module,{2,0,0}); check(destination.probes.size()==8&&destination.probes[4].part==copied[0]&&destination.probes[4].part!=a,"Copied rotational instrument did not follow its copied shaft"); const auto source_motor=std::find_if(module.parts.begin(),module.parts.end(),[&](const auto& p){return p.id==motor;})-module.parts.begin();destination.find(copied[source_motor])->drive.torque=12; MachineSimulation copied_sim(destination,1000);run(copied_sim,1000);check(copied_sim.measure().values[4]>copied_sim.measure().values[0]*2,"Copied motor or instrument still referenced the original shaft"); const auto loose=machine_extract_assembly(bench,{wheel},"Unattached flywheel");const auto inserted=machine_insert_assembly(destination,loose,{3,0,0});check(machine_shaft_for(destination,inserted.front())==0,"External assembly attachment accidentally matched a destination ID");rejects([&]{MachineSimulation invalid(destination);},"Unresolved external shaft attachment was silently repaired"); } const auto file=std::filesystem::temp_directory_path()/("fluidsim-mechanical-"+std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())+".machine.toml");save_machine(bench,file);const auto restored=load_machine(file);std::filesystem::remove(file); check(restored.parts.size()==bench.parts.size()&&restored.links.size()==bench.links.size()&&restored.primary_shaft==bench.primary_shaft,"Explicit mechanical topology did not round-trip");MachineSimulation restored_sim(restored,1000);run(restored_sim,1000);check(close(shaft(restored_sim.snapshot(),a).speed,5./3),"Saved motor/flywheel configuration changed behavior"); {std::ofstream legacy(file);legacy<