#include "FluidSim/DesktopScenario.hpp" #include #include #include #include #include #include #include #include using namespace fluidsim::port; namespace { void check(bool value,const char* message) {if(!value)throw std::runtime_error(message);} bool near(float a,float b) {return std::abs(a-b)<1e-5f;} struct TempDirectory { std::filesystem::path path=std::filesystem::temp_directory_path()/ ("fluidsim-live-tuning-"+std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); TempDirectory(){std::filesystem::create_directory(path);} ~TempDirectory(){std::error_code ignored;std::filesystem::remove_all(path,ignored);} }; } int main() { const auto& source=scenario_example("single-450"); FourStrokeScenario simulation(44100,source); for(int i=0;i<1024;++i)simulation.process(); const float rpm=simulation.crankshaft.omega,angle=simulation.crankshaft.angle; const float pressure=simulation.cylinder1.pressure,lift=simulation.cylinder1.intake_lift(); const auto samples=simulation.sample_count; auto tuned=source.engine; tuned.intake_valve.mass_kg=.047f;tuned.intake_valve.spring_rate_n_per_m=45000; tuned.intake_valve.damping_n_s_per_m=32;tuned.valve_sound.radiation_area=.0005f; tuned.ecu.idle_base_opening=.055f;tuned.fuel.pilot_jet_diameter*=1.1f; for(auto& p:tuned.intake_cam.points)p.lift_metres*=.8f; for(auto& p:tuned.ecu.ignition_map)p.degrees_btdc+=4; simulation.set_tuning(tuned); check(simulation.crankshaft.omega==rpm && simulation.crankshaft.angle==angle && simulation.sample_count==samples,"live edit restarted the engine"); check(simulation.cylinder1.pressure==pressure && simulation.cylinder1.intake_lift()==lift,"live edit reset cylinder or valve state"); const auto snapshot=simulation.snapshot(); check(near(snapshot.intake_cam_lifts[120],FourStrokeCylinder::cam_lift_at(tuned.intake_cam,480)),"live cam profile was not published"); check(near(simulation.ecu.idle_base_opening,.055f),"ECU calibration was not applied"); for(int i=0;i<2048;++i) { const auto frame=simulation.process(); check(std::isfinite(frame.left) && std::isfinite(frame.right),"live tuning produced nonfinite audio"); } auto invalid=tuned;invalid.pipes[0].length*=1.5f; bool rejected=false;try {simulation.set_tuning(invalid);}catch(const std::invalid_argument&){rejected=true;} check(rejected,"mesh edits must require a rebuild"); invalid=tuned;invalid.intake_valve.mass_kg=std::numeric_limits::quiet_NaN(); rejected=false;try {simulation.set_tuning(invalid);}catch(const std::exception&){rejected=true;} check(rejected,"invalid calibration accepted"); TempDirectory temp; const auto saved=save_tuned_engine(source,tuned,temp.path); const auto loaded=load_engine_file(saved); check(loaded.id=="single-450-tuned" && loaded.source_path!=source.source_path,"export replaced source identity"); check(near(loaded.engine.intake_valve.mass_kg,tuned.intake_valve.mass_kg),"valve mass export changed units"); check(near(loaded.engine.fuel.pilot_jet_diameter,tuned.fuel.pilot_jet_diameter),"carburetor edit was not exported"); check(near(loaded.engine.ecu.ignition_map[0].degrees_btdc,tuned.ecu.ignition_map[0].degrees_btdc),"ignition trim was not exported"); check(near(loaded.engine.intake_cam.points[10].lift_metres,tuned.intake_cam.points[10].lift_metres),"cam profile export changed units"); auto at_limit=tuned;at_limit.intake_cam.points[10].lift_metres=at_limit.intake_valve.maximum_lift_m; check(save_tuned_engine(source,at_limit,temp.path)!=saved,"repeat export overwrote an engine"); DesktopScenario desktop(source); desktop.set_controls(.2f,.1f,.6f);desktop.set_pipe_solver(PipeSolver::Hlle);desktop.set_audio_scene(OutdoorScene::Dry); auto pipes=tuned;auto& route=pipes.geometry.pipes[0];const auto end=route.points.back(),start=route.points.front(); route.points.front()={end.x+(start.x-end.x)*1.2f,end.y+(start.y-end.y)*1.2f,end.z+(start.z-end.z)*1.2f}; pipes.pipes[0].length=route.length(); desktop.rebuild_engine(pipes); const auto rebuilt=desktop.snapshot(); const float length=std::accumulate(rebuilt.pipe_lengths[0].begin(),rebuilt.pipe_lengths[0].end(),0.f); check(near(length,pipes.pipes[0].length),"rebuilt mesh uses old dimensions"); check(rebuilt.pipe_solver==PipeSolver::Hlle && rebuilt.audio_scene==OutdoorScene::Dry && near(rebuilt.clutch,.6f),"rebuild lost scene or controls"); std::atomic finite{true}; std::jthread worker([&]{for(int i=0;i<4096;++i) {auto f=desktop.process();if(!std::isfinite(f.left)||!std::isfinite(f.right))finite=false;}}); for(int i=0;i<16;++i) {auto live=pipes;live.intake_valve.mass_kg+=float(i)*.0001f;desktop.set_tuning(live);desktop.snapshot();} desktop.rebuild_engine(pipes); worker.join();check(finite,"concurrent editing or rebuild corrupted the simulation"); std::cout<<"Live tuning state, export round trip, mesh rebuild and concurrent publication passed\n"; }