From abfcce0f463e4c026c991940670179a21e48ef66 Mon Sep 17 00:00:00 2001 From: max Date: Mon, 2 Nov 2026 07:27:13 +0100 Subject: [PATCH] Add machine workshop, typed logic, mechanics and sensor-driven ECUs --- CMakeLists.txt | 67 +- README.md | 70 +- docs/COMPONENT_IMPLEMENTATION.md | 354 ++ docs/MACHINE_BUILDER.md | 620 +++ docs/MECHANICAL_BUILDER_PLAN.md | 621 +++ docs/WORKSHOP_UI_PLAN.md | 258 + include/FluidSim/Carburetor.hpp | 2 +- include/FluidSim/Component.hpp | 3 + include/FluidSim/Desktop/LogicEditor.hpp | 45 + include/FluidSim/Desktop/MachineBuilder.hpp | 57 + .../FluidSim/Desktop/MachineRenderer3D.hpp | 21 + include/FluidSim/Desktop/PortAppearance.hpp | 10 + include/FluidSim/Desktop/ScenarioMenu.hpp | 22 +- .../FluidSim/Desktop/WindowPresentation.hpp | 8 + include/FluidSim/Desktop/WorkshopLayout.hpp | 30 + include/FluidSim/Ecu.hpp | 71 +- include/FluidSim/Engine.hpp | 15 + include/FluidSim/Fluid.hpp | 89 +- include/FluidSim/FuelInjector.hpp | 31 + include/FluidSim/Machine.hpp | 288 + include/FluidSim/MachineLogic.hpp | 111 + include/FluidSim/MachinePorts.hpp | 51 + include/FluidSim/MachineSensors.hpp | 40 + include/FluidSim/Mechanics.hpp | 54 + include/FluidSim/PipeProfile.hpp | 27 + include/FluidSim/SimulationRingBuffer.hpp | 2 + machines/air-compressor.machine.toml | 1117 ++++ .../assemblies/3-cyl-header.assembly.toml | 422 ++ machines/flat6-imported.machine.toml | 3293 +++++++++++ machines/logic-compressor-bench.machine.toml | 1076 ++++ machines/paralell-twin.machine.toml | 1465 +++++ machines/sensor-alpha-n-bench.machine.toml | 1399 +++++ machines/sensor-bench.machine.toml | 165 + machines/sensor-ignition-bench.machine.toml | 1357 +++++ .../sensor-speed-density-bench.machine.toml | 1462 +++++ machines/shaft-drive-bench.machine.toml | 93 + machines/single-450-imported.machine.toml | 1134 ++++ machines/single-cylinder.machine.toml | 560 ++ machines/twin-imported.machine.toml | 1465 +++++ machines/v8-h-imported.machine.toml | 4957 +++++++++++++++++ machines/vacuum-pump.machine.toml | 254 + src/builder_geometry.cpp | 61 + src/carburetor.cpp | 4 +- src/desktop_main.cpp | 23 +- src/ecu.cpp | 137 +- src/engine.cpp | 40 +- src/engine_config.cpp | 3 + src/fuel_injector.cpp | 45 + src/logic_assembly.cpp | 25 + src/logic_assembly.hpp | 8 + src/logic_editor.cpp | 309 + src/logic_example.cpp | 52 + src/logic_runtime.cpp | 143 + src/logic_serialization.cpp | 47 + src/logic_serialization.hpp | 7 + src/machine.cpp | 621 +++ src/machine_builder.cpp | 1114 ++++ src/machine_import.cpp | 71 + src/machine_logic.cpp | 133 + src/machine_mechanics.cpp | 32 + src/machine_ports.cpp | 71 + src/machine_renderer_3d.cpp | 185 + src/machine_sensors.cpp | 77 + src/machine_simulation.cpp | 382 ++ src/mechanics.cpp | 101 + src/scenario_menu.cpp | 258 +- src/simulation_ring_buffer.cpp | 1 + src/solver.cpp | 1 + src/speed_density_example.cpp | 30 + src/window_presentation.cpp | 28 + tests/MachineTestControls.hpp | 13 + tests/junction_tests.cpp | 11 +- tests/logic_editor_tests.cpp | 86 + tests/logic_tests.cpp | 84 + tests/machine_builder_tests.cpp | 338 ++ tests/machine_logic_tests.cpp | 70 + tests/machine_mechanics_tests.cpp | 84 + tests/machine_port_tests.cpp | 130 + tests/machine_sensor_tests.cpp | 66 + tests/machine_tests.cpp | 231 + tests/mechanics_tests.cpp | 79 + tests/menu_tests.cpp | 50 + tests/physics_tests.cpp | 25 +- tests/sensor_ecu_tests.cpp | 141 + tests/speed_density_tests.cpp | 55 + tools/MachineBench.hpp | 11 + tools/machine_benchmark.cpp | 32 + tools/machine_render_benchmark.cpp | 41 + .../performance/2026-10-30/workshop-after.csv | 9 + .../2026-10-30/workshop-before.txt | 8 + .../2026-10-30/workshop-high-load.csv | 9 + .../2026-10-30/workshop-playback.csv | 3 + .../2026-10-30/workshop-render-after.csv | 3 + .../2026-10-30/workshop-render-before.csv | 3 + .../performance/2026-10-30/workshop-report.md | 95 + .../2026-10-31/instruments-playback-0.01.csv | 3 + .../2026-10-31/instruments-playback-0.1.csv | 3 + .../2026-10-31/instruments-playback-1.csv | 3 + .../2026-10-31/workshop-instruments.md | 99 + 99 files changed, 28835 insertions(+), 145 deletions(-) create mode 100644 docs/COMPONENT_IMPLEMENTATION.md create mode 100644 docs/MACHINE_BUILDER.md create mode 100644 docs/MECHANICAL_BUILDER_PLAN.md create mode 100644 docs/WORKSHOP_UI_PLAN.md create mode 100644 include/FluidSim/Desktop/LogicEditor.hpp create mode 100644 include/FluidSim/Desktop/MachineBuilder.hpp create mode 100644 include/FluidSim/Desktop/MachineRenderer3D.hpp create mode 100644 include/FluidSim/Desktop/PortAppearance.hpp create mode 100644 include/FluidSim/Desktop/WindowPresentation.hpp create mode 100644 include/FluidSim/Desktop/WorkshopLayout.hpp create mode 100644 include/FluidSim/FuelInjector.hpp create mode 100644 include/FluidSim/Machine.hpp create mode 100644 include/FluidSim/MachineLogic.hpp create mode 100644 include/FluidSim/MachinePorts.hpp create mode 100644 include/FluidSim/MachineSensors.hpp create mode 100644 include/FluidSim/Mechanics.hpp create mode 100644 machines/air-compressor.machine.toml create mode 100644 machines/assemblies/3-cyl-header.assembly.toml create mode 100644 machines/flat6-imported.machine.toml create mode 100644 machines/logic-compressor-bench.machine.toml create mode 100644 machines/paralell-twin.machine.toml create mode 100644 machines/sensor-alpha-n-bench.machine.toml create mode 100644 machines/sensor-bench.machine.toml create mode 100644 machines/sensor-ignition-bench.machine.toml create mode 100644 machines/sensor-speed-density-bench.machine.toml create mode 100644 machines/shaft-drive-bench.machine.toml create mode 100644 machines/single-450-imported.machine.toml create mode 100644 machines/single-cylinder.machine.toml create mode 100644 machines/twin-imported.machine.toml create mode 100644 machines/v8-h-imported.machine.toml create mode 100644 machines/vacuum-pump.machine.toml create mode 100644 src/builder_geometry.cpp create mode 100644 src/fuel_injector.cpp create mode 100644 src/logic_assembly.cpp create mode 100644 src/logic_assembly.hpp create mode 100644 src/logic_editor.cpp create mode 100644 src/logic_example.cpp create mode 100644 src/logic_runtime.cpp create mode 100644 src/logic_serialization.cpp create mode 100644 src/logic_serialization.hpp create mode 100644 src/machine.cpp create mode 100644 src/machine_builder.cpp create mode 100644 src/machine_import.cpp create mode 100644 src/machine_logic.cpp create mode 100644 src/machine_mechanics.cpp create mode 100644 src/machine_ports.cpp create mode 100644 src/machine_renderer_3d.cpp create mode 100644 src/machine_sensors.cpp create mode 100644 src/machine_simulation.cpp create mode 100644 src/mechanics.cpp create mode 100644 src/speed_density_example.cpp create mode 100644 src/window_presentation.cpp create mode 100644 tests/MachineTestControls.hpp create mode 100644 tests/logic_editor_tests.cpp create mode 100644 tests/logic_tests.cpp create mode 100644 tests/machine_builder_tests.cpp create mode 100644 tests/machine_logic_tests.cpp create mode 100644 tests/machine_mechanics_tests.cpp create mode 100644 tests/machine_port_tests.cpp create mode 100644 tests/machine_sensor_tests.cpp create mode 100644 tests/machine_tests.cpp create mode 100644 tests/mechanics_tests.cpp create mode 100644 tests/menu_tests.cpp create mode 100644 tests/sensor_ecu_tests.cpp create mode 100644 tests/speed_density_tests.cpp create mode 100644 tools/MachineBench.hpp create mode 100644 tools/machine_benchmark.cpp create mode 100644 tools/machine_render_benchmark.cpp create mode 100644 verification/performance/2026-10-30/workshop-after.csv create mode 100644 verification/performance/2026-10-30/workshop-before.txt create mode 100644 verification/performance/2026-10-30/workshop-high-load.csv create mode 100644 verification/performance/2026-10-30/workshop-playback.csv create mode 100644 verification/performance/2026-10-30/workshop-render-after.csv create mode 100644 verification/performance/2026-10-30/workshop-render-before.csv create mode 100644 verification/performance/2026-10-30/workshop-report.md create mode 100644 verification/performance/2026-10-31/instruments-playback-0.01.csv create mode 100644 verification/performance/2026-10-31/instruments-playback-0.1.csv create mode 100644 verification/performance/2026-10-31/instruments-playback-1.csv create mode 100644 verification/performance/2026-10-31/workshop-instruments.md diff --git a/CMakeLists.txt b/CMakeLists.txt index 9132eea..6e342f3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,7 @@ add_library(fluidsim_core src/engine_config.cpp src/carburetor.cpp src/ecu.cpp + src/fuel_injector.cpp src/engine.cpp src/crankcase.cpp src/two_stroke_cylinder.cpp @@ -21,6 +22,19 @@ add_library(fluidsim_core src/scenario_snapshot.cpp src/scenario_geometry.cpp src/physical_geometry.cpp + src/machine.cpp + src/machine_ports.cpp + src/mechanics.cpp + src/machine_mechanics.cpp + src/machine_sensors.cpp + src/machine_logic.cpp + src/logic_runtime.cpp + src/logic_serialization.cpp + src/logic_assembly.cpp + src/logic_example.cpp + src/speed_density_example.cpp + src/machine_simulation.cpp + src/machine_import.cpp src/solver.cpp src/vehicle.cpp src/simulation_ring_buffer.cpp @@ -49,6 +63,8 @@ add_executable(fluidsim_parity_trace tools/parity_trace.cpp) target_link_libraries(fluidsim_parity_trace PRIVATE fluidsim_core) add_executable(fluidsim_migrate_geometry tools/migrate_geometry.cpp) target_link_libraries(fluidsim_migrate_geometry PRIVATE fluidsim_core) +add_executable(fluidsim_machine_benchmark tools/machine_benchmark.cpp) +target_link_libraries(fluidsim_machine_benchmark PRIVATE fluidsim_core) if(FLUIDSIM_BUILD_DESKTOP) find_package(SFML 3.0.2 COMPONENTS Graphics Audio QUIET) @@ -72,15 +88,22 @@ if(FLUIDSIM_BUILD_DESKTOP) endif() find_package(OpenGL REQUIRED) add_library(fluidsim_desktop_rendering - src/desktop_ui.cpp src/scenario_menu.cpp src/engine_schematic.cpp src/engine_renderer_3d.cpp src/dyno_renderer.cpp) - target_link_libraries(fluidsim_desktop_rendering PUBLIC fluidsim_core SFML::Graphics OpenGL::GL) + src/desktop_ui.cpp src/scenario_menu.cpp src/engine_schematic.cpp src/engine_renderer_3d.cpp src/dyno_renderer.cpp + src/machine_builder.cpp src/builder_geometry.cpp src/machine_renderer_3d.cpp src/window_presentation.cpp src/logic_editor.cpp + src/audio_output_stream.cpp src/sound_engine.cpp) + target_link_libraries(fluidsim_desktop_rendering PUBLIC fluidsim_core SFML::Graphics SFML::Audio OpenGL::GL) + if(WIN32) + target_link_libraries(fluidsim_desktop_rendering PRIVATE user32) + endif() if(MSVC) target_compile_options(fluidsim_desktop_rendering PRIVATE /W4 /permissive-) else() target_compile_options(fluidsim_desktop_rendering PRIVATE -Wall -Wextra -Wpedantic) endif() - add_executable(fluidsim_desktop src/desktop_main.cpp src/audio_output_stream.cpp src/sound_engine.cpp) + add_executable(fluidsim_desktop src/desktop_main.cpp) target_link_libraries(fluidsim_desktop PRIVATE fluidsim_desktop_rendering SFML::Audio) + add_executable(fluidsim_machine_render_benchmark tools/machine_render_benchmark.cpp) + target_link_libraries(fluidsim_machine_render_benchmark PRIVATE fluidsim_desktop_rendering) if(MSVC) target_compile_options(fluidsim_desktop PRIVATE /W4 /permissive-) else() @@ -117,6 +140,34 @@ endif() include(CTest) if(BUILD_TESTING) + add_executable(fluidsim_speed_density_tests tests/speed_density_tests.cpp) + target_link_libraries(fluidsim_speed_density_tests PRIVATE fluidsim_core) + add_test(NAME fluidsim_speed_density_tests COMMAND fluidsim_speed_density_tests) + add_executable(fluidsim_sensor_ecu_tests tests/sensor_ecu_tests.cpp) + target_link_libraries(fluidsim_sensor_ecu_tests PRIVATE fluidsim_core) + add_test(NAME fluidsim_sensor_ecu_tests COMMAND fluidsim_sensor_ecu_tests) + add_executable(fluidsim_logic_tests tests/logic_tests.cpp) + target_link_libraries(fluidsim_logic_tests PRIVATE fluidsim_core) + add_test(NAME fluidsim_logic_tests COMMAND fluidsim_logic_tests) + add_executable(fluidsim_machine_logic_tests tests/machine_logic_tests.cpp) + target_link_libraries(fluidsim_machine_logic_tests PRIVATE fluidsim_core) + add_test(NAME fluidsim_machine_logic_tests COMMAND fluidsim_machine_logic_tests) + add_executable(fluidsim_machine_tests tests/machine_tests.cpp) + target_link_libraries(fluidsim_machine_tests PRIVATE fluidsim_core) + add_test(NAME fluidsim_machine_tests COMMAND fluidsim_machine_tests) + add_executable(fluidsim_machine_sensor_tests tests/machine_sensor_tests.cpp) + target_link_libraries(fluidsim_machine_sensor_tests PRIVATE fluidsim_core) + target_compile_definitions(fluidsim_machine_sensor_tests PRIVATE FLUIDSIM_SENSOR_EXAMPLE_FILE="${CMAKE_CURRENT_SOURCE_DIR}/machines/sensor-bench.machine.toml") + add_test(NAME fluidsim_machine_sensor_tests COMMAND fluidsim_machine_sensor_tests) + add_executable(fluidsim_machine_port_tests tests/machine_port_tests.cpp) + target_link_libraries(fluidsim_machine_port_tests PRIVATE fluidsim_core) + add_test(NAME fluidsim_machine_port_tests COMMAND fluidsim_machine_port_tests) + add_executable(fluidsim_mechanics_tests tests/mechanics_tests.cpp) + target_link_libraries(fluidsim_mechanics_tests PRIVATE fluidsim_core) + add_test(NAME fluidsim_mechanics_tests COMMAND fluidsim_mechanics_tests) + add_executable(fluidsim_machine_mechanics_tests tests/machine_mechanics_tests.cpp) + target_link_libraries(fluidsim_machine_mechanics_tests PRIVATE fluidsim_core) + add_test(NAME fluidsim_machine_mechanics_tests COMMAND fluidsim_machine_mechanics_tests) add_executable(fluidsim_physical_geometry_tests tests/physical_geometry_tests.cpp) target_link_libraries(fluidsim_physical_geometry_tests PRIVATE fluidsim_core) target_include_directories(fluidsim_physical_geometry_tests SYSTEM PRIVATE third_party/tomlplusplus) @@ -138,6 +189,16 @@ if(BUILD_TESTING) add_test(NAME fluidsim_scenario_examples COMMAND fluidsim_scenario_examples) endif() if(BUILD_TESTING AND FLUIDSIM_BUILD_DESKTOP) + add_executable(fluidsim_logic_editor_tests tests/logic_editor_tests.cpp) + target_link_libraries(fluidsim_logic_editor_tests PRIVATE fluidsim_desktop_rendering) + add_test(NAME fluidsim_logic_editor_tests COMMAND fluidsim_logic_editor_tests) + add_executable(fluidsim_menu_tests tests/menu_tests.cpp) + target_link_libraries(fluidsim_menu_tests PRIVATE fluidsim_desktop_rendering) + add_test(NAME fluidsim_menu_tests COMMAND fluidsim_menu_tests) + add_test(NAME fluidsim_window_tests COMMAND fluidsim_menu_tests --window-check) + add_executable(fluidsim_machine_builder_tests tests/machine_builder_tests.cpp) + target_link_libraries(fluidsim_machine_builder_tests PRIVATE fluidsim_desktop_rendering) + add_test(NAME fluidsim_machine_builder_tests COMMAND fluidsim_machine_builder_tests) add_executable(fluidsim_desktop_ui_tests tests/desktop_ui_tests.cpp) target_link_libraries(fluidsim_desktop_ui_tests PRIVATE fluidsim_desktop_rendering) add_test(NAME fluidsim_desktop_ui_tests COMMAND fluidsim_desktop_ui_tests) diff --git a/README.md b/README.md index 35b84f8..3de810f 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,54 @@ radiation, thermodynamics and convergence under time/space refinement. ## Build and run (Windows and Linux) +The desktop now includes a [3D machine builder](docs/MACHINE_BUILDER.md). Open +**Machine workshop** in the scenario menu to place and configure pipes, tanks, +valves, engine cylinders and piston pumps. Saved machines support **Edit** and +**Play**, and **Tab** switches between building and testing. +The picker includes 3D previews, carburetors and ECUs. **Import engine layout** +opens editable copies of the 450 single, twin, flat six and V8. Shift selects groups; +blue pins carry ECU control wires, and ideal junctions accept multiple pipes at +one point. Engine cylinders require an explicit connected fuel source. +**Ctrl+C / Ctrl+V** copies and pastes selected groups with their internal air and +ECU connections. **Assemblies** saves selected groups for reuse across machines. +Escape cancels tools or selection; use **Menu** to leave the workshop. +**Meters (M)** adds pressure, temperature, flow, AFR/lambda and shaft instruments +with live traces and CSV export. Test playback slows to 1% speed and supports +paused 10 ms steps. Carb/ECU and other safe settings can be tuned live; geometry +and compiled settings stay locked until returning to Build. + +The **Mech** category adds explicit shafts, flywheels, drive motors and brakes. +Cylinders/pumps connect to named shafts; independent engines can run at different +speeds. Try **Shaft, flywheel and brake bench** from saved machines. Mechanical-only +machines need no gas pipes. Older machine saves migrate in memory, and new saves +use version 10 with stable ports, mechanical parameters, sensor attachments, +a separate typed signal graph and explicit sensor-ECU/injector bindings. +The **Sense** inspector adds pressure/MAP, differential pressure, gas temperature, +total pipe mass flow, crank speed/angle and throttle-position pickups. Sensor +channels sample independently of recorded instruments and show validity/time; +deleting a host leaves an unresolved binding. Right-click **Add sensor here** +places a tap at the clicked pipe location. +The saved **Automatic air supply / logic bench** runs pressure feedback through +hysteresis, a master switch and an AND gate to a compressor drive command. Choose +**Logic** to edit its graph or **Split** to view signals beside the 3D machine. +The node library supports search, typed click/drag wiring and live controls; +Boolean wires are red, Number green, with ring inputs and filled outputs. + +ECUs now offer **Crank ignition**, **Alpha-N EFI** and **Speed-density EFI** presets alongside the +labeled legacy controller. Their grouped Logic connectors require physical crank +speed/position, plus actual TPS for Alpha-N or absolute MAP/IAT for speed-density. +Intake chambers accept explicit fuel +injector attachments with calibrated capacity and finite/ideal rail supply. +Try `machines/sensor-ignition-bench.machine.toml` and +`machines/sensor-alpha-n-bench.machine.toml` and +`machines/sensor-speed-density-bench.machine.toml`; all have working fuel transport +and combustion. See the [workshop guide](docs/MACHINE_BUILDER.md) for calibration +and the current batch-delivery model. + +Launch directly with `fluidsim_desktop --builder`, edit a saved design with +`--machine machines/vacuum-pump.machine.toml`, or run it with +`--play-machine machines/vacuum-pump.machine.toml`. + Both platforms use CMake 3.28 or newer, Git, and a C++20 compiler. The presets build with optimizations and debugging symbols (`RelWithDebInfo`). The first desktop configure fetches SFML 3.0.2 if it is not installed and needs network @@ -166,12 +214,16 @@ in each engine TOML's `[simulation] cells_per_meter`. The desktop engine editor has been removed; edit engine calibration in TOML. -Launching `fluidsim_desktop` without `--scenario` opens a small scenario menu. -Choose the twin, YZ125, Saab B205, or American V8 card, or select with Up/Down -and press Enter. +Launching `fluidsim_desktop` opens a maximized home screen with **Machine +Workshop**, **Engine Scenarios**, and **Quit**. Workshop offers a new design or +saved machines with separate Edit/Play actions. Engine Scenarios opens the engine +collection with category filters, search, a preview and a Start button. Click a +row to select it, then start with the button or Enter; Up/Down also selects rows. Escape returns from the scenario to the menu, stopping its simulation and -audio so another scenario can be selected. Escape closes the menu at the category -screen. Simulation and audio start after selection. Passing +audio so another scenario can be selected. Escape returns from a browser to the +home screen, and closes the app from Home. Menu, workshop and scenario entry all +maximize the decorated Windows window, retaining its normal restore controls. +Simulation and audio start when an engine is launched. Passing `--scenario twin`, `--scenario two-stroke`, `--scenario b205`, or `--scenario v8` bypasses the menu; `--scene` and `--exhaust` options also apply when choosing through the menu. @@ -556,10 +608,10 @@ for units, directory discovery, sections and pipe topology ordering. ### Engine families and inspired builds -The browser opens at engine categories; click a category to see variants, then -click a variant or press Enter to start. Backspace, Left, Escape or the -`< Categories` header returns to categories. Both levels support wheel and -keyboard scrolling. +Choose **Engine Scenarios** on the home screen. Categories are listed beside the +engine collection; wheel over them to reach more families. Select a variant, +then press Enter or **Start engine**. Ctrl+F focuses search. Backspace, Escape or +**Back** returns to the main menu. Lists support wheel and keyboard scrolling. `EngineConfig` supplies cylinder count (up to 12), inline/V/boxer layout, bank angle, bore/stroke/rod, compression, firing phases, rev limit and fueling. diff --git a/docs/COMPONENT_IMPLEMENTATION.md b/docs/COMPONENT_IMPLEMENTATION.md new file mode 100644 index 0000000..b7b31d3 --- /dev/null +++ b/docs/COMPONENT_IMPLEMENTATION.md @@ -0,0 +1,354 @@ +# Component implementation progress + +Active goal started 2026-11-01: implement the full +[machine systems roadmap](MECHANICAL_BUILDER_PLAN.md), in its integrated order. +The scope includes the later components; they have not been dropped because they +need additional physical models. Preserve existing working-tree changes and +user-edited machine files. + +## First implementation: typed port foundation + +Implemented: + +- Shared descriptors for Gas, Rotational, Translational, Boolean, Number and + legacy ECU assignment. Direction, cardinality, quantity, pressure reference, + required/default values and numeric ranges are explicit. +- Typed Boolean/Number values with validity and simulation timestamps. Finite + numeric checks and explicit type matching prevent implicit truth conversions. +- A shared compatibility function used by connection previews, edits and + document validation. Runtime compilation validates the same document. +- Explicit gas-domain filtering in pipe geometry updates, fluid compilation and + fuel-source searches, avoiding assumptions that every non-ECU link carries gas. +- Version 5 stable port/method names, custom physical-port identities and loading + of versions 1 through 4. Zero-storage junctions expose one shared port; old + numeric aliases migrate to it. +- Shared UI port colors and silhouettes: red Boolean, green Number, input rings, + filled outputs, neutral bidirectional gas connectors; legacy assignments blue. + +The foundational types are real APIs. Boolean/Number nodes remain subsequent work; +the first mechanical parts are implemented below. + +## Mechanical parts milestone + +Implemented Shaft / crankshaft, Flywheel, Drive motor and Brake / load with typed +gold attachment ports, real runtime behavior, 3D meshes, Mech picker category, +inspectors, live motor/load/friction tuning and named rotational instruments. +Shaft inertia and initial states require Build. Effective flywheel inertia appears +in the shaft inspector. Cylinders and pumps use their attached shaft; each ECU's +cylinders must share one shaft. Moving a shaft leaves gas geometry unchanged. + +The independent-shaft solver tracks kinetic energy, source/drive work and losses. +Its drag integration splits stops and reversals without allowing brakes to create +motion. Ideal speed drives report their supplied/absorbed work, including startup. +Engine-connected shafts currently enforce forward-only timing. The legacy starter +remains explicit shaft calibration until the dedicated starter milestone; new +shafts default to zero starter torque. + +Version 6 saves mechanical parameters/connections. Older documents migrate to +visible shafts/drives/loads and keep legacy gauge definitions. Examples/imports, +copy/paste, assembly references, live tuning and benchmark tools use explicit +mechanics. Existing user files are not rewritten on load. The new +`machines/shaft-drive-bench.machine.toml` is a ready-to-run mechanical example. + +Validation: nine focused suites pass, including analytic acceleration and damping, +work/energy accounting, brakes/static hold/reversal, two independent engines at +1200/2400 RPM, live tuning one to 3600 RPM, required attachments, mixed-shaft ECU +rejection, migration, save/load, builder/menu interactions and prior physics cases. +Rendered review covers every mechanical inspector and a running bench at +1920x1080 and 1280x720. This is not a global gas-plus-mechanics conservation study; +the existing explicit component-coupling timestep limitations remain relevant. + +## Sensor attachment milestone + +Implemented six ideal sensor assemblies: absolute pressure / MAP, differential +pressure, gas temperature / IAT, signed total pipe mass flow, crank speed/position +and throttle position. They attach to existing physical hosts without consuming +gas ports, adding storage or changing the fluid mesh. Pipe taps use normalized +arclength. Total mass flow is explicitly distinct from the future fresh-air MAF +sensor; gas temperature is not a head/coolant temperature model. + +Sensor outputs use the typed port descriptors, sample timestamps and validity. +Sampling occurs initially and after each audio sample's physics, providing the +next signal-graph boundary's readings. Crank speed, wrapped angle and Boolean +ideal synchronized status share a sample; shaft identity and unwrapped angle are +retained for future timing consumers. A stopped shaft supplies valid zero speed. +Sensor channels are independent of the eight recorded instrument channels. + +Version 7 saves stable sensor identities, named kinds, host labels, normalized +locations and pickup offsets, and preserves deleted-ID counters. Versions 1..6 +still load. Host deletion leaves a visible unresolved entity; copy/paste and +assemblies remap internal hosts and retain unresolved external bindings. + +The **Sense** inspector creates supported sensors, binds hosts/reference taps, +adjusts tap positions and pickup offsets, focuses/removes attachments and shows +live typed outputs. Right-click **Add sensor here** uses the clicked pipe location. +3D markers select their sensor; unresolved-binding checks navigate to it. +Attachments are Build-only and remain inspectable while running/paused. +The new `machines/sensor-bench.machine.toml` demonstrates all six assemblies. + +Validation covers sampling/units/signs, coherent timing and reverse crossings, +stopped pickups, remeshing/transforms, no fluid/audio perturbation, more than eight +sensor channels, host deletion, incompatible bindings, copy/paste and save/load, +plus UI placement, undo/redo, diagnostics and Test edit locks. Rendered review +covers pressure, crank and differential-pressure screens at 1920x1080/1280x720. +All ten focused regression suites pass, as does migration/round-trip validation +of the saved machines found in the workspace. The desktop executable is rebuilt. + +## Logic runtime milestone + +The document now owns a separate typed signal graph with stable node/pin keys, +saved canvas coordinates, shared port compatibility and atomic connection edits. +Version 8 saves that graph; versions 1..7 still load. Physical gas/mechanical +links and legacy blue ECU assignments retain their separate meanings. + +Implemented 35 runtime node kinds: Boolean constants, switches/buttons, numeric +constants/sliders/inputs, six Boolean gates with configurable fan-in, comparisons, +windows/hysteresis, arithmetic, clamp/gain/map, number selection, Bool-to-number, +sensor references, command references, lamps/readouts, and Boolean/Number delays. +Polymorphic numeric nodes currently use explicit configured quantities/references. +Multiply/divide use a scalar second operand; pressure subtraction produces a +differential, while absolute-pressure addition requires a differential offset. + +Graph evaluation occurs before each physics sample, consuming the previous +boundary's sensor sample and latching commands across gas CFL substeps. Evaluation +uses a compiled order, rejects combinational cycles with a named cycle path and +commits explicit memory next states together. Required unwired inputs block Test; +commands have declared inactive defaults. Invalid math/sensors stay invalid with +runtime diagnostics, and actuator consumers apply inactive fallbacks. Short button +press/release sequences survive between boundaries; fresh Test resets transient +controls and memory. Constants/thresholds can be tuned without restarting physics. + +Commands own individual motor enable/speed/torque, brake enable, throttle opening +or legacy shaft starter fields. Duplicate ownership and ECU/opening conflicts are +rejected; dormant signal-owned fields start inactive. A shaft allows only one +potentially enabled ideal speed drive. The physical inspector identifies signal +ownership and the live drive HUD uses the effective enabled state. + +Connected harnesses follow physical assembly copy/paste. Internal parts/sensors/ +nodes remap independently; omitted physical bindings remain explicitly unresolved. +The new `machines/logic-compressor-bench.machine.toml` runs actual pressure -> +below-threshold hysteresis -> master-enable AND -> compressor motor enable, with +a speed slider, targeted readouts and recorded pressure/RPM/drive-power channels. + +Core acceptance covers gate truth tables, unit/reference rules, fan-out/cardinality, +unknown keys/types, ordering/cycles, delayed feedback, math faults, manual event +ordering/reset, live tuning, command ownership/fallbacks, automatic compressor +shutdown, brake/starter commands, sample-vs-CFL timing, saved harnesses and +independent copied compressors. Twelve focused regression suites pass, along +with workspace save migration checks. Rendered review covers signal-owned drive +controls and the running pressure bench at 1920x1080 and 1280x720. The desktop is +rebuilt at this runtime milestone. Timers, edge nodes and latches remain later work. + +## Logic editor milestone + +Implemented **3D / Logic / Split** workspace tabs with one document, selection +and Test runtime. Canvas pan/zoom and the 3D camera remain independent. Selecting +a sensor/command reference selects its physical host; **Focus in 3D** opens the +split workspace. **Sense -> Show Logic** and physical **Command** buttons create +or reveal references to existing entities without duplicating physical parts. + +The node library has search, categories, an Objects list and compatible-node +filtering while a pin is pending. New compatible numeric nodes adopt that pin's +quantity/reference. Nodes drag by their headers, Shift/box selection supports +groups, Home/F fit the graph/selection, and wheel zoom preserves the point under +the pointer. Saved node positions are independent of 3D geometry. + +Connections support click/click, dragging, fan-out, input rewiring, right-click +pin disconnection and undo/redo. Rejected edits keep the old wiring and redo +history. Boolean pins/wires are red, Number green; inputs are rings and outputs +filled circles. Live values include units, required/default metadata, ranges, +pressure references and validity. Candidate combinational cycles highlight the +actual cycle nodes and retain the compiler's named path diagnostic. + +Inspectors edit names, quantities/references, sensor/command bindings and node +parameters. Invalid numeric text remains editable. Topology, control ranges, +initial states and physical bindings require Build; safe constants/thresholds +and canvas layout can change during Test. Switches, momentary buttons, sliders +and numeric controls operate at simulation boundaries. Paused controls show queued +values separately from frozen pins; slider motion coalesces. Focus loss, leaving +Logic and stopping Test release momentary buttons. Numeric readouts have compact +UI-sampled trends; the recorded meter channels remain separate. + +Graph copy/paste preserves internal wires. Same-document sensor references share +their existing sensor; cross-document references detach while retaining labels. +Pasted command nodes require a new explicit target, preventing competing owners. +Physical assembly copy/paste continues to remap complete internal harnesses. + +Validation: thirteen focused regression suites pass. Interaction cases cover +placement/search, typed wiring/rewiring, failed-edit rollback/redo, one-entry drag +undo, zoom anchoring, live tuning, paused controls, focus/view-close button release, +shared physical selection, split-view bounds and uninterrupted simulation time. +Rendered review covers 1920x1080/1280x720, including live graphs, split view and the +threshold inspector. Pixel checks confirm red/green filled outputs and hollow +input centers. Updated rendering libraries are linked into +`out/build/windows-gcc/fluidsim_desktop_preview.exe`; the canonical executable is +currently locked by the user's running workshop, which was left intact. + +Further graph/UI work remains on the roadmap: pinned operator panels, richer +binding pickers/property help, visual wire waypoints and graph-wide automatic +type inference, in addition to DPI scaling and visibility groups. Basic stage 2 +is usable; those refinements and the later component stages are not dropped. + +## Sensor ECU and fuel-delivery milestone + +Implemented the runtime primitives for the next component stage: + +- Physical crank outputs now carry pickup/shaft identity, original acquisition + time, unwrapped angle and measured speed through the signal graph. Unchanged + routing retains that evidence; arithmetic creates ordinary numeric values. + Delays retain their original acquisition time, so retimestamping a delayed + value cannot make it a current timing measurement. +- A crank-only wasted-spark clock validates matching current speed/angle channels, + counts unwrapped crossings, accepts stopped pickups and rejects stale/mixed or + reverse timing. Acquisition/recovery does not replay missed sparks. Changing + advance cannot fire twice for one revolution. +- Separate sensor ECU core with ignition-only and Alpha-N strategies. Ignition + uses measured RPM and an advance map; Alpha-N requires measured TPS and uses a + calibrated two-dimensional RPM/TPS table in kg per cylinder per four-stroke + cycle. It never receives trapped cylinder air mass or global pedal demand. + Required sensor faults cut outputs; the RPM limiter has hysteresis. +- Explicit injector delivery core with capacity limiting, declared ideal or + finite regulated rail supply, depletion and fail-closed command handling. + Delivery adds conserved fuel species, fuel-origin tracer and sensible energy + to an existing gas volume. This is continuous/batch-average delivery, not + sequential pulse-width control or a liquid-pressure circuit. +- Four-stroke cylinders accept consumed external spark pulses and cut commands. + This path requires transported fuel and does not fall back to hidden internal + spark timing. Burn progression follows the received event. + +The cores are now integrated into saved machines and the workshop. The ECU +inspector selects labeled Legacy ideal, Crank ignition or Alpha-N EFI presets. +**Show ECU connector in Logic** creates/reveals one grouped connector. Crank +speed/position are required green Number inputs; Alpha-N additionally requires +actual TPS. The connector selects its physical ECU and copied graph-only +connectors detach ownership. Physical assembly copies remap the complete harness. + +Blue assignments declare controlled cylinders. Sensor-driven ECUs do not own +throttle opening or read the global pedal; manual or separately commanded +throttles remain available. Each cylinder has an explicit **Pickup TDC deg** +calibration in the crank sensor's coordinate system. Its physical 720-degree +phase is not used as hidden ECU synchronization. Commands and spark events use +the existing simulation-sample boundaries, not UI updates. + +An explicit injector attachment can be added to a chamber, bound to an Alpha-N +ECU and configured with capacity, cylinder-dose count, fuel mass/temperature and +finite or ideal regulated supply. A nozzle appears on its host. Total injector +cylinder doses must match assigned cylinders; fuel is added once per physics +sample and then transported by the gas solver. Live inspectors show delivered +flow, remaining fuel, depletion and ECU fault/limiter status. Ignition maps and +RPM limits can be tuned live; strategy, fuel tables, attachments and pickup +calibration require Build. + +Version 9 saves ECU strategy/fuel maps, grouped input schemas, pickup calibration +and injector bindings. Versions 1..8 retain their legacy controller behavior. +Deleted/omitted ECU references stay unresolved with labels; IDs remap on assembly +insertion. Existing user files are not rewritten. Ready-to-run examples are +`machines/sensor-ignition-bench.machine.toml` and +`machines/sensor-alpha-n-bench.machine.toml`, both driven at 3000 RPM. + +The delivered EFI model is open-loop, continuous/batch-average intake delivery. +It does not claim sequential pulse timing, modeled liquid rail pressure or +evaporation/wall-film dynamics. Those abstractions remain explicitly labeled; +advanced sensor strategies remain in later stages. + +The new sensor-ECU acceptance target covers physical sensor -> graph timing, +stale/mixed/arithmetic rejection, delayed acquisition evidence, spark crossings, +RPM/TPS interpolation, fault/recovery and limiter behavior, ECU demand -> gas-host +fuel delivery, finite-supply mass/energy accounting and external spark actuation. +Acceptance also covers actual combustion in both sensor-driven examples, +live loss/recovery of timing, foreign-shaft and constant-number rejection, +depletion in the machine runtime, live ignition tuning without restarting time, +save/load, partial/full assemblies and UI preset/connector/injector controls. +All fourteen focused suites pass after rebuilding affected targets. Workspace +machine migration/round trips pass from the repository root. Rendered review +covers the new graph, ECU and injector screens at 1920x1080 and 1280x720. +Wasted exhaust sparks retain the last trapped-charge AFR reading; valid timing +still samples fresh charge when ignition is cut by the limiter. + +The Alpha-N example was opened in Build mode with +`out/build/windows-gcc/fluidsim_desktop_preview.exe`, leaving the original +workshop intact. The final checked binary, including the last charge-instrument +fix, is `out/build/windows-gcc/fluidsim_desktop_ecu_preview.exe`. Open sessions +were not restarted to replace a locked executable; recheck live processes before +linking or launching the next review build. + +## Speed-density ECU milestone (stage 4, first slice) + +Added a third sensor-driven preset, **Speed-density EFI**. Its grouped connector +requires four Number inputs from three physical sensor assemblies: crank speed, +crank position, manifold **absolute** pressure and intake gas temperature. TPS is +not required in this basic mode. Gauge/differential pressure cannot connect to +MAP; invalid, zero or stale MAP/IAT values cut fuel and new ignition events. + +Fuel demand uses a declared dry ideal-gas estimate: +`air_kg_per_cylinder_cycle = MAP * displacement * VE / (R_air * IAT)`, followed +by target AFR and the four-stroke RPM/120 conversion. Displacement, air gas +constant, target AFR and the RPM/MAP volumetric-efficiency table are calibration, +not hidden cylinder measurements. VE interpolates both axes and clamps to table +edges. The gas network still transports the explicitly injected fuel. This +open-loop estimate does not measure fresh-air fraction in diluted intake gas or +provide warm-up, acceleration enrichment, lambda trim or sequential timing. + +The preset, required input schema, calibration fields and EFI injector target +picker are integrated into the workshop. **SD** calibration fields display cc, +kPa and VE percent and require Build. Ignition-map/RPM-limit tuning remains live. +Shared strategy names/keys validate the document, graph, runtime and serializer; +unknown strategies fail explicitly. Format 10 saves the new VE/displacement +calibration and connector schema; versions 1..9 still load. + +`machines/sensor-speed-density-bench.machine.toml` is a working 3000 RPM example +with physical intake MAP/IAT taps, an explicit injector and actual combustion. +Its calibration is set when constructing the example; runtime fuel estimation +does not query cylinder air mass or geometry. + +Validation: fifteen focused suites pass, including ideal-gas charge cases, +pressure/temperature/displacement scaling, two-axis VE interpolation, required +sensor faults, live MAP loss/recovery with combustion, missing IAT host, input +reference types, save/load, assembly remapping and UI preset/connector controls. +Workspace machine migration/round trips pass from the repository root. Rendered +review at 1920x1080 and 1280x720 covers the four-input graph and ECU/injector +inspectors. Pneumatic valves and the pressure-switch supply circuit remain the +next stage-4 delivery; they are not implemented by this ECU milestone. + +Checked desktop build: `out/build/windows-gcc/fluidsim_desktop_speed_density_preview.exe`. +Existing workshop windows were left intact. Next session should start with the +physical valve slice below; no valve implementation is currently in progress. + +### Next physical component slice + +Implement needle, solenoid shutoff, differential relief and non-relieving +pressure-regulator behavior through the existing conservative gas boundaries. +Use explicit inlet/outlet paths and real body/downstream pressure; do not enforce +a setpoint by assigning gas pressure. Needle opening and solenoid coils need +typed signal command ownership; the regulator's pressure demand must declare an +absolute/gauge reference. Relief needs cracking/reseat hysteresis, solenoids need +normally-open/closed rest states and finite travel, and regulator response must +remain bounded when supply falls below demand. Include a pressure-switch preset +and a finite-tank circuit with flow, depletion and conservation acceptance cases. +Integrate picker modes, inspectors, save/load and assemblies before further UI +polishing. Keep this and all later roadmap stages in the active goal. + +## Delivery backlog + +| Stage | Scope | State | +| --- | --- | --- | +| 0 | Typed ports, validation, stable identities and migration | Complete | +| 1 | Explicit shafts, flywheel, drive motor, brake/load, sensor attachments and shaft instruments | Complete; six basic sensor assemblies implemented | +| 2 | Logic canvas; buttons, switches, constants/sliders; gates, thresholds, arithmetic, mapping, sensors, commands and displays | Basic runtime and editor integrated/verified; advanced graph UX and operator panels remain | +| 3 | Crank sensing, ignition and Alpha-N ECU presets, explicit fuel delivery and ECU connector UI | Basic presets integrated/verified, including physical combustion, saved bindings and workshop controls | +| 4 | MAP/IAT sensing, speed-density ECU, needle/shutoff/regulator/relief valves and pressure-switch control | Speed-density preset integrated/verified; pneumatic valves and pressure-switch circuit remain | +| 5 | Gears, clutch, starter; directional valves, pneumatic cylinders, linear loads/springs/dampers and quick exhaust; latches/timers/memory | Planned | +| 6 | MAF/sequential ECU options; air motors, rotary compressors, filters, nozzles and resonator/silencer assemblies | Planned | +| Later | Belt/pulley drives, heated/cooled chambers, ejectors, turbines/turbochargers and general linkages | Planned | + +Each stage needs actual runtime behavior, save/load and assembly support, usable +UI controls and focused acceptance checks. Keep this file current at milestones. + +Stage 0 validation: six focused suites pass (machine ports, machine physics and +serialization, builder interactions, menu navigation, native maximization and +desktop UI). Tests cover all current part descriptors, signal types/references, +invalid values/defaults, input direction/cardinality, custom-port reordering, +legacy ECU fan-out, old junction aliases and all saved machines found locally. +Unknown part kinds now fail explicitly instead of inheriting gas ports/storage. +Source whitespace checks pass. Existing user machine files were read and copied +to temporary test locations; they were not migrated in place. diff --git a/docs/MACHINE_BUILDER.md b/docs/MACHINE_BUILDER.md new file mode 100644 index 0000000..0b3fb41 --- /dev/null +++ b/docs/MACHINE_BUILDER.md @@ -0,0 +1,620 @@ +# 3D machine builder + +Open **Machine Workshop** from the maximized main menu. Choose **New machine**, or +select a saved machine and choose **Edit** or **Play**. Pressing **B** in the +scenario browser opens a new design directly. + +The builder supports general pneumatic circuits as well as engines. The two +example benches provide a four-stroke engine and a piston vacuum pump. The +parts picker uses cached 3D previews of the same meshes that appear in the world. + +**Import engine layout** (I) transfers the existing 450 single, parallel twin, +flat six and V8 H-pipe layouts into new editable documents. The importer preserves +pipe routes and diameters, storage dimensions, bore/stroke, firing phases, cam +profiles, valve dynamics, shaft calibration, fuel calibration and ECU settings. +It inserts a visible carburetor or throttle into the final inlet segment and adds +the appropriate ECU with control wires. The metering body and stub replace part +of that inlet segment, preserving its total route length and gas volume. + +## Build and configure + +The workshop uses a dark editor layout with resizable, collapsible Library and +Inspector panels. Drag their dividers to resize; widths and collapse states are +saved locally. **Reset UI** restores the default panel layout. The **Library** +offers categories, search and part previews; **Objects** lists the current parts. +**Ctrl+F** focuses search. Long control names fit their available width and reveal +their full text on hover. Property labels sit above their editable values. + +The inspector tabs are **Part**, **Links**, **Setup**, **Checks**, **Meters** and **Sense**. +The oscilloscope has a separate bottom dock; drag its upper edge to resize it. +The minimum window size is 1280x720, with the layout reviewed at 1920x1080 too. +Entering the workshop maximizes the Windows window; the application no longer +shrinks when moving between the workshop, home menu and engine scenarios. + +1. Pick a part on the left and click the world to place it. Placement uses the + horizontal plane set by **Machine → Place Y mm**. Hold Shift for repeated placement. +2. Select a placed part to open its properties. Right click a part for its context + menu. Click the part name to rename it. +3. **G** selects Move. Drag a colored X, Y, or Z handle to translate in world axes, + or drag a selected body to move in the screen plane. + **R** selects Rotate; drag a colored ring to rotate about that world axis. + **X** toggles snapping. Translation uses the configured grid; rotation snaps to + 15 degrees. Numeric position and rotation fields also allow precise changes. +4. Click a numeric property, type its value and press Enter. Escape cancels typing. + The minus/plus buttons make incremental changes. Scroll over the inspector to + reach additional properties. +5. Select a part to reveal its ports, then drag between ports or click a port + followed by its target. **C** optionally shows all ports. Neutral markers are + bidirectional gas ports; at least one end of a gas connection must be a pipe. + The pipe endpoint snaps to the other part. Blue pins connect existing ECU + assignment wires; ECU outputs are filled and inputs hollow. The **Links** tab + configures or disconnects connections. Use the Move gizmo to move a small + boundary symbol whose port occupies its center. **Escape** cancels a wire; + **G** returns to Move. Route mode prioritizes editing pipe points. + After choosing the first port, hover the target: a blue preview shows the + snapped pipe route or control wire; a red preview explains why it cannot connect. + Previewing does not change the design. Clicking an invalid target leaves the + first port selected so you can choose another target. +6. **P** selects Route for a selected pipe. Use **Add path point**, select a point, + then drag its X/Y/Z handles. The inspector edits the selected point's local + coordinates. Disconnect an attached endpoint before moving it independently. + Select a numbered point and edit **Point dia mm** to change its bore. Every + point has its own diameter; adjacent values form a linear taper along that + segment. This replaces the former inlet/outlet diameter fields. + **Ctrl-click** near the selected pipe's centerline to insert a bend at that + location, or right click a pipe and choose **Insert bend here**. The new point + starts on the original route, preserving length and gas volume until moved. + Length, the selected point's bore and volume are shown above the viewport while editing. + +Alt+left drag or right drag on empty space orbits the view. Middle drag pans; +the wheel zooms; Home fits the design and **F** focuses the selected part or group. +Q selects without a transform tool. +Shift-click adds/removes parts from a selection. Shift-drag on empty space selects +a group; Ctrl+A selects the whole machine. Move and Rotate operate on that group. +Ctrl+C copies the selected parts, their internal air/control connections and +attached probes; Ctrl+V inserts a translated copy. Ctrl+D duplicates immediately. +Connections to parts outside the copied group are not copied. Delete removes the +selection. Ctrl+Z/Ctrl+Y undo/redo. The copied group remains available when loading +another machine during the same workshop session. Right-clicking a selected part +keeps the group selected and opens Copy, Paste, Duplicate and Save as assembly. + +Click **Assemblies** to save the selected group for reuse or insert a saved +assembly into the current design. Assemblies are stored in +`machines/assemblies/` as `*.assembly.toml` files and can be copied between +workspaces. Their internal connections, component settings, pipe profiles and +attached probes are preserved. On insertion, part and probe IDs are remapped and +the group is placed near the current selection (or the view center), selected and +focused for positioning. The destination machine's shaft and simulation settings +apply to the inserted components. Shaft gauges remain specific to each machine. + +Escape cancels a placement/connection or clears selection; it never leaves the +workshop. Use **Menu** to return to the scenario menu, or close the application +window to exit. + +In Move mode, grab an atmosphere symbol directly to select and drag it, or use +its X/Y/Z handles. Its attached pipe endpoint follows the move. Atmosphere +symbols remain selectable at the end of a wide pipe. + +## Machine checks + +Press **H** or click **Checks** to open the checks panel in Build or Test. Warnings +identify cylinders without a connected fuel source, ECUs without assigned parts, +and fixed throttles that are closed without an assigned ECU. Notes identify +sealed pipe/cylinder ports and remind you to crank a stopped free shaft. + +Click a check to select and focus its part. Use the Part or Connections tabs to +configure it. Wheel or Previous/Next browses longer lists. Checks refresh after +edits, undo and redo; they do not change or prevent testing a design. A sealed +port can be intentional. Fuel checks establish wiring/plumbing reachability; +they do not guarantee flow through valves, correct calibration or combustion. +Unresolved sensor checks open the affected attachment in **Sense**. + +Ideal junctions support multiple pipes on the same point. Connect free pipe ends +to the junction's center marker. Connecting a third pipe to an occupied +pipe-to-pipe join automatically replaces that join with a shared node. Switching +to stored-volume mode gives branches distinct ports on the visible box; switching +back to an ideal node brings them to its center. + +## Physical dimensions + +There is one design geometry shared by rendering, picking and simulation: + +- **Pipes and reducers:** centerline segment lengths determine the simulated + length. Each route point defines a diameter. Diameter varies linearly between + adjacent points, allowing expansions, necks and reducers within one pipe. The + finite-volume mesh integrates the exact frustum volumes across each cell, + including cells that cross multiple profile segments. This preserves gas + volume across mesh densities. Bends stay at their edited positions. + Visual route points do not force extra simulation cells: the physics mesh follows + total arclength at the selected cell density. A genuinely short pipe remains short + and can require more CFL substeps. + Sharp changes in area also constrain the timestep by the ratio between face + area and the gas volume stored in a cell. +- **Chambers and storage junctions:** the visible internal box dimensions define + capacity as X × Y × Z. Rotation changes the port locations and appearance, + and preserves volume. +- **Cylinders and piston pumps:** bore, stroke and compression ratio define the + gas cavity and clearance. The rendered piston and current simulated volume + use the same displacement. Engine cylinders also expose valve diameters/counts, + cam timing/lift, valve mass, spring, damping, preload, AFR and spark advance. +- **Throttle and check valve bodies:** throttles use their visible elliptical + tube dimensions for gas storage; check valves use their box dimensions. + Diameter and discharge coefficient define the restriction. Throttles + can use the test pedal or a fixed opening. Check valves pass flow from their + local -X inlet to +X outlet above the configured cracking pressure. +- **Ideal junctions:** characteristic and legacy modes have zero stored volume + and are drawn as node symbols. The storage mode is drawn as a physical box. +- **Carburetor:** the elliptical throat stores gas according to its length and + Y/Z diameters. The restriction diameter controls flow. Pilot/main/needle jets + meter liquid fuel from the float bowl into that gas; the solver transports it + through pipes into the cylinders. Jets, needle corrections, fuel density, bowl + capacity/refill, fuel film and discharge settings are editable. +- **ECU:** a controller housing with no gas storage. Blue wires assign injection + and ignition to cylinders, and throttle control to a throttle or carburetor. + One ECU can control multiple parts. An unconnected ECU controls none. +- **Pressure/atmosphere and end-cap symbols:** these represent boundary conditions + and have no storage volume. A pressure boundary is an infinite reservoir with + the configured pressure and temperature; an end cap reflects flow. + +Moving, rotating or resizing a connected component updates the attached pipe +endpoints. Its pipes therefore change length when their routes change. There is +no pressure-driven visual expansion in this editor. + +The flow equations remain one dimensional in pipes and lumped in chambers. +The 3D layout supplies physical sizes and junction directions; this is not a 3D +CFD mesh. Valve restrictions and zero-volume fittings use the existing boundary +models. Ideal pressure-actuated check valves have no moving reed mechanics. + +## Test and return to editing + +Press **Tab** or **Test** to compile the design and run it with audio and pressure +colors. Select a part to read its current pressure and volume. W/S or the pedal +bar changes throttle; hold V for starter torque on a free shaft. Space pauses +or resumes both physics and audio. Test offers **1x, 0.5x, 0.25x, 0.1x and 0.01x** +playback; Page Down halves the speed and Page Up doubles it (1% to 100%). +**Step 10 ms** advances a paused machine by exactly 0.01 simulated seconds. +The physics timestep and engine calibration stay the same at every playback speed; +audio pitch slows with playback. Buffered audio scales with speed to keep controls +responsive. The four existing pipe solvers and five audio +environments are selectable under **Machine** before starting a test. + +New engine benches use a free shaft at zero initial RPM. Hold **Hold to start** +or V to crank. A cylinder only receives fuel from an explicitly assigned injection +ECU or from fuel physically delivered through a connected carburetor. There is no +implicit cylinder injector. A carburetor can run with mechanical ignition; an +optional ignition/idle ECU can control spark and opening. + +For EFI, connect the ECU's blue output to each cylinder's blue input. Connect +that output to the throttle's blue input for idle/throttle control. ECU settings +include idle target/opening limits, PI gains, ignition-map points, cruise/power +AFR, throttle response, overrun cuts, rev limit, resume RPM and minimum cut time. +Fuel and spark cuts are independent. A cut lasts at least its configured time and +resumes only below the resume RPM. Carburetor ECUs use **Ignition / idle only** +so they do not add an invisible second fuel source. + +The **Mech** category contains **Shaft / crankshaft**, **Flywheel**, **Drive motor** +and **Brake / load**. Connect their gold mechanical ports. Cylinders and piston +pumps also have a gold shaft port; each must attach to a named shaft before Test. +Moving a shaft changes its attachment lines without moving its cylinders or pipes. + +Each shaft has independent angle, initial speed, base inertia and friction. +Flywheels add inertia once; the shaft inspector shows the connected total. +Motors can apply signed torque or impose an ideal target speed. The ideal speed +drive reports the work it supplies or absorbs, including speed changes. A brake +applies opposing torque and drag, dissipates work and cannot drive a stopped shaft +backward. Pure mechanical machines can run without any gas pipes. + +Use **Setup** to choose the primary readout shaft, or pin it from a shaft's +inspector. Each ECU's controlled cylinders must share a shaft. Engine timing is +currently forward-only; standalone mechanical shafts support reverse rotation. +The legacy starter calibration remains on migrated shafts until explicit starter +parts are added. New shafts have no hidden starter torque by default. + +The saved **Shaft, flywheel and brake bench** demonstrates acceleration, motor +power and absorbed load power. Its file is `machines/shaft-drive-bench.machine.toml`. + +The overlay distinguishes **SPEED CONTROLLED**, **TORQUE DRIVEN**, **FREE SHAFT**, **FUEL OR SPARK CUT**, +**VALVE FLOAT**, and **NO FUEL SOURCE**. The model has no fixed 16,000 RPM cap. +With limiting disabled, controlled checks ran at 16,000, 20,000 and 24,000 RPM +without positivity repairs. The unloaded bench settled around 17,500 RPM as net +torque fell toward zero. This depends on geometry, valve dynamics, fuel/spark +calibration and losses; increasing a rev limit does not guarantee engine torque +at that speed. + +Unconnected gas ports on pipes and cylinders are sealed; the status line reports +their count. Unused tank faces are simply walls. Unconnected mechanical attachments +receive a navigable check and prevent Test. +Invalid dimensions, duplicate connections and physically separated connected +ports are rejected. Numerical failures stop the test and report an error. + +Press **Tab** again to stop playback and return to the same design and camera. +Starting another test initializes a fresh gas and mechanical state. + +## Live tuning + +Properties are editable during Test unless marked **BUILD** and dimmed. Live changes +are validated and synchronized between physics batches, preserving gas state, +shaft state, simulation time, carburetor fuel film and controller history. Carb +jets, needle corrections, fuel settings, ECU calibration, fixed throttle opening, +check-valve cracking pressure, cylinder combustion settings, shaft losses, +motor torque/target speed and brake loads can be adjusted live. Live edits support undo/redo and saving, +and remain in the design when returning to Build. + +Pipe routes/diameters, transforms, part dimensions, bore/stroke, cam profiles and +valve mechanics, connections, initial gas conditions, solver/mesh settings and +compiled boundary coefficients, initial shaft states and shaft/flywheel inertia +require Build. A live undo that would change one +of these properties is rejected without changing the running machine. + +## Instruments and traces + +Press **M** or **Meters**. Select a part, then choose **Pressure**, **Temp**, **Flow**, +**AFR**, **Lambda** or **Charge AFR**. **RPM**, **Torque** and **Power** target the +selected shaft or attached rotational part; without a compatible selection they +use the declared primary shaft. Up to eight channels can be attached. +Probes add no gas volume or restriction. Colored P1/P2 markers locate probes in +the world; clicking one in Select or Test opens its instrument settings. +Use **< Probe / Probe >** to choose the channel plotted on the scope. + +Pipe taps use a fraction of arclength from Start to End, adjustable in the panel. +Measurements use the cell containing the tap, so spatial detail follows mesh +resolution. Lumped-part flow is net flow into the part; pipe flow is signed +Start-to-End. Pressure is absolute kPa, temperature K, flow g/s, torque Nm and +power kW. Motor gauges report drive torque/work, brakes report signed reaction +torque and positive absorbed power, and new shaft gauges report net acceleration +torque and kinetic power. Migrated legacy output gauges retain their running +averages of engine output after shaft friction, excluding bench drive and load; +the inspector identifies that measurement mode. + +**Bulk AFR and lambda** track fuel origin as a conserved passive tracer, including +fuel already burned. Exhaust readings therefore travel with the gas through +pipes, chambers and shared junctions and mix according to transported mass. +Put a downpipe probe after a collector to read the combined exhaust there. +The exhaust starts as clean air, so downstream readings can remain very lean +until the original air has been purged by exhaust gas. +Lambda is bulk AFR divided by the probe's stoichiometric reference (default 14.7). +Changing the reference changes the displayed ratio, not the combustion chemistry. +**Charge AFR**, available on cylinders, reads current unburned fresh-air mass +divided by remaining unburned fuel mass. It becomes undefined after that fuel is +used. No fuel is shown as **--**, rather than an invented stoichiometric reading. +These are ideal model instruments; sensor heating, response delay and detailed +oxygen chemistry are not simulated. + +The scope samples at 2,756.25 Hz in simulation time and retains the latest 32,768 +samples (about 11.9 simulated seconds). Choose a 0.05, 0.2, 1 or 5 second display +window; the graph shows min/max/mean and preserves peaks when compressing samples +onto screen pixels. Frequencies above half the sampling rate cannot be resolved. +The scope stays open while switching to Part settings, allowing live carb/ECU +tuning while watching a trace. X closes the scope. Space freezes the machine for +inspection. The last capture remains available after returning to Build. + +**Export captured CSV** writes simulation timestamps and labeled channel units to +`verification/measurements/`; undefined readings are empty cells. **Clear captured +trace** starts a new capture without resetting the machine. Adding/removing a +channel or changing a tap/reference clears the capture to keep its schema +consistent. Instruments can be configured during Test without reloading physics. + +## Saving + +Ctrl+S saves the design. A new design asks for a file name; Ctrl+Shift+S saves a +separate copy. Files use the versioned `*.machine.toml` format in `machines/` +relative to the working directory, or `FLUIDSIM_MACHINE_DIR` when set. Coordinates +and dimensional values are stored in SI units; inspector fields show their units. + +Open/Saved files refreshes the library and provides separate Edit and Play +buttons. The scenario browser discovers saved machines each time it opens. +The document is validated before saving, and the written temporary file is read +back through the same loader before replacement. Unsaved changes prompt before +opening a different design, returning to the menu or closing the window. + +Version 2 adds explicit fuel/control parts, control wires, imported port locations, +cam profiles and shaft losses. Version 1 files still load, but cylinders no longer +receive automatic fuel; add a connected carburetor or injection ECU to an older +engine design. The importer currently supports four-stroke layouts with uniform +ducts. Two-stroke crankcase/scavenge assemblies and profiled silencers are not yet +imported. Existing engine TOML files retain their separate schema. + +Version 3 stores `point_diameters_m`, one diameter in metres per `points_m` entry, +for each pipe. Version 1 and 2 pipes load with their former linear taper sampled +at each point's arclength, preserving the original shape and volume. Saving writes +version 3. Inserted points inherit the interpolated diameter; moving a point keeps +its assigned diameter. Undo/redo and duplicated assemblies retain the full profile. +Version 4 also saves instrument channels, tap positions and lambda references. +Versions 1 through 3 still load. Recorded samples are exported separately as CSV. + +Version 5 saves connection ports and methods by stable names instead of enum +indices. Custom chamber ports retain paired coordinate/key lists. Versions 1 +through 4 migrate on load, including legacy aliases of a shared ideal junction. +Saving writes version 5; existing files are not rewritten until explicitly saved. +Port descriptors now identify gas, rotational, translational, Boolean, Number +and legacy ECU-assignment domains. Only implemented parts appear in the picker; +declaring a domain does not create its future components. + +Version 6 adds explicit shafts and mechanical configurations. Loading versions 1 +through 5 creates a visible shaft and any required speed drive/load, preserves +cylinder phase offsets and retargets legacy shaft instruments. Saving writes +version 6 with named mechanical connections; no existing file is changed merely +by loading it. Copy/paste and assemblies retain internal mechanical attachments +and instrument targets; external attachments remain unresolved. + +Version 7 adds sensor attachments with stable IDs and named kinds, host/reference +bindings, normalized pipe taps and crank offsets. Part/sensor ID counters survive +save/load so deleted identities are not reused. Versions 1 through 6 still load. +Version 8 also saves a separate typed logic graph with stable node/pin keys, +parameters, references and independent canvas positions. Saving writes version 8; +loading alone does not rewrite an existing file. + +## Sensor attachments + +Open `machines/sensor-bench.machine.toml` for a ready-to-run demonstration of all +six sensor assemblies, a filling tank, fixed valve and independently driven shaft. + +Select a physical part and open **Sense**. Only supported sensor buttons are +enabled: pressure/MAP and gas temperature/IAT on gas storage, pipes and environment +boundaries; differential pressure on two such taps; signed total mass flow on a +pipe; crank speed/position on an explicit shaft; TPS on a throttle or carburetor. +Right-click a pipe and choose **Add sensor here...** to use the clicked arclength +location. Otherwise a new pipe sensor starts at 50%. Move its tap in Sense. + +For differential pressure, add the positive tap first. Select the reference +physical part, return to Sense, switch to **Reference (-) tap** and choose +**Bind reference to selected part**. The output is positive pressure minus +reference pressure, in pascals. Absolute pressure outputs use pascals absolute; +temperature uses kelvin; total mass flow uses kg/s, positive from pipe Start to +End. A volume has no unique through-flow direction, so flow sensors require a pipe. + +The ideal crank pickup supplies signed angular speed in rad/s, crank angle in +0..2pi radians and a Boolean synchronized output. The pickup's configurable +reference offset applies to its angle, while the runtime retains the shaft ID +and unwrapped crossings. Zero speed while stopped is valid. This is an ideal +pickup, without tooth acquisition or cam-phase inference. TPS reports mechanism +opening fraction, including the existing ECU throttle response, rather than +substituting pedal demand for a fixed opening. + +Sensors sample once per simulation audio sample and display sample time and +validity during Test. Number outputs have filled green dots; Boolean outputs have +filled red dots. Attached sensors introduce no gas storage or flow resistance, +and do not use any of the eight recorded meter channels. Use their Logic +references to wire sensor-driven ECUs; the labeled legacy ECU retains its +compatibility behavior. + +Click an S-numbered marker to inspect its sensor. Previous/Next browses all +attachments, including unresolved ones. To rebind, select a compatible physical +part, reopen Sense and choose **Bind host to selected part**. Deleting a host +keeps the sensor and its former host label; missing/incompatible bindings show +invalid outputs rather than ordinary zero readings. Copy/paste and assemblies +remap internal sensor hosts; omitted external references remain unresolved. +Creation, binding, pickup offsets and removal require Build and support undo/redo. + +These sensors measure gas temperature and total mass flow. Fresh-air MAF, cam +phase, lambda and material/head temperature sensors follow their own roadmap +stages and physical models. + +## Logic editor and runtime + +`machines/logic-compressor-bench.machine.toml` demonstrates an automatic air +supply: tank pressure feeds below-threshold hysteresis (120/150 kPa absolute), +an AND gate combines demand with a master switch, and a command node owns the +compressor motor's enable field. A numeric control supplies angular speed. The +saved defaults run the demonstration from Test. Choose **Logic** or **Split** +in the workspace toolbar to inspect its graph beside the physical machine. + +The Logic library offers node categories, search and an Objects list. Pick a node, +then click the canvas to place it. Drag node headers to arrange the graph; Shift +and a selection rectangle select groups. Middle drag pans, wheel zooms about the +pointer, Home fits the active view, and F focuses its selection. Switching views +keeps the camera, canvas navigation, selection and running simulation. In Split, +click the desired pane to choose its navigation/inspector context. + +Drag between compatible pins, or click a pin and then its target. Inputs are +hollow rings; outputs are filled circles. Boolean is red and Number green. One +output can fan out. Starting from a connected input rewires it atomically; +rejection leaves the original connection intact. Right-click a pin to disconnect +its wires. Escape cancels a pending connection. The library filters compatible +node kinds while a pin is pending, and a newly placed numeric node adopts that +pin's quantity/reference. Other numeric schemas are configured in the inspector. + +Select a node to edit its name, parameters, quantity and pressure reference. +Numbers use the units shown on their fields/pins (SI internally). Enter applies +a text edit; Escape cancels it. Invalid text remains editable. Hover pins for +type, direction, units, validity, supported ranges and required/default metadata. +Combinational-cycle candidates highlight their cycle nodes and report the path. +H opens diagnostics; clicking a signal warning reveals the affected node. + +Create sensors in 3D **Sense**, then choose **Show Logic** to reveal their reference. +The **Command** button on supported physical parts creates/reveals a command +reference. Its inspector chooses a command function and compatible physical +target. Selecting a sensor/command node selects that physical host; **Focus in +3D** opens Split and focuses the host. Reference nodes do not add gas storage or +duplicate a physical sensor/actuator. + +During Test, node topology, schemas, bindings, control ranges and initial memory +require Build. Safe constants, thresholds and canvas layout remain editable. +Click switch controls, hold momentary buttons, drag sliders, or type into numeric +controls on their nodes. Paused control changes queue for the next physics sample; +their control widgets identify queued values while pin values stay frozen. Momentary +buttons release on focus loss, leaving Logic, or stopping Test. Manual Test inputs +do not silently overwrite their configured initial states. Readouts show compact +UI-sampled trends; recorded meter channels and CSV export remain separate. + +Ctrl+C/V copies nodes and internal wires; Ctrl+D duplicates, Delete removes, and +Ctrl+Z/Y undo/redo. Same-document sensor copies reference the same sensor entity. +Copies pasted into another document detach external sensor references and keep +their labels. Pasted command nodes require an explicit new target so they cannot +take over an existing actuator field accidentally. Copying physical assemblies +remaps their internal sensors, command targets and connected control harnesses. + +The graph runtime supports typed controls, Boolean gates, comparisons, arithmetic, +mapping, selectors, sensors, commands, displays and explicit one-tick delays. +Each audio sample evaluates the graph before physics and holds commands during +fluid substeps. Unknown types/ports, incompatible quantities, multiple drivers +and combinational cycles are rejected. Required unconnected inputs block Test. +Missing measurements and math faults carry invalid status; motor/valve commands +apply their declared inactive fallback rather than retaining a previous command. + +The graph is saved in `[logic]` tables, including independent canvas positions. +Physical inspector controls identify signal-owned fields; existing manual or ECU +controls cannot simultaneously own them. Existing engine ECUs retain their +labeled legacy compatibility model; new sensor-driven presets use explicit +measurements as described below. + +## Sensor-driven ignition and Alpha-N EFI + +Open `sensor-ignition-bench.machine.toml` or `sensor-alpha-n-bench.machine.toml` +from saved machines for a working 3000 RPM test bench. The first uses a physical +carburetor; the second transports fuel from an attached intake injector. Both +have crank-to-ECU wiring visible in **Logic**. Press Test to run, then inspect +inputs and physical parts in Logic/3D or Split. + +Select an ECU and cycle its preset between **Legacy ideal**, **Crank ignition** +and **Alpha-N EFI**. Sensor presets assign only cylinders through blue links; +choose **Show ECU connector in Logic** and connect crank speed and angle from +one physical pickup. Alpha-N also requires a throttle-position sensor. No driver +demand wire is needed for a manually adjusted throttle. The cylinder's **Pickup +TDC deg** is its calibrated TDC in the sensor's wrapped crank coordinates; for a +pickup offset of zero, a cylinder physical phase of 90 degrees has TDC at 270. +Crank-only ignition is wasted spark, without hidden cam/cycle synchronization. + +For EFI, select an intake chamber and choose **Attach fuel injector**, then +choose its Alpha-N fuel ECU. Scroll its properties to set injector capacity, +initial fuel mass, fuel temperature, finite/ideal rail supply and cylinder-dose +count. A common-plenum injector can deliver several cylinder doses; their total +must match the ECU's assigned cylinders. The ECU's **Fuel RPM**, **Fuel TPS** and +**Fuel cell** fields edit the calibrated table (RPM rows, TPS columns, displayed +mg per cylinder per four-stroke cycle). This is an open-loop calibration, not a +measurement of trapped air mass. + +Required input faults, stale or mixed pickups, and pickup/engine shaft mismatch +cut new spark events and EFI delivery. Constants and arithmetic cannot substitute +for physical crank timing. Previously ignited fuel finishes its physical burn. +The ECU inspector reports sensor faults and rev limiting; injector hosts report +flow and remaining fuel. Ignition maps and RPM limits allow live tuning. Fuel +maps, injector setup, strategy and pickup calibration require Build. + +Fuel delivery currently uses a declared regulated rail and continuous/batch-average +vaporized fuel addition into the gas volume. There is no liquid rail-pressure +sensor or sequential pulse-width/evaporation model yet. Format 9 saves the new +bindings and calibrations; older machines continue loading without being rewritten. + +## Speed-density EFI + +Open `sensor-speed-density-bench.machine.toml` for the minimal MAP/IAT engine +example. Select **Speed-density EFI** in the ECU preset cycle and choose **Show +ECU connector in Logic**. Connect crank speed/position, an absolute-pressure MAP +pickup in the intake manifold and a gas-temperature IAT pickup at the intake. +TPS is not required for this basic preset. Missing or invalid measurements cut +fuel and new ignition events; the controller does not read hidden cylinder charge. + +In the ECU properties, **SD displacement cc** is the calibrated swept volume per +cylinder. **SD target AFR**, air gas constant and the VE table determine the +open-loop estimate `MAP * displacement * VE / (R_air * IAT)`. **SD VE RPM** gives +the rows and **SD VE MAP kPa** the columns; **SD VE cell %** edits row-major table +values. Values outside the table axes use its edge calibration. These settings +require Build; the ignition map and rev limiter still permit live tuning. + +Injector attachments can target Alpha-N or speed-density ECUs. Their explicit +capacity and finite/ideal rail supply continue to limit actual delivered fuel. +The dry ideal-gas estimate does not measure fresh-air fraction under intake-gas +dilution and currently has no warm-up, acceleration or lambda corrections. Format +10 saves this strategy and its calibration while preserving loading of versions +1..9. + +## Architecture and further work + +The proposed expansion is documented in the +[machine systems roadmap](MECHANICAL_BUILDER_PLAN.md): typed ports and logic, +explicit shafts, pneumatic parts, sensor attachments and sensor-driven ECU +presets, with one integrated delivery order. See the linked implementation +progress for completed parts and the remaining planned features. + +`MachineDocument` stores parts with stable IDs, local geometry and explicit port +connections. `MachineSimulation` compiles that document into the existing +`Pipe1D`, `Volume0D`, `FourStrokeCylinder`, `BoundarySystem`, `Solver` and +`MechanicalSystem` components. +Shaft, ECU, cam, combustion and fuel-metering updates run once per audio sample, +as in the original engine scenarios. Gas transport and pressure-operated check +valves retain CFL substeps. Compiled part/gate lists avoid repeated graph scans. +The editor consumes snapshots while testing and owns its document separately. +Pipe meshes are cached until their local route or diameters change; transforms +and pressure colors reuse the mesh. Part icons share a cached texture atlas. + +The editor includes fifteen part types, gas and mechanical example circuits, four preset imports, +save/load, group transforms, pipe routing, shared nodes, undo/redo, selection +copy/paste, reusable assemblies, and Build/Test. Remaining workshop work is +focused on port-picking and control-wire visibility in dense assemblies, clearer +group-edit feedback, property help, and adjustable port placement. New machine +types, gears and clutches remain on the component roadmap; explicit shafts are implemented. + +### Next session + +Continue the active component goal: explicit shafts, flywheels, drives, brakes +and six basic sensor assemblies are implemented and verified. The typed logic +runtime, persistence, physical commands and basic Logic/split editor are integrated +with thirteen focused suites passing at the editor milestone. Sensor-driven +ignition/Alpha-N presets, grouped ECU inputs, pickup calibration and explicit +injector attachments are now integrated, saved and tested with actual combustion +in two new benches. Speed-density EFI now adds a third working bench with actual +MAP/IAT inputs, calibrated VE/displacement, saved bindings and workshop controls. +Fifteen focused suites pass. Continue stage 4 with physical +needle/shutoff/regulator/relief valves and pressure-switch control; see the next +physical component slice in the progress file. Keep advanced graph UX/operator panels on the UI +backlog. See [component progress](COMPONENT_IMPLEMENTATION.md). Preserve the +existing uncommitted working tree and user-edited machine files. + +### Historical session notes + +The user asked to stop polishing and launch the current build on 2026-10-30. +The updated desktop executable was built and launched with +`machines/single-450-imported.machine.toml`. Changes are saved locally and remain +uncommitted. Preserve the existing working tree. + +Continue with small usability refinements, particularly port picking and control +wire visibility in dense assemblies, group-edit feedback, and property help. +The 2026-10-31 usability pass added validated connection previews, click-position +bend insertion, focus selection and navigable machine checks. It also corrected +the OpenGL/SFML state handoff so controls remain visible after raw 3D rendering. +If the reported 16,000 RPM stop persists, reproduce it with the user's exact +machine/settings: controlled tests passed through 24,000 RPM and the free bench +settled around 17,500 RPM, so a universal 16,000 RPM cap was not reproduced. +Legacy version 1 engine files need an explicit carburetor or injection ECU now. + +Workshop performance was fixed on 2026-10-30. Arclength meshing, sample-rate +control updates, compiled gate lists and cached rendering reduced flat6 simulation +cost from 104.4 to 8.8 us/sample at 3,000 RPM and 35% pedal. It now uses 98 cells +and approximately one CFL substep/sample, matching the native layout's mesh. +The importer positions its metering body to allow a longer outlet stub while +preserving the inlet endpoints, total route length and gas volume. Untouched stock +imports were refreshed; custom saved designs retain their authored geometry. +Flat6 and V8 audio-driven playback kept real time with no measured underruns. +See the [performance report](../verification/performance/2026-10-30/workshop-report.md) +for timings, validation and reproducible commands. Further work can focus on the +usability items above and closer calibration parity with native engine scenarios; +the workshop still represents its metering hardware as explicit editable parts. + +## Checks + +`fluidsim_machine_tests` covers centerline/profile volumes, rotated tanks, +connection snapping, malformed graphs, file round trips, closed/open throttle +behavior, shared nodes, group transforms, explicit fuel sources, ECU cut duration, +carburetor-only combustion, preset imports, vacuum pumping and an EFI bench. +`fluidsim_machine_builder_tests` checks projection and picking, world rotations, +mouse-driven translation, Shift selection, assembly duplication/deletion, undo +and Build/Test transitions. Assembly checks cover connected subset copies, +air/control endpoint remapping, geometry and probe persistence, cross-document +paste, library saving/browsing/insertion, and insertion undo/redo. Escape and +explicit Menu/window exit paths are checked separately. +Its `--preview ` option writes editor screenshots for layout review. +It also checks pipe-cache invalidation, transform/pressure cache reuse, deleted +parts and the icon atlas. Physics checks cover route-knot-independent meshing, +real short-pipe CFL limits and pump sample-rate refinement. +Additional checks cover non-mutating connection previews, incompatible air/control +pins, occupied ports and collapsed pipes; shared fuel-source diagnosis; perspective +bend insertion with undo/redo; and check-to-part navigation. Preview mode also +verifies rendered UI pixels after both scene and icon rendering. +Instrument checks cover probe persistence, exhaust AFR/lambda after combustion, +conserved fuel-origin transport through solvers and junctions, pause/step/slow +playback, live tuning without a reload, locked geometry, live undo/redo and CSV +channel units. The [instrument verification report](../verification/performance/2026-10-31/workshop-instruments.md) +records the playback checks with eight channels active. +`fluidsim_machine_tests --rpm-sweep` checks high RPM and a 30-second free-running +acceleration trace with the ECU rev limiter disabled. diff --git a/docs/MECHANICAL_BUILDER_PLAN.md b/docs/MECHANICAL_BUILDER_PLAN.md new file mode 100644 index 0000000..66fe5ec --- /dev/null +++ b/docs/MECHANICAL_BUILDER_PLAN.md @@ -0,0 +1,621 @@ +# Machine systems roadmap: mechanics, pneumatics, logic and ECU sensors + +Planning draft, 2026-11-01. This describes proposed work, not implemented features. +Implementation has started; see [component progress](COMPONENT_IMPLEMENTATION.md) +for completed foundations and the remaining delivery backlog. + +## Goal + +Build machines from explicit gas, mechanical and signal connections. Engines, +pumps and actuators exchange physical work; sensors measure named locations; +logic and ECUs control declared targets. The workshop should make those paths +easy to connect, inspect and diagnose. + +Originally cylinders and piston pumps used one implicit machine-wide crankshaft. +Explicit shaft, flywheel, drive and brake parts are now implemented, with migrated +legacy layouts and targeted instruments. Six basic sensor assemblies and their +physical bindings/live inspector are also implemented. The basic logic runtime +and 3D/Logic/Split editor, sensor-driven ignition/Alpha-N presets and explicit +intake injector attachments are integrated and verified. Stage 4 now also has a +working speed-density preset using actual MAP/IAT signals and calibrated VE. +Advanced graph UX, the stage-4 pneumatic valves and the later physical systems +remain on the delivery backlog. + +## Agreed design rules + +- Boolean signals are red; Number signals are green. Directed inputs are hollow + rings and outputs filled circles. Physical ports such as gas/pressure remain + bidirectional and use distinct neutral domain symbols. +- Establish typed ports, stable identities and validation before adding parts + that depend on them. Types must exist in the document and runtime, not only UI. +- Use one machine document across 3D Build, Logic and Test. Canvas references, + sensors and instruments do not create duplicate physical machines. +- Sensor attachments belong at named physical locations. Readouts may fan out + to ECUs, logic and instruments. Controllers use connected measurements and + explicit calibration/defaults rather than hidden access to unrelated state. +- Provide simple starting presets with few mandatory connections; expand their + visible optional inputs as features are enabled. Missing and invalid data + have explicit, different behavior. +- Preserve legacy machines through versioned migrations and labeled compatibility + controllers. Keep their operating behavior reviewable before changing models. +- UI work prioritizes consistent appearance, responsive layout and preventing + text/control overlap at the primary 1920x1080 desktop target. Test starts running + in the existing workspace; panels resize/collapse and retain their widths. + +## Integrated delivery order + +This is the authoritative implementation order. The track sections below detail +their own parts and acceptance targets; their local numbering is not a separate +release schedule. + +| Stage | Deliverable | Completion example | +| --- | --- | --- | +| 0 | Shared port descriptors, quantity/reference types, stable keys, validation and migration scaffolding | Every connection path rejects the same incompatible types; existing machines still load. | +| 1 | Explicit shafts, flywheel, drive/load, basic sensor attachments and targeted instruments | Two independent engines; one shaft driving a loaded piston pump. | +| 2 | Logic canvas, manual controls, gates, thresholds, live values and valve/motor command ownership | Sensor -> threshold -> enable switch -> drive, with red/green signal wiring. | +| 3 | Sensor-driven ignition ECU and Alpha-N EFI, crank sensing and ECU connector UI | Carbureted ignition control and minimal EFI both run from explicit sensors; missing crank stops controlled combustion. | +| 4 | MAP/IAT sensors, speed-density ECU, regulator/relief/shutoff valves and pressure-switch preset | Sensor-driven engine and automatic regulated air supply bench. | +| 5 | Gears, clutch, starter; directional valves, linear actuators and loads; stateful logic | Geared pump, clutched load and timed pneumatic actuator circuits. | +| 6 | MAF/sequential ECU options, air motor, rotary compressor, acoustic assemblies and remaining UI refinement | Stored-air motor and richer sensor/actuator examples. | +| Later | Thermal parts, belt detail, ejector, turbine/turbocharger and general linkages | Add only with a defined physical model and acceptance cases. | + +Ship small usable slices within each stage. Stage 3 depends on defining sensor +timing and explicit fuel delivery, not merely drawing extra ECU input pins. + +## Proposed parts + +| Part | Purpose and editable properties | Delivery | +| --- | --- | --- | +| Shaft / crankshaft | Named rotational node; angle, initial RPM, base inertia and friction. Cylinders and pumps attach with individual phase offsets. | First release | +| Flywheel | Adds inertia to its connected shaft; explicit inertia initially, with geometry-derived inertia as a later option. | First release | +| Drive motor | Ideal speed-controlled bench drive or torque-controlled motor; target speed or torque and direction. Report required drive torque and power in speed mode. | First release | +| Brake / load | Applies a constant opposing torque and optional speed-dependent drag; reports absorbed power. | First release | +| Gear pair | Fixed signed speed ratio between two shafts, with an explicit efficiency/loss model. Tooth counts determine the ratio. | Second release | +| Clutch | Connects two shafts through a torque-limited friction coupling; engagement, torque capacity, slip and dissipated power. | Second release | +| Starter | Torque-limited drive with a one-way coupling and hold-to-start input. Initially uses a specified torque curve. | Second release | +| Belt / pulley drive | Same-direction or crossed drive with pulley ratio; an ideal fixed-ratio model first, slip/tension later. | Later | +| Rotary compressor / pump | Continuous shaft load coupled to gas flow; requires a defined displacement or performance-map model. | Later | + +Existing cylinders and piston pumps gain mechanical attachment ports. Their +phase is relative to the attached shaft. A shaft can have multiple attachments; +there is no automatic connection because parts happen to be nearby. + +For the initial release, one shaft part represents a rigid rotational node. +Its visible length helps placement; it does not introduce torsional flexibility. +Base shaft inertia and attached flywheel inertia are counted once. Separate +shafts rotate independently until connected by a supported coupling. + +## Connection and simulation rules + +- Distinguish gas ports, control pins and rotational ports by shape, label and + color. Preview compatibility and explain rejected connections before clicking. +- Mechanical attachment lines identify the shaft association. They do not + silently move cylinders or imply a physically supported bent driveshaft. +- Keep shaft angle/speed per shaft. Route cylinder torque, pump torque, losses, + drives and loads to the correct shaft; sample its RPM for each attached ECU. +- Initially require cylinders controlled by one ECU to share a shaft. Diagnose + mixed-shaft assignments until explicit speed-reference selection is added. +- Ideal gears impose a speed/angle relationship. Solve connected rigid groups + with inertia reflected through their ratios; do not use competing independent + RPM updates or stiff penalty torques to approximate a rigid connection. +- Validate contradictory ratios, incompatible prescribed speeds and unsupported + constraint loops. Start with tree-shaped gear networks and reject loops with + actionable diagnostics until a general constraint solver is available. +- Friction and brake torque oppose motion, and must not push a stopped shaft + into reverse. Drive torque can select direction. Check existing cylinder/cam + assumptions before enabling reverse engine operation. +- Track external work, rotational kinetic energy and coupling losses. Clutch + slip dissipates energy; an ideal prescribed-speed drive supplies or absorbs + whatever work is required and reports it. +- Connection changes and initial state changes require Build. Safe torque, + speed-target and engagement tuning can remain available during Test. + +## Workshop UI + +The [workshop UI rework](WORKSHOP_UI_PLAN.md) specifies the workspace layout, +interaction conventions and user-selected preferences in more detail. + +### First release: connecting and understanding the machine + +- Searchable parts picker with Gas, Engine, Mechanical and Control categories. +- Connection filters for Gas, Mechanical and Control; fade unrelated links. +- Larger screen-space port targets, hover labels, endpoint highlighting and + candidate cycling where ports overlap. Preserve existing gas connection rules. +- Mechanical overview listing each shaft, its attached parts and its drive/load. + Selecting a row selects and focuses the corresponding assembly in 3D. +- Shaft inspector groups Initial state, Inertia and losses, and Connections. + Show total connected inertia separately from editable base inertia. +- Cylinder/pump inspector shows the named shaft and phase offset, with an action + to select the shaft. Unattached mechanical parts receive a navigable check. +- RPM, torque and power instruments require a named target. Torque readings + identify what is measured, such as cylinder output, motor input or load torque; + a shaft network has no single universal transmitted-torque value. +- Test overlay shows the selected shaft's RPM and operating state. With several + shafts, let the user pin the main readout instead of silently choosing one. + +### Second release: coupled machines and editing feedback + +- Gear connection preview shows input/output shafts, ratio and rotation direction. +- Clutch control and instruments show engagement, slip RPM and lost power. +- A compact power-path view highlights the selected shaft's connected network. +- Group inspector shows selection count, pivot and mixed property values; + offer only edits applicable to the selected parts. +- Add concise property help with units, physical meaning and Build/Test status. +- Add named saved visibility groups for dense assemblies. Hiding a part only + affects editing visibility, never simulation membership. + +Adjustable gas port placement is a separate follow-up. Ports represent physical +openings, so editing them must update connected geometry and preserve validation; +they should not be freely dragged as purely cosmetic connection markers. + +## Save files, imports and assemblies + +- Introduce a new machine format version with explicit shaft references and + mechanical connections, using stable IDs. +- Upgrade older files to one visible shaft with their existing inertia, losses, + initial speed, drive and load settings. Create drive/load parts as needed and + attach all existing cylinders and pumps with their original phase offsets. +- Preserve existing starter behavior during migration; replace it with an + explicit starter when the starter model is available. +- Retarget legacy shaft instruments to the migrated shaft. Retain the existing + torque metric's meaning and label it explicitly during the transition. +- Engine imports create explicit mechanical parts. Copy/paste and assemblies + remap internal shaft references and retain external attachments as visibly + unresolved; they must never attach to a destination shaft by coincidence. + +## Mechanical track scope and acceptance + +Before these implementation stages, deliver the typed port foundation described +in the logic section below. Mechanical and pneumatic additions should use that +shared contract from their first implementation. + +1. **Explicit shaft foundation:** document/serialization migration, independent + shaft runtime state, cylinder/pump attachment, rendering and selection. + Acceptance: two disconnected engines can run at different speeds; existing + imported machines retain their operating behavior and phase relationships. +2. **Usable mechanical bench:** flywheel, drive motor and brake/load; connection + filters, port picking, shaft inspector and targeted instruments. + Acceptance: assemble and inspect an engine driving a loaded piston pump; + disconnected attachments and fuel/control problems are easy to locate. +3. **Coupled shafts:** fixed gears, clutch and explicit starter, with ratio and + power-path feedback. Acceptance: accelerate a second inertia through a clutch, + observe slip losses, and drive a pump at a different speed through gears. +4. **Broader machines:** belt drives and a rotary compressor/pump after choosing + and validating their physical models; adjustable gas ports and visibility + groups as workshop follow-ups. + +Validation should cover legacy migration, assembly ID remapping, independent +shafts, phase continuity, inertia response, gear direction/ratio and reflected +inertia, prescribed-drive work, clutch losses and stop/reversal behavior. +Use focused simulation and builder checks as each stage is implemented. + +The first mechanical slice covers the shaft foundation and usable bench above. +Detailed electrical motors, +batteries, gear contact dynamics, shaft flex and belt tension need separate models +and are outside that milestone. + +## Pneumatic and fluid-powered expansion + +Extend the current compressible-gas system with useful circuits: store compressed +air, regulate it, switch it, and convert its pressure into linear or shaft work. +Existing chambers already serve as finite tanks, and the piston pump can form a +reciprocating compressor with external check valves. Add useful presets and +assemblies for these before introducing redundant part types. + +This roadmap initially targets gas. Liquid hydraulics would need a separately +defined fluid model, including compressibility, cavitation and appropriate +boundary treatment, before the same UI could promise hydraulic behavior. + +### Proposed fluid parts + +| Part | Behavior and properties | Priority | +| --- | --- | --- | +| Adjustable restriction / needle valve | Two-port metering with editable opening and effective flow area; supports reverse flow and choking through the shared gas boundary model. | A: basic circuits | +| Shutoff / solenoid valve | Two-port on/off valve; normally open/closed state, commanded opening and finite actuation time. | A | +| Pressure relief valve | Opens from a protected node to an explicit outlet above a differential-pressure setting; reseat behavior prevents chatter. | A | +| Pressure regulator | Reduces downstream pressure using a bounded inlet restriction with response dynamics; cannot increase pressure or hold a target without supply capacity. Non-relieving initially. | A | +| Pressure switch | Senses a chosen pressure and emits a Boolean signal with separate on/off thresholds; starts/stops a compressor or operates a valve. | A | +| Directional valve | 3/2 first, then 5/2; named supply, work and exhaust ports, explicit internal paths and transition behavior. | B: actuators | +| Pneumatic actuator | Single-acting spring-return first, then double-acting; bore, rod, stroke, moving mass, friction, dead volumes and end stops. | B | +| Linear load / spring | Opposing force, attached mass, spring and damping for an actuator's translational port. | B | +| Quick-exhaust valve | Vents an actuator locally through its own outlet when the supply path depressurizes. | B | +| Air motor | Positive-displacement gas motor with inlet, outlet and shaft port; displacement, leakage and mechanical losses. | C: shaft power | +| Rotary compressor | Shaft-driven gas compression with displacement/map, leakage and losses; load arises from gas work. | C | +| Nozzle / venturi | Converging/diverging pipe profile preset first; a later ejector adds a suction port and a validated entrainment/mixing model. | C | +| Filter / porous restriction | Calibrated pressure loss and optional gas storage; acoustic attenuation requires an additional defined model. | C | +| Silencer / resonator | Pipe/chamber assemblies first: expansion chamber, side branch and Helmholtz-style resonator. Absorptive silencers need an explicit loss model. | C | +| Cooler / heated chamber | Defined wall temperature or thermal mass and gas-wall heat transfer; report heat flow alongside gas state. | Later | +| Turbine / turbocharger | Map-based turbine and compressor on explicit shaft nodes; requires performance data and operating-range handling. | Later | + +Priority letters describe the pneumatic track, not a promise to finish it before +the mechanical milestones. Prioritize A alongside the shaft foundation, B next, +and C once independent shafts and component coupling are established. + +### Physical behavior and scope + +- Valves transfer conserved mass, species and energy through actual ports. + A regulator must not overwrite a chamber pressure to enforce its setpoint. +- Supply and exhaust are visible connections. An unconnected outlet remains + sealed under the current builder rule; warn when a valve needs an exhaust path. +- Regulator and switch inspectors state whether settings are absolute, gauge or + differential pressure. Store physical pressures in SI and define the reference. +- Actuator force uses the pressures on both piston faces and their effective + areas. Each chamber volume follows piston motion, with positive dead volume; + gas work and mechanical work must balance. +- Add translational mechanical ports for actuators and linear loads. An actuator + is distinct from the existing crank-driven piston pump. A general linkage + mechanism can connect translation to rotation in a later milestone. +- Resolve moving valves, actuator stops and small chambers with appropriate + component timesteps. The existing pipe CFL limit alone is insufficient. +- Air motors and compressors exchange torque/work with the attached shaft. + Their models define starting, stall, reversal and off-design behavior rather + than extrapolating efficiency formulas through zero speed. +- Pressure switches and solenoids use typed signal ports. Add a manual switch + and a simple hysteretic controller; define their update order. Keep ECU + assignments intact while extending the control connection system. + +### Fluid UI and instruments + +- Add Supply/storage, Valves, Actuators, Rotating machines and Thermal/acoustic + subcategories, plus searchable saved circuit assemblies. +- Label valve ports P (supply), A/B (work), and R/S (exhaust), with tooltips in + plain language. Show connected paths and moving valve state during Test. +- Preview actuator stroke limits and current rod position in 3D. Its inspector + shows chamber pressures, net force, position and velocity. +- Add differential pressure, actuator force/travel/velocity, valve state and + switch output instruments. Flow probes must identify the measured port and + direction; show mass flow by default and label volumetric reference conditions. +- Highlight a selected supply path and its exhaust route; distinguish blocked + flow from disconnected parts. Add checks for missing exhausts, absent supply, + unattached mechanical loads and incompatible signal connections. +- Show regulator target beside actual pressure, tank stored gas state, and + absorbed/delivered shaft power. Keep invalid or out-of-range readings explicit. + +### Playable circuit milestones + +1. **Air supply bench:** existing piston pump + check valves + tank, pressure + switch, relief valve and regulator. Charge a finite tank, cycle the drive + between pressure thresholds and feed a restricted downstream branch. +2. **Actuator bench:** regulated supply + 3/2 valve + spring-return actuator; + follow with a 5/2 valve and double-acting actuator driving a linear load. +3. **Stored-air motor:** charge the tank, disconnect the compressor drive, and + run an air motor connected to a flywheel and brake until pressure falls. +4. **Acoustic bench:** interchangeable resonator/silencer assemblies with inlet + and outlet pressure traces and source captures. Compare pressure loss and + acoustic response at consistent operating points. +5. **Boost system:** shaft compressor first, then a map-based turbine/compressor + assembly with intake and exhaust connections once those models are ready. + +Acceptance checks should include sealed-system mass/energy accounting, choking +and reverse flow, relief opening/reseating, regulator response and insufficient +supply, switch hysteresis, valve port states, actuator force/work and end stops, +and motor/compressor gas-work-to-shaft-work balance. These belong to the future +implementation stages; this document adds planning only. + +## Typed signals and logic nodes + +### Confirmed current implementation + +The shared typed-port foundation, physical sensor attachments and a general +signal-graph runtime are now implemented. `MachineDocument::logic` stores nodes +and named typed wires separately from physical connections. Runtime nodes include +controls, gates, thresholds, numeric operations, sensor/command references, +readouts and explicit delays. See [component progress](COMPONENT_IMPLEMENTATION.md) +for the verified scope. The basic Logic canvas, shared 3D/Logic/split workspace, +typed wire editing, interactive controls and graph inspectors are now integrated. +Advanced graph UX, operator panels and the later component stages remain planned. + +`MachineConnectionKind::Control` remains legacy ECU assignment. Those blue wires +assign a controller; the new Boolean/Number graph does not reinterpret them. + +### Shared port contract: implement first + +- Give each port an explicit domain: Gas, Rotational, Translational, Boolean, + Number, or the existing ECU assignment domain during migration. Domain and + signal direction must be data, not inferred from a part kind or rendered color. +- Define ports through a shared descriptor with a stable key, display name, + domain, input/output direction for signals, cardinality, and typed defaults. + Number ports also declare quantity/unit, supported range and any required + pressure reference. Physical ports retain their domain-specific behavior. +- Use stable serialized port keys, versioned migration for old numeric indices, + and stable part/node kind keys. Adding a port or reordering an enum must not + silently reconnect an existing saved machine to a different function. +- Store signal values as Boolean or finite numeric values with explicit validity. + Do not encode Booleans as arbitrary floating-point values or use NaN as truth. +- Only connect matching signal types from output to input. One output may feed + multiple inputs; one input accepts one driver. No automatic Bool/Number cast. + Explicit conversion nodes handle thresholding and Boolean-to-number selection. +- For numbers, also validate quantity compatibility. Pressure, RPM and opening + fraction must not connect interchangeably because all are numeric. Store SI + values; present convenient units, and require explicit scaling/mapping where + a quantity changes. Enforce arithmetic quantity rules and configure constants + with a quantity. Infer polymorphic node ports consistently when compiling. +- Required unwired inputs produce a navigable check and block Test; optional + inputs show a declared default in the UI. Invalid sensor/math output remains + visibly invalid and follows a documented per-consumer fallback; actuator + command inputs default to a declared inactive state, not a previous command. +- Use the same descriptors and compatibility checks for connection previews, + actual edits, loading, validation, compilation and runtime binding. Keep + signal links out of gas connectivity walks, pipe snapping and fuel searches. +- Preserve existing ECU assignments as their own relation; migrate them through + explicit adapters or dedicated ports when individual ECU commands are exposed. + Never reinterpret old ECU links as Boolean or Number links. + +### Color and node editing + +- **Boolean ports and wires: red. Number ports and wires: green.** These hues + identify the type consistently in 3D, node view, picker, inspector and legend. +- **Inputs are hollow rings; outputs are filled circles.** Apply this convention + consistently to directed ports on every node and part in both 3D and Logic + views. Shape communicates direction; red/green communicates Boolean/Number + type. Labels and hover text identify both type and direction without color. +- Physical ports without an input/output direction, such as pressure/gas + connections, remain bidirectional and use a distinct neutral physical connector + symbol. Their appearance must not imply one-way flow. A pressure sensor's gas + connection is bidirectional; its numeric measurement output is a filled green + circle. Rotational and translational attachments likewise use their physical + domain symbol rather than suggesting a fixed power-flow direction. +- Bright/dim styling and true/false text show Boolean state while retaining the + red type color and input-ring/output-circle silhouette. Number hover labels + show value, units and validity. +- Replace green as the gas identity with neutral gas markers. Connection validity + uses a separate checkmark or cross/message so success/error feedback cannot be + mistaken for a signal type or input/output direction. +- Add a **Logic** view: a pannable, zoomable node canvas for the same machine. + Logic nodes have saved canvas positions; sensors and actuators appear as + references to their existing physical parts, not duplicated simulated parts. +- Inputs appear on the left, outputs on the right. Drag to connect, branch from + outputs, reroute wires, disconnect and undo. Filter compatible nodes when + dragging from a pin. Label incompatible targets with a rejection reason. +- Keep 3D part placement separate from canvas layout. Selecting a sensor or + actuator reference can focus its physical part, and vice versa. Logic nodes + need no gas storage or physical 3D housing to function. +- Add live pin values, Boolean state and compact number traces during Test. + Pause freezes logic with physics; stepping advances both. A control panel can + pin buttons, switches, sliders and readouts for use without opening the graph. + +### Initial node library + +| Group | Nodes | Defined behavior | +| --- | --- | --- | +| Manual Boolean controls | Toggle switch, momentary button, Boolean constant | Switch retains its state; button is true while held. Release on focus loss, view close or Test stop. | +| Manual numeric controls | Number constant, slider, numeric input | Quantity, unit, range and initial value are explicit; user edits arrive at simulation boundaries. | +| Boolean gates | AND, OR, NOT, XOR, NAND, NOR | AND/OR start with two inputs and can add inputs; named, deterministic truth tables. | +| Threshold and comparison | Greater than, less than, equal within tolerance, range/window, hysteresis threshold | Number inputs produce Boolean outputs; comparisons use compatible quantities. Hysteresis has separate on/off thresholds and an initial state. | +| Numeric arithmetic | Add, subtract, multiply, divide, min, max, absolute value, clamp | Compatible quantity rules; division by zero produces an invalid value and a diagnostic. | +| Mapping and selection | Gain/offset, map range, Boolean-controlled number selector, Bool-to-number | Selector chooses between two compatible numbers; Bool-to-number emits configured false/true values. Mapping declares quantities and clamping behavior. | +| State and timing | Set/reset latch, rising/falling edge, on-delay, off-delay, pulse timer | Reset wins if set and reset coincide. Timers use simulation time. Edge nodes initialize from their input without emitting a false startup edge. | +| Number memory | One-tick delay, sample-and-hold | Explicit initial value; held quantities remain typed. Delay separates feedback across ticks. | +| Sensors | Pressure, differential pressure, temperature, mass flow, shaft RPM, actuator position, limit switch | Attach to a physical target/port without changing its gas volume. Supply validity and quantity metadata. | +| Outputs and commands | Valve enable, valve opening, motor enable/target, clutch engagement, starter command | Each target exposes its supported type, units/range and fallback. Explicit manual, signal or existing-controller ownership prevents competing commands. | +| Displays | Boolean lamp, numeric readout, trace | Observe a typed signal without changing its evaluation. | + +Starter nodes are switches, buttons, constants/sliders, AND/OR/NOT, thresholds +with hysteresis, number selection, pressure/RPM sensors, command inputs and +displays. Add the rest after the core graph and timing semantics are established. + +### Evaluation, feedback and persistence + +- Initially evaluate the compiled signal graph once per audio sample, using the + configured sample period (currently typically 1/44100 s). Sample physical + sensors at the boundary, evaluate the graph, latch actuator commands, then + advance physics. Fluid CFL substeps hold those commands. Slower playback does + not change control time relative to simulated time. +- Topologically evaluate combinational nodes. Treat memory/delay outputs as + stored state, then commit their next state simultaneously after evaluation. + Reject combinational cycles with a highlighted cycle path; explicit state + permits feedback. Physical sensor/actuator feedback crosses a physics step. +- Consume manual-input events in order at a simulation boundary. Preserve a + press/release pair between boundaries for one tick; edge outputs last one + tick, and delayed output validity/initialization is defined explicitly. +- Save graph topology, port keys, node parameters, canvas layout and configured + initial states. Each fresh Test resets to those initial states; transient + button presses, timer state and latch state do not silently become defaults. +- Topology edits require Build. Allow validated constants, thresholds and manual + controls during Test at synchronized boundaries. Copy/paste and assemblies + remap internal references and expose unresolved external sensor/target bindings. + +### Foundation acceptance and delivery order + +1. **Typed ports:** shared descriptors, typed links, stable serialization keys, + validation and legacy migration. Confirm all declared domains are represented + before mechanical and pneumatic nodes depend on them. +2. **Basic logic:** red Boolean and green Number pins/wires, node canvas, manual + controls, gates, thresholds, sensors, commands and live values. +3. **Stateful control:** timers, latch, edge detection, memory and numeric mapping, + followed by reusable control assemblies and pinned control panels. + +Required implementation checks: Bool-to-Bool and Number-to-compatible-Number +connections succeed; Bool/Number mismatches, quantity mismatches, reversed +direction and multiple drivers fail consistently in preview, load and compile. +Also verify fan-out, defaults, unknown port keys, save/load and copy/paste, +ECU migration, invalid math, graph ordering, cycle rejection, delayed feedback, +pause/step behavior and momentary-button release. Core runtime checks now cover +types, wiring validation, evaluation, persistence, faults, delayed feedback and +button-event release/reset. Canvas interactions, live inspector edits, focus/view- +close release, shared selection, pause/step and split layouts now have interaction +and rendered checks. Pinned operator panels and the remaining UI refinements still +need their own acceptance work. + +First demonstration: pressure sensor -> hysteresis threshold -> AND with a +manual enable switch -> compressor drive enable. Show tank pressure as a green +Number wire and compressor demand as a red Boolean wire. Follow with a button +and timer driving a pneumatic valve, and a slider commanding a proportional +valve through an explicit range mapping. + +## Sensor-driven ECUs + +### Current behavior and intended boundary + +The current `Ecu::step` receives pedal and the implicit shaft RPM directly. +`Ecu::fuel_for_air` doses fuel from cylinder air mass, and cylinder code supplies +the timing context. There are no general wired MAP, TPS, crank or IAT inputs. +Existing AFR/lambda instruments are ideal transported fuel-origin measurements, +not oxygen-sensor or heater models. + +Retain a labeled **Legacy ideal ECU** for migrated machines until equivalent +sensor-driven examples are established. New ECU presets use explicit sensor +inputs, controller calibration and declared actuator targets. Do not disguise +direct cylinder-air-mass access as speed-density or MAF fuel estimation. + +### ECU family and minimum connections + +Use one shared ECU runtime with selectable strategy/capability presets. Different +picker entries can provide the desired ECU types without duplicating validation, +sensor binding and fault handling. Keep calibration separate from measurements. + +| ECU preset | Required measurements in its basic mode | Optional features and their added requirements | +| --- | --- | --- | +| Ignition / carburetor | Crank speed and crank position | MAP or TPS for load-based spark; TPS/driver demand and a commanded throttle for electronic idle. Fuel remains physically supplied by the carburetor. | +| Alpha-N EFI | Crank speed/position and throttle position (TPS) | IAT, engine temperature and barometric corrections; lambda trim; cam phase for sequential operation. Basic mode uses a calibrated RPM/TPS fuel table. | +| Speed-density EFI | Crank speed/position, manifold absolute pressure (MAP), intake air temperature (IAT) | TPS for acceleration enrichment/overrun logic, temperature warm-up correction, barometric correction, lambda trim and cam phase. Requires displacement and volumetric-efficiency calibration. | +| MAF EFI | Crank speed/position and fresh-air mass flow | TPS, MAP, temperature corrections, lambda trim and cam phase. Requires a stated manifold-filling/charge-allocation model; inlet flow is not automatically trapped cylinder charge. | +| Advanced / custom | Required set of the explicitly selected base strategy | Configure supported feature modules; each declares added sensors, targets and fallback behavior. Initially no arbitrary scripting. | + +All presets also need declared controlled cylinders/ignition targets; EFI needs +explicit enabled fuel-delivery targets. These are configuration/actuator bindings, +not extra sensor inputs. Engine geometry, firing order, trigger offset, fuel +properties and maps are calibration, not wires. + +Basic crank-only modes use supported batch injection and wasted-spark or +distributor timing. Four-stroke sequential injection/individual-cycle ignition +requires cam phase or another explicit cycle-synchronization source. Do not +silently read the simulated cylinder's 720-degree phase to remove that requirement. + +Temperature, lambda, TPS and other optional inputs are not universally mandatory. +Features activate their requirements: no TPS means no TPS-based enrichment; +no lambda means open-loop fueling. Fixed reference values are editable and labeled +where a preset supports them. Required base-strategy inputs cannot silently fall +back to hidden physics values. A manual throttle can be used without driver-demand +or throttle-actuation wires; electronic throttle adds its own demand/feedback needs. + +The strategy split follows the Speeduino manual's MAP/speed-density and +TPS/Alpha-N distinction. MegaSquirt documentation provides trigger/cam examples +for batch/wasted-spark versus sequential operation. The exact minimum sets above +are proposed simulation presets, not a claim about a particular hardware ECU. + +### Sensor placement and representation + +| Sensor | Attachment / physical representation | Outputs and scope | +| --- | --- | --- | +| Crank position/speed | Attach to a named shaft, with visible pickup marker and reference offset | Number speed and crank angle; explicit synchronized/valid status. One sensor assembly supplies both. | +| Cam phase | Attach to an explicit cam drive when available; initially a configured phase pickup associated with the engine shaft | Number cycle phase and Boolean synchronized status. The initial pickup is an ideal cycle-reference abstraction, not simulated cam gearing. | +| MAP / pressure | Tap a pipe at normalized arclength or attach to a chamber/plenum | Number absolute pressure. Select the manifold downstream of the throttle; support per-runner or configured combined readings for individual throttles. | +| Differential pressure | Reference two named gas taps | Number pressure difference with labeled positive/reference sides. | +| IAT / gas temperature | Pipe tap or chamber gas attachment | Number temperature; ideal gas reading first, optional lag later. | +| TPS | Attach to throttle/carburetor mechanism | Number actual opening fraction, distinct from commanded opening or pedal demand. | +| Driver demand / pedal | Standalone manual/control part | Number demand fraction; optional for manual-throttle engines. | +| MAF | Pipe tap for an ideal mass-flow sensor; later an inline body if its restriction/storage is modeled | Number signed fresh-air mass flow, with declared reverse-flow behavior. Preserve total-flow instruments as a separate quantity. | +| Lambda | Exhaust pipe/collector attachment | Number lambda plus validity. Start with a labeled ideal mixture tracer; defer wideband oxygen chemistry, heater and response modeling. Closed-loop use requires explicit opt-in to that approximation. | +| Engine/head temperature | Attach to a cylinder/head thermal node when implemented | Number material temperature. Until then use a declared fixed calibration input; cylinder gas temperature is not coolant/head temperature. | +| Barometric pressure | Standalone ambient sensor or tap of an explicitly selected environment boundary | Number absolute pressure; never substitute manifold pressure during running. | +| Fuel pressure | Attach to a fuel rail/circuit once one exists | Number pressure; until then injector pressure is a declared calibration, not a pretend sensor reading. | +| Knock, oil pressure, battery voltage | Future dedicated physical models | Expose only after their state/model exists; do not synthesize plausible readings from unrelated gas variables. | + +Sensors are saved entities with stable IDs and host references; their Logic view +nodes refer to those same entities. Store pipe locations by arclength fraction, +not mesh cell index, and chamber/shaft targets by stable identity. Placement must +survive mesh rebuilds, transforms and valid geometry edits. Invalid/deleted hosts +leave visible unresolved bindings rather than silently retargeting another part. + +Use **Add sensor** on the selected part or pipe location, then offer only supported +measurements. Attached taps do not consume a gas connection, create storage or +alter flow. A standalone hose-connected pressure sensor has a bidirectional gas +port and real hose/dead volume if included; its Number output is still a filled +green circle. Flow sensors that add a body must explicitly add its losses/volume. + +Each sensor exposes measured quantity, units, location, sample time, validity and +optional response/filter settings. Ideal sensors come first; lag, noise, range +limits and fault injection are later options with deterministic initialization. +Existing probes may share sampling code; ECU inputs must not depend on the +instrument UI's eight-channel limit or its lower-rate recording samples. + +### Numeric timing inputs and synchronization + +ECU measurement inputs are Number rings, including speed, crank angle, MAP, +TPS and temperature. Crank speed and position are separate typed quantities, +not a single overloaded number. Optional Boolean sync/run/kill inputs are red. + +An ideal crank pickup outputs phase in a declared 360-degree reference frame and +speed from the same timestamp. Cam reference resolves the four-stroke cycle. +Keep unwrapped rotation/crossing information in the timing subsystem so wraparound +or a crossing within one simulation step cannot lose an ignition/injection event. +Validate coherent sensor provenance and phase offsets against controlled shafts. +A constant RPM or an unrelated shaft sensor is not sufficient timing authority. +The declared ideal timing channel may supply synchronization metadata alongside +the numeric reading; it is not an invisible additional physics-state lookup. + +Raw Hall/VR tooth waveforms and missing-tooth decoders are a later fidelity mode. +They need timestamped edge delivery and a decoder that outputs these same typed +measurements; sampling a Boolean once per audio sample must not be assumed to +capture every tooth at high speed. General logic may process speed and angle, +but transformations used for timing must preserve or explicitly invalidate the +phase/time reference. Ordinary stale or incoherent values cause a visible fault. + +### Actuation and fuel estimation + +- EFI computes fuel demand from the selected sensor strategy and calibration. + Account explicitly for speed-density charge estimates, Alpha-N table meaning, + or MAF manifold filling before assigning fuel to cylinders. +- Initially allow labeled per-cylinder ideal injector targets with explicit fuel + mass commands and delivery timing. Fuel mass/energy enters through that modeled + injection boundary; do not call the old `fuel_for_air` path behind the new ECU. + Separate injector/rail parts can replace that abstraction later. +- Expose per-cylinder fuel dose, spark advance and enable/cut commands with clear + units and scheduling semantics. Numeric advance is an angle before TDC, not an + arbitrary instantaneous spark pulse. Cylinder phase offsets remain calibrated + mechanical relationships used by the timing scheduler. +- ECU outputs and standalone logic must not simultaneously drive an actuator + input. Use an explicit selector/command owner. Multiple sensor consumers are + allowed. Each ECU initially controls cylinders on one crankshaft. +- Loss of required crank synchronization cuts its controlled fuel and spark; + missing optional lambda disables trim. Define preset-specific responses to MAP, + IAT, TPS and MAF faults. Never automatically switch fuel strategy unless an + explicit configured fallback has the required sensors and calibration. + +### ECU connector UI and diagnostics + +- Show Required, Connected and Optional input groups, with expandable Timing, + Air/load, Temperatures, Mixture and Auxiliary sections. Keep every connected + pin visible; show more optional pins on demand to avoid a wall of connectors. +- Every input displays required/optional status, quantity/unit, current value, + source sensor and any active fallback. Feature selection updates requirements + immediately; hiding a pin does not disconnect it or disable its validation. +- Add an ECU setup checklist: choose preset, assign cylinders/shaft, place required + sensors, connect measurements, assign actuators, verify timing and enable Test. + Offer sensor creation at a selected physical location; never guess a plenum. +- Highlight a sensor's physical marker and its wire when selecting an ECU input. + Show named ports and connection lists for dense ECU layouts; support reroutes + and saved harness groups without merging distinct signals into an untyped bus. +- Diagnose missing input, invalid measurement, synchronization loss, wrong shaft, + incorrect measurement kind/location, target conflicts and uncalibrated maps. + A valid zero RPM while stopped is not a missing sensor or a sensor failure. +- Provide compact status: stopped/cranking/running, synchronized, fueling strategy, + open/closed-loop mixture, active cuts and fallback reason. Retain ring/filled + circle direction shapes and red Boolean/green Number colors throughout. + +### Migration and acceptance + +Legacy imports initially retain Legacy ideal ECU behavior. Offer a deliberate +**Convert to sensor ECU** workflow that creates/reuses named shaft sensors and +selected TPS/MAP/IAT attachments, preserves geometry, and reports calibration that +cannot be converted. Do not promise an existing ideal-air-mass fuel calibration +will produce equivalent behavior from a newly invented VE or Alpha-N table. + +Demonstrations: minimal crank-driven ignition with carburetor; Alpha-N with crank +and TPS; speed-density with crank, MAP and IAT; optional lambda correction using +the declared ideal tracer; independent ECUs for two shafts. Add sequential and +MAF examples only after their prerequisites are complete. + +Acceptance includes required/optional input matrices, sensor positioning after +mesh/geometry changes, assembly remapping and host deletion, zero-RPM startup, +phase wrap and event crossings, cam loss, wrong-shaft rejection, stale input and +fault recovery, deterministic pause/step, explicit fuel-mass accounting, actuator +ownership and optional-feature degradation. Compare controller estimates against +physical state diagnostically without feeding that ground truth back secretly. + +Planning references: + +- [Speeduino overview: supported strategies and trigger inputs](https://wiki.speeduino.com/en/Overview) +- [Speeduino tuning: MAP and air-density/load estimation](https://wiki.speeduino.com/en/tuning/Tuning) +- [MegaSquirt MS3 hardware guide: crank/cam triggering examples](https://www.msextra.com/doc/pdf/MS3XV30_Hardware-1.4.pdf) diff --git a/docs/WORKSHOP_UI_PLAN.md b/docs/WORKSHOP_UI_PLAN.md new file mode 100644 index 0000000..56b67d9 --- /dev/null +++ b/docs/WORKSHOP_UI_PLAN.md @@ -0,0 +1,258 @@ +# Workshop UI rework + +Design specification, updated 2026-11-01. Implementation status is listed below. +Physical parts, typed signals and ECU requirements are defined in the +[machine systems roadmap](MECHANICAL_BUILDER_PLAN.md). + +## Implementation status + +The first UI pass implements the dark theme, shared adaptive panel geometry, +resizing/collapse with saved preferences, library search/categories and previews, +an Objects list, a measured inspector with stacked property fields, fitted control +labels and hover disclosure, direct click/drag connections, persistent Run/Pause/ +Step controls, navigable checks, and a separate resizable oscilloscope dock. +Existing ECU assignment ports use input rings and filled output circles; gas +ports retain their physical semantics. The Sense inspector now adds physical +sensor taps, unresolved binding repair and per-sample live outputs, using green +Number and red Boolean output dots. Sensor attachments remain locked during Test. + +The builder and desktop UI regressions pass. Rendered review covers empty, ECU, +Checks, Meters, running/paused and collapsed layouts at 1920x1080 and 1280x720. + +Logic/split views now share the running machine, with a searchable node library, +typed click/drag wiring, reference selection, live values/controls, parameter +inspectors and compact number trends. Input rings/output circles and red Boolean/ +green Number colors are checked in rendered output. Thirteen focused suites pass; +graph and split layouts are reviewed at 1920x1080 and 1280x720. + +Grouped ECU connectors follow their runtime systems. Automatic DPI/UI scale +overrides, an object hierarchy with +visibility groups, richer property grouping/help and pinned operator controls +remain follow-up UI work. Their design below remains the intended direction. + +## Confirmed direction + +- Dark engineering editor with compact toolbars, clear panels and restrained + colors. Reserve strong colors for signal types, selection and relevant state. +- Separate **3D** and **Logic** tabs, with an optional split view. Selection and + running simulation are shared across views; camera and canvas navigation state + remain independent. Switching views does not restart Test. +- Connect by dragging between ports or clicking a port then its target. A + dedicated Connect mode is not required. Escape cancels a pending connection. +- Signal inputs are hollow rings; outputs are filled circles. Boolean is red, + Number green. Bidirectional physical ports use neutral domain symbols. +- Panels remain in predictable positions, resize and collapse, and remember + their widths. Provide a reset-layout action and constrain restored sizes to + the current window so the central workspace stays usable. +- The library uses compact rows with small previews, categories and search. + Selecting an item shows a larger preview with its purpose and essential ports. +- Starting Test preserves the current workspace, selection and navigation state. + Lock structural edits while allowing safe live tuning and logic inspection. +- Test starts running. Pause and Step remain visible; Step is enabled when + paused, with a tooltip explaining that state when disabled. +- Primary design target is a 1920x1080 desktop window. The first priority is + consistent appearance and layout, especially preventing text/control overlap. +- Remaining routine design decisions are made autonomously, per user direction. + +## Layout and visual baseline + +Use a shared layout/component layer rather than individually positioned controls. +Rendering and hit testing consume the same computed rectangles. Define shared +theme values for spacing, typography, surfaces, borders and interaction states. + +Initial dimensions below are logical pixels at 100% UI scale, and may be tuned +through visual review while preserving the agreed structure. + +| Element | Default | +| --- | --- | +| Spacing | 4 px base unit; 8 px control gaps; 12 px panel padding; 16 px section gaps | +| Typography | 14 px body/fields, 12 px secondary labels, 16 px section titles; measured line height | +| Controls | 30 px minimum row height, increased when text or UI scale requires it | +| Top command bar | 44 px, with less-used file actions in a menu | +| Workspace toolbar | 36 px; 3D/Logic tabs and split toggle stay discoverable | +| Left panel | 260 px default; resizable between 220 and 420 px | +| Inspector | 340 px default; resizable between 280 and 520 px | +| Bottom area | Collapsed initially; 240 px default when opened, vertically resizable | +| Status bar | 24 px minimum; longer messages go to Checks or a tooltip | +| UI scale | Auto from display scaling, plus 100%, 125%, 150% and 200% overrides | + +Use a neutral charcoal palette with clear panel boundaries and one restrained +selection accent. Reuse the same fields, buttons, tabs, badges and menus across +all views. Strong red/green remains reserved for the agreed signal types; add +icons/text to distinguish faults and success from those types. + +Support a 1280x720 window at 100% scale through a compact layout. As available +logical space shrinks, move secondary toolbar actions into an overflow menu, +collapse the library/object panel, and offer the inspector as a temporary panel. +Preserve access to required actions and readable text instead of shrinking fonts. +If split view cannot fit two usable canvases, use a single tab and explain why +split is unavailable. Large UI scaling follows the same compact behavior. + +Choose a vertical split by default: 3D left, Logic right, with a draggable divider +and a swap action. Remember divider position, panel widths/collapse states and +active view in user preferences; clamp them on restore. Store machine-specific +camera/canvas layout separately from physical geometry and signal values. + +### Text and overlap rules + +- Measure text before layout. Use content-sized rows and explicit minimum sizes; + never assume every property label fits a fixed width. +- Inspector rows place label, value, unit and optional action in separate layout + cells. Narrow inspectors stack labels above fields; units never cover digits. +- Wrap help and diagnostic text and grow the row. Ellipsize long names only in + compact lists/tabs, with the complete name available on hover and in selection. +- Clip rendering and pointer handling to the same panel scroll region. Fixed + headers and footers reserve their own space so scrolling content cannot cover + buttons, tabs or trace controls. +- Tooltips and menus reposition within the window and respect their anchor. + Keep the focused text field visible when scrolling or opening a panel. +- Lay out ECU ports from descriptor rows with adequate label/value space; nodes + grow vertically as ports expand. Wires route away from pin labels before bends. +- World labels use screen-space placement with collision suppression; prioritize + selected/hovered labels and expose the rest on demand. Hidden labels must not + hide the underlying port's tooltip or named connection-list access. + +## Workspace structure + +Proposed layout: a slim document/command bar, workspace tabs, a left library/object +panel, central workspace, right inspector, and optional bottom diagnostics/traces. +Use resize handles and collapse controls within this predictable layout. + +- Top bar: machine name, unsaved status, file actions, undo/redo and simulation + controls. Show current run state unambiguously even in Logic view. +- 3D toolbar: Select, Move, Rotate, Route, snapping and fit/focus. Connection + gestures work directly on visible ports and do not start a transform drag. +- Logic toolbar: node search/add, frame selection/all, connection filters and + live-value visibility. Node creation targets the canvas under the pointer. +- Library: parts, sensors, logic nodes and saved assemblies with search and + categories, displayed as compact rows with small previews. Show a larger + selected-item preview and compatibility when adding from a signal port. +- Objects: named parts, sensors, shafts, ECUs and assemblies. Search, select, + focus, rename and hide without removing simulation membership. +- Inspector: selection identity, grouped properties, connections and contextual + help. Show units next to editable values and explain locked Build properties. +- Bottom area: navigable checks, traces and event history. Selecting an issue + locates the relevant part, port or connection in the appropriate view. +- Status bar: current gesture, keyboard hints and short feedback. Important + failures remain in Checks rather than disappearing with a transient message. + +## Core workflows + +### Place and configure + +Search for a part, select it, preview placement, then click to place. Shift repeats +placement. Escape cancels. Placed parts immediately expose a name and the small +set of essential properties; advanced groups expand on demand. Preserve current +copy/paste, assembly and undo workflows as the UI changes. + +For multiple selections show selection count, pivot and mixed values. Edits apply +only to compatible properties; do not display one arbitrary part's value as a +shared value. Selection/focus is synchronized with the object tree and Logic view. + +### Connect + +Hover highlights a port with name, domain, direction, units and connection state. +Dragging from it or clicking it begins a preview. Compatible targets stand out; +incompatible targets explain the reason. A separate checkmark/cross communicates +validity without overwriting the red/green signal colors. + +Keep ring centers easy to pick and port target size readable at any zoom. For +overlapping ports provide a named candidate chooser. Clicking a part body selects +it; hovering a hidden port must not silently steal a move gesture. + +Signal connections do not move physical parts. Gas connections keep their current +geometry/snapping rules and show the proposed pipe path before commit. Physical +port alignment errors are visible. Disconnect/reconnect commands are available +from ports and the inspector; all connection edits are undoable. + +### Add sensors and configure ECUs + +Use Add sensor on a selected part or a specific pipe location. Offer supported +sensor types, show the tap/pickup marker, and create a matching Logic reference. +Selecting either representation reveals its host and measurement location. + +ECU inspector groups Required, Connected and Optional inputs by function. Keep +connected ports visible and let optional groups expand. A guided checklist helps +choose a strategy, attach required sensors and assign actuator targets. Every +measurement displays units, source, validity and fallback state. + +### Inspect and test + +Simulation controls are independent of 3D/Logic tab selection. Distinguish Run, +Pause, Step and Stop/return-to-edit. Show simulation speed and time. Structural +properties explain why editing is locked during Test; safe live tuning shows +its applied value without resetting the machine. + +Entering Test does not switch to a different workspace or change selection. +Keep the current 3D camera, Logic canvas and split-view arrangement. Stopping +returns to editing in that same context; live tuning retains existing persistence +and undo behavior. Start running immediately, with Pause and Step available in +the persistent simulation controls. + +Hovering a signal reveals its current value; the inspector can pin it to a trace +or control panel. Physical measurements can also be added from the selected part. +Readouts identify the shaft, sensor or port they measure. Missing and invalid +signals never display as an ordinary zero or false state. + +## Readability and interaction requirements + +- Text and controls respond to window size and UI scaling. Scroll panels rather + than drawing controls outside their bounds; avoid the current fixed-coordinate + overlap risk as parts and properties grow. +- Focused numeric fields accept keyboard edits, units are visible, and invalid + input remains editable with a local explanation. Escape cancels the edit; + shortcuts do not trigger transforms while typing in a field. +- Preserve useful shortcuts, add visible tooltips, and keep camera gestures + separate from node-canvas gestures. Document both in a compact help overlay. +- Selection, warning and disabled states remain legible in the dark theme. + Signal meaning is also conveyed through labels and direction shapes. +- Keep the viewport stable when changing selection. Fit/focus is explicit. + Unrelated panel refreshes must not jump scroll position or steal text focus. +- Keep raw rendering and UI state ownership explicit so panel rendering remains + reliable after 3D drawing. Rendering optimization must preserve hit testing. + +## Delivery slices + +1. Appearance and layout foundation: shared controls/theme, responsive panels, + typography, text measurement, clipping and consistent input handling. Rework + the existing workshop first so today's parts already have a coherent UI. +2. Descriptor-driven inspector, connection gestures, named ports and filters. +3. Searchable library/object tree, then Logic tab and optional split view with + shared selection, stable node layout and live values. +4. Sensor placement and grouped ECU connector workflow. +5. Integrated test controls, traces, checks and pinned operator controls. + +Coordinate these slices with the systems roadmap: UI can display a type only +after document/runtime descriptors define it. Validate layouts at the agreed +minimum window size and scaling, connection picking in dense assemblies, keyboard +focus, save/restore behavior and the same workflows across 3D, Logic and split view. + +## Navigation decisions + +Keep the existing familiar 3D controls: Alt+left drag or right drag on empty space +orbits, middle drag pans, wheel zooms, F focuses selection and Home fits all. +An actual drag orbits; right click without dragging opens the context menu. +UI interaction consumes input before viewport handling, and text editing captures +its shortcuts. Escape cancels the active gesture before clearing selection. + +Logic uses wheel zoom about the pointer and middle drag to pan. Plain left click +selects, Shift modifies selection, and dragging empty canvas makes a selection +rectangle. Drag nodes by their header, ports by their connector and panel sizes +by their divider; the pointer/hover state identifies the pending action. + +## First milestone acceptance + +Review the existing single-cylinder, flat-six and V8 imports with long names, +multi-selection, pipe route editing, ECU properties, Checks and instruments. +Exercise empty, selected, hovered, disabled, focused, invalid and running states. + +At 1920x1080 and 1280x720 at 100% scale, and at representative larger display +scales, verify that text never overlaps unrelated controls, field units remain +readable, panels scroll within bounds and essential commands stay reachable. +Resize while editing, reopen saved panel layouts, expand long property groups +and open menus near window edges. Appearance/layout review is required before +adding the new systems' more complex panels. Later slices repeat this review for +the dense ECU connector view, Logic and split view as they become available. + +No further preference decisions are blocking this plan. Implementation will use +these defaults and adjust details through focused visual and interaction review. diff --git a/include/FluidSim/Carburetor.hpp b/include/FluidSim/Carburetor.hpp index b69f05b..9e3c15d 100644 --- a/include/FluidSim/Carburetor.hpp +++ b/include/FluidSim/Carburetor.hpp @@ -29,7 +29,7 @@ struct CarburetorConfig { class Carburetor : public CarburetorConfig { public: - explicit Carburetor(float throat_diameter = 0.0381f); + explicit Carburetor(float throat_diameter = 0.0381f,const CarburetorConfig& config={}); // air_mass_flow is positive from atmosphere toward the engine. void step(float dt, float pedal, float slide, float rpm, float air_mass_flow, diff --git a/include/FluidSim/Component.hpp b/include/FluidSim/Component.hpp index a25243f..f4ef730 100644 --- a/include/FluidSim/Component.hpp +++ b/include/FluidSim/Component.hpp @@ -13,6 +13,7 @@ struct Port { float gamma_value{1.4f}, gas_constant_value{287.0f}; float air_fraction{1.0f}; float fuel_fraction{}; + float fuel_equivalent_fraction{}; // fuel origin, including fuel already burned float* owner_energy{}; float* owner_air_mass{}; float* owner_exhaust_mass{}; @@ -25,6 +26,8 @@ class Component { public: virtual ~Component() = default; virtual std::vector ports() = 0; + // Optional actuation at each CFL substep, before boundary fluxes are resolved. + virtual void prepare_step(float /*dt*/) {} virtual void update_state(float dt) = 0; }; diff --git a/include/FluidSim/Desktop/LogicEditor.hpp b/include/FluidSim/Desktop/LogicEditor.hpp new file mode 100644 index 0000000..4c8ef57 --- /dev/null +++ b/include/FluidSim/Desktop/LogicEditor.hpp @@ -0,0 +1,45 @@ +#pragma once +#include "FluidSim/Machine.hpp" +#include "FluidSim/Desktop/WorkshopLayout.hpp" +#include +#include +#include + +namespace fluidsim::port::desktop { +struct LogicEditorHooks { + MachineDocument& document; + const MachineSnapshot* live{}; + bool shift{}; + std::function&)> edit; + std::function boolean_control; + std::function number_control; + std::function release_buttons; + std::function select_physical,focus_physical; + std::function status; + std::function example; +}; +class LogicEditor { +public: + LogicEditor(); + ~LogicEditor(); + void reset_document(); + void deactivate(const LogicEditorHooks&); + bool editing_text()const; + bool active_gesture()const; + std::uint32_t selected_node()const; + void select_node(std::uint32_t,const MachineDocument&); + void select_part_reference(std::uint32_t,const MachineDocument&); + void fit(const LogicGraph&,sf::FloatRect,bool selection_only=false); + void reveal(std::uint32_t,const MachineDocument&,sf::FloatRect); + sf::Vector2f project(LogicPosition,sf::FloatRect)const; + LogicPosition unproject(sf::Vector2f,sf::FloatRect)const; + sf::FloatRect node_rect(const LogicNode&,sf::FloatRect)const; + sf::Vector2f pin_position(const LogicNode&,const std::string&,sf::FloatRect)const; + // Panels can be owned independently in split view; the canvas is always live. + bool event(const sf::Event&,const LogicEditorHooks&,const WorkshopLayout&,sf::FloatRect canvas,bool panels); + void draw(sf::RenderWindow&,const sf::Font*,const LogicEditorHooks&,const WorkshopLayout&,sf::FloatRect canvas,bool panels); +private: + struct Impl; + std::unique_ptr impl_; +}; +} diff --git a/include/FluidSim/Desktop/MachineBuilder.hpp b/include/FluidSim/Desktop/MachineBuilder.hpp new file mode 100644 index 0000000..4c3ed50 --- /dev/null +++ b/include/FluidSim/Desktop/MachineBuilder.hpp @@ -0,0 +1,57 @@ +#pragma once +#include "FluidSim/Machine.hpp" +#include "FluidSim/Desktop/OrbitCamera.hpp" +#include +#include +#include + +namespace fluidsim::port::desktop { +struct BuilderRay {Vec3 origin,direction;}; +BuilderRay builder_ray(const OrbitCamera&,sf::FloatRect,sf::Vector2f); +std::optional builder_project(const OrbitCamera&,sf::FloatRect,Vec3); +// Returns metres along the ray; OBBs and duct centerlines use the rendered dimensions. +std::optional builder_pick_part(const MachinePart&,BuilderRay,float tolerance=0); +Vec3 builder_rotate_world(Vec3 euler,unsigned axis,float degrees); +struct BuilderPipeLocation {std::size_t segment{};Vec3 local_point;float screen_distance{};}; +std::optional builder_pipe_location(const OrbitCamera&,sf::FloatRect,const MachinePart&,sf::Vector2f); +struct MachinePlaybackStats {double simulation_seconds{};std::uint64_t underruns{};std::size_t buffered{};double speed{1};bool paused{};}; +enum class WorkshopView {ThreeD,Logic,Split}; +class MachineBuilder { +public: + MachineBuilder(); + ~MachineBuilder(); + const MachineDocument& document()const; + std::vector selected_parts()const; + void set_document(MachineDocument,std::filesystem::path source={}); + bool load_file(const std::filesystem::path&); + void start_test(); + void stop_test(); + bool testing()const; + MachinePlaybackStats playback_stats()const; + void set_test_speed(double); + void pause_test(); + void step_test(); + std::vector measurements()const; + bool export_measurements(const std::filesystem::path&); + bool finished()const; + bool wants_close_window()const; + bool handle_event(const sf::Event&,sf::Vector2u); + void update(); + void draw(sf::RenderWindow&,const sf::Font*); + static sf::FloatRect viewport(sf::Vector2u); + sf::FloatRect workspace_viewport(sf::Vector2u)const; + sf::FloatRect logic_viewport(sf::Vector2u)const; + WorkshopView workspace_view()const; + void set_workspace_view(WorkshopView); + std::uint32_t selected_logic_node()const; + std::vector logic_values()const; + sf::FloatRect logic_node_rectangle(std::uint32_t,sf::Vector2u)const; + sf::Vector2f logic_pin_position(std::uint32_t,const std::string&,sf::Vector2u)const; + void restore_workspace_preferences(); + const OrbitCamera& camera()const; +private: + struct Impl; + std::unique_ptr impl_; +}; +void run_machine_builder(sf::RenderWindow&,const sf::Font*,const std::filesystem::path& file={},bool play=false); +} diff --git a/include/FluidSim/Desktop/MachineRenderer3D.hpp b/include/FluidSim/Desktop/MachineRenderer3D.hpp new file mode 100644 index 0000000..fcf95e9 --- /dev/null +++ b/include/FluidSim/Desktop/MachineRenderer3D.hpp @@ -0,0 +1,21 @@ +#pragma once +#include "FluidSim/Machine.hpp" +#include "FluidSim/Desktop/OrbitCamera.hpp" +#include +#include +namespace fluidsim::port::desktop { +struct MachineRenderStats {std::uint64_t pipe_mesh_builds{},icon_atlas_builds{};std::size_t cached_pipes{};}; +class MachineRenderer3D { +public: + MachineRenderer3D(); + ~MachineRenderer3D(); + MachineRenderer3D(const MachineRenderer3D&)=delete; + MachineRenderer3D& operator=(const MachineRenderer3D&)=delete; + void draw(sf::RenderWindow&,const MachineDocument&,const MachineSnapshot*,sf::FloatRect,const OrbitCamera&,std::uint32_t selected,const MachinePart* ghost=nullptr,const std::vector& selection={}); + void draw_icon(sf::RenderWindow&,MachinePartKind,sf::FloatRect); + MachineRenderStats stats()const; +private: + struct Impl; + std::unique_ptr impl_; +}; +} diff --git a/include/FluidSim/Desktop/PortAppearance.hpp b/include/FluidSim/Desktop/PortAppearance.hpp new file mode 100644 index 0000000..d31dedf --- /dev/null +++ b/include/FluidSim/Desktop/PortAppearance.hpp @@ -0,0 +1,10 @@ +#pragma once +#include "FluidSim/MachinePorts.hpp" +#include +namespace fluidsim::port::desktop { +inline sf::Color port_color(PortDomain domain){ + switch(domain){case PortDomain::Boolean:return {235,92,102};case PortDomain::Number:return {87,201,128};case PortDomain::Gas:return {189,200,214};case PortDomain::Rotational:return {229,184,91};case PortDomain::Translational:return {177,139,220};case PortDomain::EcuAssignment:return {91,161,242};} + return sf::Color::White; +} +inline bool filled_port(const MachinePortDescriptor& descriptor){return descriptor.direction==PortDirection::Output;} +} diff --git a/include/FluidSim/Desktop/ScenarioMenu.hpp b/include/FluidSim/Desktop/ScenarioMenu.hpp index d4bd9d4..f30d3f3 100644 --- a/include/FluidSim/Desktop/ScenarioMenu.hpp +++ b/include/FluidSim/Desktop/ScenarioMenu.hpp @@ -2,6 +2,26 @@ #include "FluidSim/ScenarioCatalog.hpp" #include #include +#include namespace fluidsim::port::desktop { -std::optional choose_scenario(sf::RenderWindow&,const sf::Font*,const std::vector&); +struct ScenarioMenuChoice { + std::optional example; + std::filesystem::path machine; + bool play{}; +}; +enum class MenuScreen {Home,Workshop,Scenarios}; +class ScenarioMenu { +public: + ScenarioMenu(std::vector,std::vector machines={}); + ~ScenarioMenu(); + bool handle_event(const sf::Event&,sf::Vector2u); + void draw(sf::RenderWindow&,const sf::Font*); + bool finished()const; + MenuScreen screen()const; + std::optional take_choice(); +private: + struct Impl; + std::unique_ptr impl_; +}; +std::optional choose_scenario(sf::RenderWindow&,const sf::Font*,const std::vector&); } diff --git a/include/FluidSim/Desktop/WindowPresentation.hpp b/include/FluidSim/Desktop/WindowPresentation.hpp new file mode 100644 index 0000000..195f829 --- /dev/null +++ b/include/FluidSim/Desktop/WindowPresentation.hpp @@ -0,0 +1,8 @@ +#pragma once +#include + +namespace fluidsim::port::desktop { +// Maximize the decorated OS window; retain normal restore/minimize controls. +void maximize_window(sf::RenderWindow&); +bool window_is_maximized(const sf::RenderWindow&); +} diff --git a/include/FluidSim/Desktop/WorkshopLayout.hpp b/include/FluidSim/Desktop/WorkshopLayout.hpp new file mode 100644 index 0000000..ba40a52 --- /dev/null +++ b/include/FluidSim/Desktop/WorkshopLayout.hpp @@ -0,0 +1,30 @@ +#pragma once +#include +#include +#include + +namespace fluidsim::port::desktop { +// One geometry source for drawing, picking and viewport projection. +struct WorkshopLayout { + sf::FloatRect left, right, viewport, library, search; + float inspector_x{}, inspector_width{}, height{}, width{}; + static constexpr float field_top=302, field_height=52; + int field_rows()const{return std::max(1,int((height-164-field_top)/field_height));} + int library_rows()const{return std::max(1,int(library.size.y/36));} + sf::FloatRect field_row(int row)const{return {{inspector_x+12,field_top+row*field_height},{inspector_width-24,48}};} + sf::FloatRect field_value(int row)const{auto r=field_row(row);return {{r.position.x,r.position.y+21},{r.size.x-68,26}};} + static WorkshopLayout calculate(sf::Vector2u size,float left_width=260,float right_width=340,bool left_closed=false,bool right_closed=false){ + WorkshopLayout l;l.width=float(size.x);l.height=float(size.y); + const float available=std::max(480.f,l.width-440); + float lw=left_closed?28.f:std::clamp(left_width,220.f,420.f),rw=right_closed?28.f:std::clamp(right_width,280.f,520.f); + if(lw+rw>available){const float excess=lw+rw-available;rw=std::max(right_closed?28.f:280.f,rw-excess);lw=std::max(left_closed?28.f:180.f,std::min(lw,available-rw));} + l.left={{8,96},{lw,std::max(100.f,l.height-154)}}; + l.right={{l.width-rw-8,96},{rw,std::max(100.f,l.height-154)}}; + l.inspector_x=l.right.position.x;l.inspector_width=rw; + l.viewport={{l.left.position.x+lw+8,96},{std::max(100.f,l.right.position.x-lw-24),std::max(100.f,l.height-154)}}; + l.search={{20,152},{lw-24,30}}; + l.library={{20,230},{lw-24,std::max(36.f,l.height-474)}}; + return l; + } +}; +} diff --git a/include/FluidSim/Ecu.hpp b/include/FluidSim/Ecu.hpp index 9ed97a3..dad8481 100644 --- a/include/FluidSim/Ecu.hpp +++ b/include/FluidSim/Ecu.hpp @@ -1,8 +1,29 @@ #pragma once +#include "FluidSim/MachinePorts.hpp" +#include #include namespace fluidsim::port { +// Accepts only coherent, current channels from one physical crank pickup. +// Signed reverse measurements remain valid sensors, but cannot drive this +// forward-only ignition controller. A stationary crank is valid. +std::optional ecu_crank_sample(const SignalValue& speed, + const SignalValue& angle,double time); + +class CrankIgnitionClock { +public: + struct Result {bool valid{};unsigned sparks{};}; + // Crank-only wasted spark: one event per revolution at the configured + // pickup angle, without consulting a cylinder's hidden 720-degree phase. + Result sample(const SignalValue& speed,const SignalValue& angle, + double time,double advance_degrees,double tdc_degrees=0); + void reset(){previous_.reset();last_event_cycle_.reset();} +private: + std::optional previous_; + std::optional last_event_cycle_; +}; + // Ideal sensors and injectors; calibration parameters are not engine measurements. struct EcuConfig { enum class IdleControl { Feedback, FixedOpening }; @@ -18,6 +39,8 @@ struct EcuConfig { float rev_limit_rpm{10000.0f}, rev_resume_rpm{9750.0f}; bool rev_limit_enabled{true}; bool rev_limit_fuel_cut_enabled{true}; + bool rev_limit_ignition_cut_enabled{true}; + float rev_limit_cut_time{}; // Minimum cut duration in seconds, followed by RPM hysteresis. float overrun_cut_rpm{3500.0f}, overrun_resume_rpm{2400.0f}; bool overrun_fuel_cut_enabled{true}; bool overrun_ignition_cut_enabled{false}; @@ -35,7 +58,7 @@ public: float target_afr() const { return afr_; } float spark_advance() const { return spark_; } bool fuel_cut() const { return (limiter_ && rev_limit_fuel_cut_enabled) || (overrun_ && overrun_fuel_cut_enabled) || fault_; } - bool ignition_cut() const { return limiter_ || (overrun_ && overrun_ignition_cut_enabled) || fault_; } + bool ignition_cut() const { return (limiter_ && rev_limit_ignition_cut_enabled) || (overrun_ && overrun_ignition_cut_enabled) || fault_; } bool rev_limited() const { return limiter_; } bool idle_active() const { return idle_; } bool sensor_fault() const { return fault_; } @@ -45,5 +68,51 @@ private: bool initialized_{}; float opening_{}, target_{}, integral_{}, afr_{14.7f}, spark_{10.0f}; bool limiter_{}, overrun_{}, fault_{}, idle_{}; + float limiter_time_{}; +}; + +// Separate from the legacy ECU: measurements are supplied through typed wires. +// Alpha-N cells are calibrated kg per cylinder per four-stroke cycle, indexed +// by RPM rows and measured TPS columns. No trapped-air-mass input is available. +struct SensorEcuConfig { + enum class Strategy {Ignition,AlphaN,SpeedDensity}; + Strategy strategy{Strategy::Ignition}; + std::vector ignition_map{{0,10},{6000,30}}; + std::vector fuel_rpm{0,6000},fuel_tps{0,1}; + std::vector fuel_per_cycle{0.000002,0.00002,0.000002,0.00002}; + // Open-loop dry ideal-gas estimate per cylinder. Geometry, VE and fuel AFR + // are calibration; MAP and IAT must arrive as current wired measurements. + double displacement{.0003},air_gas_constant{287},target_afr{14.7}; + std::vector ve_rpm{0,6000},ve_pressure{20000,100000}; + std::vector volumetric_efficiency{.9,.9,.9,.9}; + bool rev_limit_enabled{true}; + double rev_limit_rpm{7000},rev_resume_rpm{6800}; +}; +inline constexpr int sensor_ecu_strategy_count=3; +const char* sensor_ecu_strategy_key(SensorEcuConfig::Strategy); +const char* sensor_ecu_strategy_name(SensorEcuConfig::Strategy); +SensorEcuConfig::Strategy sensor_ecu_strategy_from_key(std::string_view); +SensorEcuConfig::Strategy next_sensor_ecu_strategy(SensorEcuConfig::Strategy); +struct SensorEcuMeasurements {SignalValue speed,angle,tps,map,iat;}; +struct SensorEcuState { + bool sensor_fault{true},rev_limited{}; + double spark_advance{},fuel_per_cycle{},fuel_mass_flow{}; + double estimated_air_per_cycle{},volumetric_efficiency{}; + unsigned spark_events{}; +}; +class SensorEcu { +public: + explicit SensorEcu(SensorEcuConfig config); + void apply_calibration(SensorEcuConfig config); + const SensorEcuState& sample(const SignalValue& speed,const SignalValue& angle, + const SignalValue& tps,double time); + const SensorEcuState& sample(const SensorEcuMeasurements&,double time); + const SensorEcuState& state()const{return state_;} + void reset(){clock_.reset();state_={};limiter_=false;} +private: + SensorEcuConfig config_; + CrankIgnitionClock clock_; + SensorEcuState state_; + bool limiter_{}; }; } // namespace fluidsim::port diff --git a/include/FluidSim/Engine.hpp b/include/FluidSim/Engine.hpp index 036446b..180103d 100644 --- a/include/FluidSim/Engine.hpp +++ b/include/FluidSim/Engine.hpp @@ -4,6 +4,8 @@ #include "FluidSim/Constants.hpp" #include #include +#include +#include #include #include @@ -25,6 +27,8 @@ public: float average_power{}, average_torque{}; void add_torque(float torque); + float take_torque(); // Consume gas torque when an external shaft integrator owns motion. + void record_output(float torque,float power); void step(float dt); private: float external_torque_{}; @@ -51,6 +55,7 @@ public: float rev_limit_rpm{7500.0f}, rev_limit_cut_time{0.05f}, rev_limit_hysteresis{200.0f}; float air_mass{}, exhaust_mass{}, energy{}, volume{}, pressure{101325.0f}; float fuel_mass{}, burn_fraction{}; + float fuel_equivalent_mass{}; // passive fuel-origin tracer, unaffected by combustion bool combustion_active{}, fuel_injected{}, rev_limit_active{}; Port intake{}, exhaust{}; @@ -84,6 +89,14 @@ protected: class FourStrokeCylinder final : public EngineCylinder { public: + struct IgnitionCommand {bool fire{},cut{true};}; + // Present selects external timing exclusively. A fire pulse is consumed + // once; missing subsequent commands cannot recreate internal timed sparks. + // Requires physically transported fuel, including carburetor operation. + std::optional ignition_command; + // Once per cycle, before ignition consumes fuel. Independent of spark cut. + float charge_afr{std::numeric_limits::quiet_NaN()}; + bool charge_sampled{}; struct CamPoint { float angle_degrees{}, lift_metres{}; }; struct ValveDynamics { float mass_kg{0.035f}; @@ -106,6 +119,8 @@ public: static float cam_lift_at(const CamProfile& profile,float degrees) { return sample_cam(profile,degrees).lift_metres; } private: + float external_burn_angle_{}; + bool external_charge_ready_{}; struct ValveState { float lift{},velocity{};bool floating{}; }; const CamProfile* intake_cam_{}; const CamProfile* exhaust_cam_{}; diff --git a/include/FluidSim/Fluid.hpp b/include/FluidSim/Fluid.hpp index 24bcafa..bb0c6f2 100644 --- a/include/FluidSim/Fluid.hpp +++ b/include/FluidSim/Fluid.hpp @@ -54,10 +54,12 @@ public: float air_fraction() const { return air_mass_ / std::max(mass(), 1e-12f); } float fuel_fraction() const { return fuel_mass_ / std::max(mass(), 1e-12f); } float fuel_mass() const { return fuel_mass_; } + float fuel_equivalent_fraction()const{return fuel_equivalent_mass_/std::max(mass(),1e-12f);} void add_fuel_mass(float value, float temperature = 300.0f) { if (!std::isfinite(value) || value < 0.0f || !std::isfinite(temperature) || temperature <= 0.0f) throw std::invalid_argument("invalid fuel addition"); fuel_mass_ += value; + fuel_equivalent_mass_ += value; energy_ += value * gas_constant / (gamma - 1.0f) * temperature; sync_ports(); } @@ -68,9 +70,11 @@ public: const float total = value * volume_ / (gas_constant * target_t); const float fraction = air_fraction(); const float fuel = fuel_fraction(); + const float equivalent=fuel_equivalent_fraction(); air_mass_ = total * fraction; fuel_mass_ = total * fuel; exhaust_mass_ = total * std::max(1.0f - fraction - fuel, 0.0f); + fuel_equivalent_mass_=total*equivalent;equivalent_roundoff_=0; energy_ = value * volume_ / (gamma - 1.0f); air_roundoff_ = exhaust_roundoff_ = fuel_roundoff_ = energy_roundoff_ = 0; sync_ports(); @@ -80,7 +84,7 @@ public: throw std::invalid_argument("invalid volume timestep"); if (dt == 0) return; - double d_air = 0, d_exhaust = 0, d_fuel = 0, d_energy = 0; + double d_air = 0, d_exhaust = 0, d_fuel = 0, d_energy = 0, d_equivalent=0; const float fraction = air_fraction(); const float fuel = fuel_fraction(); for (const Port &p : ports_) { @@ -90,11 +94,13 @@ public: d_fuel += double(p.mass_flow) * flow_fuel; d_exhaust += double(p.mass_flow) * std::max(1.0f - flow_fraction - flow_fuel, 0.0f); d_energy += double(p.mass_flow) * p.enthalpy; + d_equivalent+=double(p.mass_flow)*(p.mass_flow>=0?std::max(p.fuel_equivalent_fraction,p.fuel_fraction):fuel_equivalent_fraction()); } accumulate(air_mass_, air_roundoff_, d_air * dt); accumulate(exhaust_mass_, exhaust_roundoff_, d_exhaust * dt); accumulate(fuel_mass_, fuel_roundoff_, d_fuel * dt); accumulate(energy_, energy_roundoff_, d_energy * dt); + accumulate(fuel_equivalent_mass_,equivalent_roundoff_,d_equivalent*dt); const float new_volume = volume_ + volume_change_rate * dt; if (!(new_volume > 0) || !std::isfinite(new_volume)) throw std::runtime_error("invalid moving volume"); @@ -118,6 +124,8 @@ public: air_mass_ = std::max(air_mass_, 0.0f); exhaust_mass_ = std::max(exhaust_mass_, 0.0f); fuel_mass_ = std::max(fuel_mass_, 0.0f); + fuel_equivalent_mass_=std::clamp(fuel_equivalent_mass_,fuel_mass_,mass()); + if(fuel_equivalent_mass_::min()){fuel_equivalent_mass_=0;equivalent_roundoff_=0;} sync_ports(); } void update_state(float dt) override { update(dt); } @@ -132,6 +140,7 @@ public: private: float volume_, air_mass_{}, exhaust_mass_{}, fuel_mass_{}, energy_; + float fuel_equivalent_mass_{};double equivalent_roundoff_{}; // Preserve sub-ulp transfers without changing the float port/budget ABI. double air_roundoff_{}, exhaust_roundoff_{}, fuel_roundoff_{}, energy_roundoff_{}; static void accumulate(float& value, double& roundoff, double increment) { @@ -149,6 +158,7 @@ private: p.enthalpy = gamma / (gamma - 1.0f) * p.pressure / std::max(p.density, 1e-12f); p.air_fraction = air_fraction(); p.fuel_fraction = fuel_fraction(); + p.fuel_equivalent_fraction=fuel_equivalent_fraction(); } void sync_ports() { for (Port &p : ports_) @@ -161,10 +171,12 @@ struct GasCell { float energy{1.225f * (101325.0f / (pipe_gm1 * 1.225f))}; float air{1.0f}; float fuel{}; + float fuel_equivalent{}; }; struct GasState { float rho{1.225f}, velocity{}, pressure{101325.0f}, air{1.0f}; float fuel{}; + float fuel_equivalent{}; }; class Pipe1D { @@ -172,7 +184,7 @@ public: std::vector linear_resistance, quadratic_resistance; // Integrated face fluxes: kg/s, N, W, kg/s of fresh air and fuel, positive along +x. struct Flux { - float m{}, p{}, e{}, y{}, fuel{}; + float m{}, p{}, e{}, y{}, fuel{}, fuel_equivalent{}; }; struct RoeState { double root_density{}, enthalpy{}; }; PipeSolver solver{PipeSolver::Rusanov}; @@ -200,6 +212,11 @@ public: if (!std::isfinite(cell_areas[i]) || !std::isfinite(cell_lengths[i]) || cell_areas[i] <= 0 || cell_lengths[i] <= 0) throw std::invalid_argument("pipe geometry must be finite and positive"); + if(std::any_of(cell_areas.begin(),cell_areas.end(),[&](float a){return a!=cell_areas.front();})){ + geometry_cfl_lengths_.resize(cells.size()); + for(std::size_t i=0;i(i))); } float cell_mass(int i) const { return cells.at(i).rho * cell_areas.at(i) * cell_lengths.at(i); } @@ -212,7 +229,9 @@ public: auto s = primitive(cells[i]); if (!(s.rho > 0) || !(s.pressure > 0) || !std::isfinite(s.velocity) || !std::isfinite(s.pressure)) throw std::runtime_error("nonphysical pipe state in CFL calculation"); - dt = std::min(dt, cfl * cell_lengths[i] / wave(s)); + float speed=wave(s),length=cell_lengths[i]; + if(!geometry_cfl_lengths_.empty()){length=geometry_cfl_lengths_[i];if(i)speed=std::max(speed,wave(primitive(cells[i-1])));if(i+1=0) return fa; @@ -312,7 +338,7 @@ public: float((sr*fa.p-sl*fb.p+diff*(b.rho*ub-a.rho*ua))*inv), float((sr*fa.e-sl*fb.e+diff*(eb-ea))*inv), float((sr*fa.y-sl*fb.y+diff*(b.rho*b.air-a.rho*a.air))*inv), - float((sr*fa.fuel-sl*fb.fuel+diff*(b.rho*b.fuel-a.rho*a.fuel))*inv)}; + float((sr*fa.fuel-sl*fb.fuel+diff*(b.rho*b.fuel-a.rho*a.fuel))*inv),float((sr*fa.fuel_equivalent-sl*fb.fuel_equivalent+diff*(b.rho*b.fuel_equivalent-a.rho*a.fuel_equivalent))*inv)}; }; if constexpr(method==PipeSolver::Hlle) return hlle(); const double dl=a.rho*(sl-ua), dr=b.rho*(sr-ub); @@ -333,7 +359,7 @@ public: } const double mass=f.m+speed*(density-q.rho); return {float(mass),float(f.p+speed*(density*sm-q.rho*q.velocity)), - float(f.e+speed*(energy-(left?ea:eb))),float(mass*q.air),float(mass*q.fuel)}; + float(f.e+speed*(energy-(left?ea:eb))),float(mass*q.air),float(mass*q.fuel),float(mass*q.fuel_equivalent)}; } static Flux numerical_flux(const GasState& a,const GasState& b,PipeSolver solver, float ca,float cb,bool* fallback=nullptr) { @@ -407,6 +433,7 @@ public: float en = cells[i].energy - factor * (fr.e - fl.e); const float air = cells[i].rho * cells[i].air - factor * (fr.y - fl.y); const float fuel = cells[i].rho * cells[i].fuel - factor * (fr.fuel - fl.fuel); + const float equivalent=cells[i].rho*std::max(cells[i].fuel_equivalent,cells[i].fuel)-factor*(fr.fuel_equivalent-fl.fuel_equivalent); if (!std::isfinite(rho) || !std::isfinite(mom) || !std::isfinite(en) || !std::isfinite(air) || !std::isfinite(fuel)) throw std::runtime_error("nonfinite finite-volume update"); @@ -434,9 +461,10 @@ public: // subnormal concentrations; total gas mass and energy are unchanged. const float fraction = std::clamp(air / rho, 0.0f, 1.0f); const float fuel_fraction = std::clamp(fuel / rho, 0.0f, 1.0f - fraction); + const float equivalent_fraction=std::clamp(equivalent/rho,fuel_fraction,1.f); next_cells_[i] = {rho, mom, en, fraction < std::numeric_limits::min() ? 0.0f : fraction, - fuel_fraction < std::numeric_limits::min() ? 0.0f : fuel_fraction}; + fuel_fraction < std::numeric_limits::min() ? 0.0f : fuel_fraction,equivalent_fraction::min()?0.f:equivalent_fraction}; } cells.swap(next_cells_); prepared_ = false; @@ -444,11 +472,12 @@ public: left_override_ = right_override_ = false; } static Flux scaled(Flux f, float a) { - return {f.m * a, f.p * a, f.e * a, f.y * a, f.fuel * a}; + return {f.m * a, f.p * a, f.e * a, f.y * a, f.fuel * a,f.fuel_equivalent*a}; } private: std::vector next_cells_; + std::vector geometry_cfl_lengths_; std::vector states_; std::vector reconstructed_left_, reconstructed_right_; std::vector waves_; @@ -480,13 +509,14 @@ private: const float dp=minmod(q.pressure-a.pressure,b.pressure-q.pressure); const float da=minmod(q.air-a.air,b.air-q.air); const float df=minmod(q.fuel-a.fuel,b.fuel-q.fuel); + const float de=minmod(q.fuel_equivalent-a.fuel_equivalent,b.fuel_equivalent-q.fuel_equivalent); const float h=.5f*dt/cell_lengths[i]; const GasState half{q.rho-h*(q.velocity*dr+q.rho*du), q.velocity-h*(q.velocity*du+dp/q.rho), q.pressure-h*(q.velocity*dp+gamma*q.pressure*du), - q.air-h*q.velocity*da,q.fuel-h*q.velocity*df}; + q.air-h*q.velocity*da,q.fuel-h*q.velocity*df,q.fuel_equivalent-h*q.velocity*de}; const auto edge=[&](float sign) { return GasState{half.rho+sign*dr, - half.velocity+sign*du,half.pressure+sign*dp,half.air+sign*da,half.fuel+sign*df}; }; + half.velocity+sign*du,half.pressure+sign*dp,half.air+sign*da,half.fuel+sign*df,half.fuel_equivalent+sign*de}; }; const auto left=edge(-.5f), right=edge(.5f); const auto physical=[](const GasState& v) { return v.rho>1e-12f && v.pressure>1e-6f && std::isfinite(v.pressure) && @@ -510,7 +540,7 @@ private: .5f * (ma * a.velocity + a.pressure + mb * b.velocity + b.pressure - c * (mb - ma)), .5f * ((ea + a.pressure) * a.velocity + (eb + b.pressure) * b.velocity - c * (eb - ea)), .5f * (ma * a.air + mb * b.air - c * (b.rho * b.air - a.rho * a.air)), - .5f * (ma * a.fuel + mb * b.fuel - c * (b.rho * b.fuel - a.rho * a.fuel))}; + .5f * (ma * a.fuel + mb * b.fuel - c * (b.rho * b.fuel - a.rho * a.fuel)),.5f*(ma*a.fuel_equivalent+mb*b.fuel_equivalent-c*(b.rho*b.fuel_equivalent-a.rho*a.fuel_equivalent))}; } }; @@ -668,7 +698,7 @@ private: struct JunctionNode { std::vector ends; std::vector states; - struct Face { double pressure{},rho{},velocity{},mass{},enthalpy{},air{},fuel{},loss{}; }; + struct Face { double pressure{},rho{},velocity{},mass{},enthalpy{},air{},fuel{},loss{},equivalent{}; }; std::vector faces; JunctionMethod method{JunctionMethod::Characteristic}; double turning_loss{}; @@ -689,7 +719,7 @@ private: s.velocity*=e.left?-1:1;node.states[k]=s; speed=std::max(speed,2*(std::sqrt(gamma*s.pressure/s.rho)+std::abs(double(s.velocity)))); } - double area_sum=0,rho=0,energy=0,air=0,fuel=0; + double area_sum=0,rho=0,energy=0,air=0,fuel=0,equivalent=0; for(std::size_t k=0;k0)equivalent+=f.mass*f.equivalent; if(f.mass>0) { incoming+=f.mass; heat+=f.mass*f.enthalpy; air+=f.mass*q.air; fuel+=f.mass*q.fuel; } } double balance=0; @@ -769,6 +803,7 @@ private: const auto e=node.ends[k]; const auto& pipe=pipes_[e.pipe]; f.mass=pipe.cell_area(end(pipe,e.left))*f.rho*f.velocity; f.enthalpy=h; f.air=air/incoming; f.fuel=fuel/incoming; + f.equivalent=equivalent/incoming; } balance+=f.mass; } @@ -808,12 +843,13 @@ private: } } } - double incoming=0, outgoing=0, heat=0, air=0, fuel=0, scale=1; + double incoming=0, outgoing=0, heat=0, air=0, fuel=0, scale=1,equivalent=0; for(std::size_t k=0;k=.98*std::sqrt(gamma*f.pressure/f.rho)) return false; if(f.mass>0) { incoming+=f.mass;heat+=f.mass*f.enthalpy;air+=f.mass*f.air;fuel+=f.mass*f.fuel; } + if(f.mass>0)equivalent+=f.mass*f.equivalent; else outgoing-=f.mass; } if(incoming>1e-16 && outgoing<=1e-16) { @@ -821,12 +857,14 @@ private: // Residual tolerance at rest must not create a one-sided mass source. for(auto& face:node.faces) face.mass=0; incoming=heat=air=fuel=0; + equivalent=0; } // Remove root roundoff by scaling all outflows together. A shared donor // budget scales advective fluxes together, preserving node conservation. for(std::size_t k=0;k0?incoming/outgoing:0; f.enthalpy=incoming>0?heat/incoming:0; f.air=incoming>0?air/incoming:0; f.fuel=incoming>0?fuel/incoming:0; } + if(f.mass<0)f.equivalent=incoming>0?equivalent/incoming:0; if(f.mass>0) { const auto e=node.ends[k]; const auto& pipe=pipes_[e.pipe];const int i=end(pipe,e.left); scale=std::min(scale,.25*pipe.cell_mass(i)/(f.mass*dt)); @@ -838,7 +876,7 @@ private: const auto e=node.ends[k]; auto& pipe=pipes_[e.pipe]; const auto& f=node.faces[k]; const double orientation=e.left?-1:1, m=f.mass*scale; const double momentum=m*f.velocity+f.pressure*pipe.cell_area(end(pipe,e.left)); - pipe.set_boundary_flux(e.left,{float(orientation*m),float(momentum),float(orientation*m*f.enthalpy),float(orientation*m*f.air),float(orientation*m*f.fuel)}); + pipe.set_boundary_flux(e.left,{float(orientation*m),float(momentum),float(orientation*m*f.enthalpy),float(orientation*m*f.air),float(orientation*m*f.fuel),float(orientation*m*f.equivalent)}); } return true; } @@ -1036,17 +1074,19 @@ private: const float direction = o.left ? 1.f : -1.f; const float species = from ? v.air_fraction : s.air; const float fuel = from ? v.fuel_fraction : s.fuel; + const float equivalent=from?std::max(v.fuel_equivalent_fraction,v.fuel_fraction):s.fuel_equivalent; const float signed_mass = direction * mdot; p.set_boundary_flux(o.left, {signed_mass, mdot * velocity + face_pressure * area + s.pressure * (face_area - area), - signed_mass * h, signed_mass * species, signed_mass * fuel}); - ghost(p, o.left, {density, direction * velocity, face_pressure, species, fuel}); + signed_mass * h, signed_mass * species, signed_mass * fuel,signed_mass*equivalent}); + ghost(p, o.left, {density, direction * velocity, face_pressure, species, fuel,equivalent}); v.mass_flow = -mdot; v.enthalpy = h; if (!from) v.air_fraction = s.air; if (!from) v.fuel_fraction = s.fuel; + if(!from)v.fuel_equivalent_fraction=s.fuel_equivalent; } void resolve_open(Open &o, float dt) { auto &p = pipes_[o.pipe]; @@ -1078,13 +1118,14 @@ private: if (!(temperature > 0)) throw std::runtime_error("ambient inflow exceeds stagnation enthalpy"); face.rho = face.pressure/(gas_constant*temperature); face.air = 1; face.fuel = 0; + face.fuel_equivalent=0; } } ghost(p, o.left, face); const float mass_flux = face.rho*face.velocity*o.area; const float enthalpy = gamma/(gamma-1)*face.pressure/face.rho + .5f*face.velocity*face.velocity; p.set_boundary_flux(o.left, {mass_flux, mass_flux*face.velocity+face.pressure*o.area, - mass_flux*enthalpy, mass_flux*face.air, mass_flux*face.fuel}); + mass_flux*enthalpy, mass_flux*face.air, mass_flux*face.fuel,mass_flux*face.fuel_equivalent}); o.last_volume_flow = direction*face.velocity*o.area; o.last_mass_flow = direction*mass_flux; o.last_face_pressure = face.pressure; @@ -1098,7 +1139,7 @@ private: const bool out = o.left ? s.velocity <= -c : s.velocity >= c; const bool ambient_inflow = o.left ? u >= 0 : u <= 0; GasState g = out ? s : GasState{rho, u, o.pressure, ambient_inflow ? 1.f : s.air, - ambient_inflow ? 0.f : s.fuel}; + ambient_inflow ? 0.f : s.fuel,ambient_inflow?0.f:s.fuel_equivalent}; ghost(p, o.left, g); const float a = std::min(o.area, p.cell_area(i)); auto f = Pipe1D::scaled(o.left ? Pipe1D::riemann(g, s) : Pipe1D::riemann(s, g), a); @@ -1123,10 +1164,10 @@ private: auto f = Pipe1D::scaled(Pipe1D::numerical_flux(sa, sb, a.solver == b.solver ? a.solver : PipeSolver::Rusanov), area); a.set_boundary_flux(o.left_a, {da * f.m, f.p + sa.pressure * (a.cell_area(ia) - area), da * f.e, - da * f.y, da * f.fuel}); + da * f.y, da * f.fuel,da*f.fuel_equivalent}); b.set_boundary_flux(o.left_b, {db * f.m, f.p + sb.pressure * (b.cell_area(ib) - area), db * f.e, - db * f.y, db * f.fuel}); + db * f.y, db * f.fuel,db*f.fuel_equivalent}); } }; diff --git a/include/FluidSim/FuelInjector.hpp b/include/FluidSim/FuelInjector.hpp new file mode 100644 index 0000000..a3675c7 --- /dev/null +++ b/include/FluidSim/FuelInjector.hpp @@ -0,0 +1,31 @@ +#pragma once +#include "FluidSim/MachinePorts.hpp" + +namespace fluidsim::port { +class Volume0D; +// Initial delivery model: calibrated regulated rail, with declared ideal or +// finite supply. Rail pressure dynamics and liquid plumbing are separate work. +struct FuelInjectorConfig { + enum class Supply {IdealRail,FiniteRail}; + Supply supply{Supply::FiniteRail}; + double maximum_mass_flow{.001}; // kg/s at the calibrated rail pressure + double initial_fuel_mass{.1}; // kg, used only by FiniteRail + float fuel_temperature{300}; // sensible energy of the vaporized fuel +}; +struct FuelInjectorState { + bool command_fault{},saturated{},empty{}; + double mass_flow{},delivered_mass{},remaining_mass{}; +}; +class FuelInjector { +public: + explicit FuelInjector(FuelInjectorConfig config={}); + // Continuous/batch-average delivery, not a sequential pulse-width model. + // Adds conserved fuel species, tracer and sensible energy to its host. + const FuelInjectorState& deliver(Volume0D& host,const SignalValue& command, + double time,double dt); + const FuelInjectorState& state()const{return state_;} +private: + FuelInjectorConfig config_; + FuelInjectorState state_; +}; +} diff --git a/include/FluidSim/Machine.hpp b/include/FluidSim/Machine.hpp new file mode 100644 index 0000000..286eefd --- /dev/null +++ b/include/FluidSim/Machine.hpp @@ -0,0 +1,288 @@ +#pragma once +#include "FluidSim/Engine.hpp" +#include "FluidSim/PhysicalGeometry.hpp" +#include "FluidSim/PipeSolver.hpp" +#include "FluidSim/OutdoorAudio.hpp" +#include "FluidSim/Ecu.hpp" +#include "FluidSim/Carburetor.hpp" +#include "FluidSim/FuelInjector.hpp" +#include "FluidSim/PipeProfile.hpp" +#include "FluidSim/MachinePorts.hpp" +#include "FluidSim/Mechanics.hpp" +#include "FluidSim/MachineSensors.hpp" +#include "FluidSim/MachineLogic.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace fluidsim::port { +namespace machine_math { +Vec3 add(Vec3,Vec3); +Vec3 subtract(Vec3,Vec3); +Vec3 multiply(Vec3,float); +float dot(Vec3,Vec3); +Vec3 cross(Vec3,Vec3); +float length(Vec3); +Vec3 unit(Vec3); +// Intrinsic XYZ Euler angles, in degrees. Rendering and picking use this transform. +Vec3 rotate(Vec3,Vec3 degrees); +Vec3 inverse_rotate(Vec3,Vec3 degrees); +Vec3 rotate_euler_world(Vec3,unsigned axis,float degrees); +} +enum class MachinePartKind { Pipe, Chamber, Junction, Throttle, CheckValve, Cylinder, PistonPump, Reservoir, Cap, Carburetor, Ecu, Shaft, Flywheel, DriveMotor, Brake }; +inline constexpr int machine_part_kind_count=15; +enum class MachineConnectionKind { Orifice, Inertance, Characteristic, Legacy, Control, Mechanical }; +inline constexpr int machine_connection_kind_count=6; +const char* machine_part_name(MachinePartKind); +const char* machine_part_key(MachinePartKind); +const char* machine_connection_name(MachineConnectionKind); +const char* machine_connection_key(MachineConnectionKind); +struct MachineShaftConfig { + float inertia{.15f},initial_speed{},initial_angle{}; + float friction_constant{},friction_viscous{},friction_quadratic{},starter_torque{}; +}; +struct MachineDriveConfig {float target_speed{100},torque{1};bool speed_control{true},enabled{true};}; +struct MachineBrakeConfig {float constant{1},viscous{},quadratic{};bool enabled{true};}; +struct MachineSensorEcuConfig { + bool enabled{}; // False preserves the explicitly labeled legacy ECU. + SensorEcuConfig::Strategy strategy{SensorEcuConfig::Strategy::Ignition}; + std::vector fuel_rpm{0,6000},fuel_tps{0,1}; + std::vector fuel_per_cycle{.000002f,.00002f,.000002f,.00002f}; + float displacement{.0003f},air_gas_constant{287},target_afr{14.7f}; + std::vector ve_rpm{0,6000},ve_pressure{20000,100000},volumetric_efficiency{.9f,.9f,.9f,.9f}; + bool operator==(const MachineSensorEcuConfig&)const=default; +}; +struct MachineInjectorConfig { + std::uint32_t ecu{}; + std::string ecu_label; + int cylinders{1}; // Number of calibrated cylinder doses delivered here. + float maximum_flow{.001f},initial_mass{.1f},temperature{300}; + bool finite_supply{true}; + bool operator==(const MachineInjectorConfig&)const=default; +}; + +struct MachinePart { + std::uint32_t id{}; + MachinePartKind kind{MachinePartKind::Pipe}; + std::string name; + Vec3 position{0,.25f,0}, rotation{}, size{.12f,.12f,.12f}; + // Pipe centerline in part-local metres. Length is always derived from it. + std::vector points{{-.15f,0,0},{.15f,0,0}}; + // One diameter per route point, linearly interpolated along each segment. + // Empty profiles are legacy/programmatic pipes using the two scalar values. + std::vector point_diameters; + float diameter{.04f}, outlet_diameter{.04f}; + float pressure{101325}, temperature{300}, opening{1}, discharge{.8f}; + bool pedal_control{true}; + float cracking_pressure{100}, phase_degrees{}, maximum_lift{.008f}; + float intake_open{350}, intake_close{580}, exhaust_open{130}, exhaust_close{370}; + CylinderConfig cylinder{}; + FourStrokeCylinder::ValveDynamics valve{}; + FourStrokeCylinder::ValveDynamics exhaust_valve{}; + FourStrokeCylinder::CamProfile intake_profile,exhaust_profile; + // Normalized body coordinates preserve imported port locations when resized. + std::vector port_coordinates; + // Stable identities paired with custom chamber/junction port coordinates. + std::vector port_keys; + EcuConfig ecu; + MachineSensorEcuConfig sensor_ecu; + std::optional injector; // Explicit chamber attachment. + float ignition_tdc_degrees{}; // Calibrated crank pickup angle, 0..360. + CarburetorConfig carburetor; + bool injection_enabled{true}; + // Junction method 0 stores gas in its visible box; 1/2 are zero-volume nodes. + int junction_method{}; + float turning_loss{}; + MachineShaftConfig shaft; + MachineDriveConfig drive; + MachineBrakeConfig brake; +}; +struct MachinePort { + std::uint32_t part{}; + unsigned port{}; + bool operator==(const MachinePort&) const = default; +}; +SensorEcuConfig machine_sensor_ecu_config(const MachinePart&); +struct MachineLink { + MachinePort a,b; + MachineConnectionKind kind{MachineConnectionKind::Orifice}; + float discharge{1}, inertance_length{.01f}, loss{}; +}; +enum class MachineQuantity {Pressure,Temperature,MassFlow,AirFuelRatio,Lambda,ChargeAfr,Rpm,Torque,Power}; +inline constexpr std::size_t machine_probe_limit=8; +inline constexpr float machine_afr_reading_limit=1000.f; +enum class MachineReadingState {Valid,NoFuel,LeanLimit,FuelCut,AwaitingCharge}; +const char* machine_reading_state_name(MachineReadingState); +const char* machine_quantity_name(MachineQuantity); +const char* machine_quantity_unit(MachineQuantity); +float machine_quantity_scale(MachineQuantity); +bool machine_quantity_supported(const MachinePart&,MachineQuantity); +struct MachineProbe { + std::uint32_t id{},part{}; + std::string name; + MachineQuantity quantity{MachineQuantity::Pressure}; + float position{.5f}; // fraction of pipe arclength; lumped parts ignore it + float reference_afr{14.7f}; // stoichiometric AFR for the selected fuel + bool generated_output{}; // Migrated legacy shaft torque/power excludes bench motor/load. + bool operator==(const MachineProbe&)const=default; +}; +struct MachineMeasurement { + double time{}; + std::array values{}; // SI; undefined readings are NaN + std::array states{}; + std::size_t count{}; +}; +struct MachineDocument { + std::string name{"Untitled machine"}; + float cells_per_meter{40}, drive_rpm{}, shaft_inertia{.15f}, load_torque{}; + bool driven{}; + // Deprecated calibration fields above are read only by legacy migration. + std::uint32_t primary_shaft{}; + float friction_constant{.15f},friction_viscous{.002f},friction_quadratic{},starter_torque{15}; + PipeSolver solver{PipeSolver::Rusanov}; + OutdoorScene scene{OutdoorScene::Wall}; + std::uint32_t next_id{1}; + std::vector parts; + std::vector links; + std::vector probes; + std::vector sensors; + std::uint32_t next_sensor_id{1}; + LogicGraph logic; + MachinePart* find(std::uint32_t); + const MachinePart* find(std::uint32_t) const; + MachinePart& add(MachinePartKind,Vec3 position={0,.25f,0}); + MachineSensor& add_sensor(MachineSensorKind,std::uint32_t host,float position=.5f); + MachineSensor* find_sensor(std::uint32_t); + const MachineSensor* find_sensor(std::uint32_t)const; + void remove_sensor(std::uint32_t); + void remove(std::uint32_t); + void disconnect(MachinePort); + // At least one endpoint must be a pipe. Its centerline snaps to the other port. + void connect(MachinePort,MachinePort,MachineConnectionKind=MachineConnectionKind::Orifice); + void sync_connections(std::uint32_t moved_part=0); + void transform_selection(const std::vector& ids,Vec3 pivot,Vec3 translation,unsigned rotation_axis=0,float degrees=0); +}; +bool machine_sensor_supported(const MachinePart&,MachineSensorKind,bool reference=false); +MachineSensorStatus machine_sensor_binding(const MachineDocument&,const MachineSensor&); +std::optional machine_sensor_position(const MachineDocument&,const MachineSensorTap&); +void validate_machine_sensors(const MachineDocument&); +bool machine_control_port(const MachinePart&,unsigned); +MachinePortDescriptor machine_port_descriptor(const MachinePart&,unsigned); +unsigned machine_port_index(const MachinePart&,const std::string& key); +PortDomain machine_link_domain(const MachineDocument&,const MachineLink&); +bool machine_rotational_part(MachinePartKind); +std::uint32_t machine_shaft_for(const MachineDocument&,std::uint32_t part); +std::uint32_t machine_primary_shaft(const MachineDocument&); +bool machine_shaft_driven(const MachineDocument&,std::uint32_t shaft); +// Convert an old shared-shaft document to visible mechanical parts once. +void upgrade_machine_mechanics(MachineDocument&); +bool machine_port_available(const MachineDocument&,MachinePort); +unsigned machine_port_count(const MachinePart&); +std::string machine_port_name(const MachinePart&,unsigned); +Vec3 machine_local_port(const MachinePart&,unsigned); +Vec3 machine_world_point(const MachinePart&,Vec3); +Vec3 machine_local_point(const MachinePart&,Vec3); +Vec3 machine_port_position(const MachinePart&,unsigned); +std::vector machine_pipe_path(const MachinePart&); +float machine_pipe_length(const MachinePart&); +std::vector machine_pipe_diameters(const MachinePart&); +std::vector machine_pipe_sections(const MachinePart&); +float machine_pipe_diameter(const MachinePart&,float distance); +void machine_insert_pipe_point(MachinePart&,std::size_t segment,Vec3 local_point); +void machine_remove_pipe_point(MachinePart&,std::size_t point); +float machine_part_volume(const MachinePart&); +Vec3 machine_part_size(const MachinePart&); +// Drafts may contain unconnected ports. Unconnected duct ends are explicitly capped at test time. +void validate_machine(const MachineDocument&); +std::vector machine_diagnostics(const MachineDocument&); +struct MachineConnectionPreview { + bool allowed{}; + std::string message; + std::optional pipe; +}; +// Dry-run the actual connection and validation without changing the design. +MachineConnectionPreview machine_connection_preview(const MachineDocument&,MachinePort,MachinePort); +bool machine_has_fuel_source(const MachineDocument&,std::uint32_t cylinder); +struct MachineCheck { + std::uint32_t part{}; + bool warning{}; + std::string title,advice; + std::uint32_t sensor{}; + std::uint32_t logic_node{}; +}; +std::vector machine_checks(const MachineDocument&); +std::filesystem::path machine_directory(); +std::vector machine_files(); +std::filesystem::path machine_assembly_directory(); +std::vector machine_assembly_files(); +// Center of the selected world geometry, including routed pipes. +Vec3 machine_selection_center(const MachineDocument&,const std::vector&); +// Stores only selected parts, internal links and their probes, relative to the center. +MachineDocument machine_extract_assembly(const MachineDocument&,const std::vector&,std::string name={}); +// Inserts an independent copy atomically; returned IDs follow assembly.parts order. +std::vector machine_insert_assembly(MachineDocument&,const MachineDocument&,Vec3 center); +MachineDocument load_machine(const std::filesystem::path&); +void save_machine(const MachineDocument&,const std::filesystem::path&); +MachineDocument machine_pump_example(); +MachineDocument machine_engine_example(); +MachineDocument machine_logic_example(); +MachineDocument machine_sensor_engine_example(bool injection=false); +MachineDocument machine_speed_density_example(); +struct ScenarioExample; +MachineDocument machine_from_engine(const ScenarioExample&); + +struct MachinePartState { + std::uint32_t id{}; + float pressure{101325}, temperature{300}, volume{}, flow{}, piston_fraction{}, intake_lift{}, exhaust_lift{}, opening{}; + std::vector pipe_pressures; + float fuel_mass{},fuel_flow{},afr{},burn_fraction{}; + bool combustion{},fuel_cut{},ignition_cut{},idle_control{},valve_float{},fuel_source{}; + bool actuator_enabled{},signal_controlled{}; + double command_target{}; + double injector_remaining{},injector_delivered{}; + bool sensor_fault{},rev_limited{},injector_fault{},injector_empty{}; + float spark_advance{}; + std::uint32_t shaft{}; + double shaft_angle{},shaft_speed{},rotational_inertia{},torque{},power{},kinetic_energy{},mechanical_work{},mechanical_loss{}; +}; +struct MachineSnapshot { + double time{}; + float rpm{}, torque{}, power{}; + std::uint64_t cells{},substeps{},positivity_repairs{}; + float minimum_cell_length{}; + std::uint32_t minimum_cell_part{}; + std::vector parts; + std::uint32_t primary_shaft{}; + std::vector shafts; + std::vector sensors; + std::vector logic; +}; +// Owns the compiled graph. Editing never mutates a running solver; Test compiles a fresh graph. +class MachineSimulation { +public: + explicit MachineSimulation(const MachineDocument&,int sample_rate=44100); + ~MachineSimulation(); + MachineSimulation(const MachineSimulation&)=delete; + MachineSimulation& operator=(const MachineSimulation&)=delete; + AudioFrame process(); + void set_controls(float pedal,bool starter=false); + void set_listener(AudioListener); + MachineSnapshot snapshot() const; + MachineMeasurement measure() const; + // Latest sample-boundary values, independent of recorded instrument channels. + MachineSensorState sense(std::uint32_t sensor)const; + void set_logic_control(std::uint32_t node,bool); + void set_logic_control(std::uint32_t node,double); + void release_logic_buttons(); + // Applies safe tuning and instrument changes between samples, preserving state. + // Geometry, topology, initial conditions and compiled boundary settings must match. + void apply_tuning(const MachineDocument&); +private: + struct Impl; + std::unique_ptr impl_; +}; +} diff --git a/include/FluidSim/MachineLogic.hpp b/include/FluidSim/MachineLogic.hpp new file mode 100644 index 0000000..a577fe1 --- /dev/null +++ b/include/FluidSim/MachineLogic.hpp @@ -0,0 +1,111 @@ +#pragma once +#include "FluidSim/MachineSensors.hpp" +#include "FluidSim/Ecu.hpp" +#include +#include +#include +#include +#include + +namespace fluidsim::port { +struct MachineDocument; +enum class LogicNodeKind { + BooleanConstant,Switch,Button,NumberConstant,Slider,NumberInput, + And,Or,Not,Xor,Nand,Nor,Greater,Less,Equal,Window,Hysteresis, + Add,Subtract,Multiply,Divide,Minimum,Maximum,Absolute,Clamp,GainOffset,MapRange,Select,BooleanToNumber, + Sensor,Command,Lamp,Readout,BooleanDelay,NumberDelay,Ecu +}; +inline constexpr int logic_node_kind_count=36; +enum class LogicCommand {MotorEnable,MotorSpeed,MotorTorque,BrakeEnable,ThrottleOpening,Starter}; +inline constexpr int logic_command_count=6; +struct LogicPosition {float x{},y{};bool operator==(const LogicPosition&)const=default;}; +struct LogicNode { + std::uint32_t id{}; + LogicNodeKind kind{LogicNodeKind::Switch}; + std::string name; + LogicPosition position; + SignalQuantity quantity{SignalQuantity::Scalar},output_quantity{SignalQuantity::Scalar}; + PressureReference pressure_reference{PressureReference::None},output_pressure_reference{PressureReference::None}; + double value{},minimum{},maximum{1},low{.25},high{.75},tolerance{1e-6}; + double gain{1},offset{},false_value{},true_value{1},input_low{},input_high{1},output_low{},output_high{1}; + bool boolean{},initial{},below{},clamp{true}; + unsigned input_count{2}; + std::uint32_t sensor{},part{}; + MachineSensorKind sensor_kind{MachineSensorKind::Pressure}; // Schema survives an unresolved reference. + SensorEcuConfig::Strategy ecu_strategy{SensorEcuConfig::Strategy::Ignition}; + LogicCommand command{LogicCommand::MotorEnable}; + std::string binding_label; + bool operator==(const LogicNode&)const=default; +}; +struct LogicEndpoint { + std::uint32_t node{}; + std::string port; + bool operator==(const LogicEndpoint&)const=default; +}; +struct LogicLink { + LogicEndpoint from,to; + bool operator==(const LogicLink&)const=default; +}; +struct LogicGraph { + std::uint32_t next_id{1}; + std::vector nodes; + std::vector links; + LogicNode* find(std::uint32_t); + const LogicNode* find(std::uint32_t)const; + LogicNode& add(LogicNodeKind,LogicPosition={}); + void remove(std::uint32_t); + void disconnect(const LogicEndpoint&); + void connect(LogicEndpoint,LogicEndpoint); // Canonical output -> input; validates without mutation on failure. + bool operator==(const LogicGraph&)const=default; +}; +const char* logic_node_key(LogicNodeKind); +const char* logic_node_name(LogicNodeKind); +const char* logic_command_key(LogicCommand); +const char* logic_command_name(LogicCommand); +bool logic_memory_node(LogicNodeKind); +std::vector logic_node_ports(const LogicNode&,const MachineDocument* context=nullptr); +MachinePortDescriptor logic_port(const LogicGraph&,const LogicEndpoint&); +PortCompatibility logic_connection_preview(const LogicGraph&,LogicEndpoint,LogicEndpoint); +void validate_logic_graph(const LogicGraph&); // Drafts may have required unconnected inputs. +std::vector logic_evaluation_order(const LogicGraph&,std::vector* cycle_path=nullptr); +std::vector logic_required_inputs(const LogicGraph&); +bool logic_topology_equal(const LogicGraph&,const LogicGraph&); +// Empty means a compatible physical binding. Missing targets remain unresolved. +std::string logic_binding_problem(const MachineDocument&,const LogicNode&); +const LogicNode* logic_command_owner(const MachineDocument&,std::uint32_t part,LogicCommand); +void validate_machine_logic(const MachineDocument&); +struct LogicNodeState { + std::uint32_t id{}; + std::vector values; // Same order as logic_node_ports, including inputs. + std::string problem; +}; +struct LogicCommandState { + std::uint32_t node{},part{}; + LogicCommand command{}; + SignalValue value; // Invalid values require the declared inactive consumer fallback. +}; +struct LogicEcuInputs { + std::uint32_t node{},part{}; + SignalValue speed,angle,tps,map,iat; +}; +class MachineLogicRuntime { +public: + using SensorReader=std::function; + explicit MachineLogicRuntime(const MachineDocument&,SensorReader={}); + ~MachineLogicRuntime(); + MachineLogicRuntime(const MachineLogicRuntime&)=delete; + MachineLogicRuntime& operator=(const MachineLogicRuntime&)=delete; + void evaluate(double time,double dt); + SignalValue value(const LogicEndpoint&)const; + std::vector snapshot()const; + const std::vector& commands()const; + const std::vector& ecu_inputs()const; + void set_manual(std::uint32_t,bool); + void set_manual(std::uint32_t,double); + void release_buttons(); + void apply_tuning(const MachineDocument&); +private: + struct Impl; + std::unique_ptr impl_; +}; +} diff --git a/include/FluidSim/MachinePorts.hpp b/include/FluidSim/MachinePorts.hpp new file mode 100644 index 0000000..c66c793 --- /dev/null +++ b/include/FluidSim/MachinePorts.hpp @@ -0,0 +1,51 @@ +#pragma once +#include +#include +#include +#include + +namespace fluidsim::port { +enum class PortDomain {Gas,Rotational,Translational,Boolean,Number,EcuAssignment}; +enum class PortDirection {Bidirectional,Input,Output}; +enum class PortCardinality {Single,Multiple}; +// Quantities carry meaning as well as dimensions. Opening and scalar, or crank +// angle and cycle phase, require an explicit conversion even when units match. +enum class SignalQuantity {None,Scalar,Fraction,Pressure,Temperature,MassFlow,FreshAirMassFlow,AngularSpeed,Angle,CyclePhase,Torque,Power,Position,Velocity,Force,Time,Inertia}; +enum class PressureReference {None,Absolute,Gauge,Differential}; +enum class SignalValidity {Valid,Missing,Fault}; +// Runtime-only pickup evidence. Arithmetic creates new values without timing +// authority; routing retains the original acquisition time, including delays. +struct SignalTiming { + std::uint32_t sensor{},shaft{}; + double sampled_at{},unwrapped_angle{},angular_speed{}; + bool operator==(const SignalTiming&)const=default; +}; +struct SignalValue { + std::variant value{false}; + SignalValidity validity{SignalValidity::Missing}; + double timestamp{}; + std::optional timing; + static SignalValue boolean(bool,double timestamp=0); + static SignalValue number(double,double timestamp=0); +}; +struct MachinePortDescriptor { + std::string key,name; + PortDomain domain{PortDomain::Gas}; + PortDirection direction{PortDirection::Bidirectional}; + PortCardinality cardinality{PortCardinality::Single}; + SignalQuantity quantity{SignalQuantity::None}; + PressureReference pressure_reference{PressureReference::None}; + bool required{}; + std::variant default_value; + std::optional minimum,maximum; + bool operator==(const MachinePortDescriptor&)const=default; +}; +struct PortCompatibility {bool allowed{};std::string message;}; +const char* port_domain_key(PortDomain); +const char* signal_quantity_key(SignalQuantity); +const char* signal_quantity_unit(SignalQuantity); // Internal SI units. +bool signal_domain(PortDomain); +void validate_port_descriptor(const MachinePortDescriptor&); +PortCompatibility port_compatibility(const MachinePortDescriptor&,const MachinePortDescriptor&); +bool signal_value_valid_for(const SignalValue&,const MachinePortDescriptor&); +} diff --git a/include/FluidSim/MachineSensors.hpp b/include/FluidSim/MachineSensors.hpp new file mode 100644 index 0000000..9880160 --- /dev/null +++ b/include/FluidSim/MachineSensors.hpp @@ -0,0 +1,40 @@ +#pragma once +#include "FluidSim/MachinePorts.hpp" +#include +#include +#include + +namespace fluidsim::port { +enum class MachineSensorKind {Pressure,DifferentialPressure,GasTemperature,MassFlow,Crank,ThrottlePosition}; +inline constexpr int machine_sensor_kind_count=6; +struct MachineSensorTap { + std::uint32_t part{}; + float position{.5f}; // Normalized pipe arclength, independent of fluid cells. + std::string label; // Retains the intended host when a binding is unresolved. + bool operator==(const MachineSensorTap&)const=default; +}; +struct MachineSensor { + std::uint32_t id{}; + MachineSensorKind kind{MachineSensorKind::Pressure}; + std::string name; + MachineSensorTap target,reference; + float angle_offset{}; // Radians, added to the physical shaft angle. + bool operator==(const MachineSensor&)const=default; +}; +enum class MachineSensorStatus {Valid,MissingHost,IncompatibleHost,Fault}; +struct MachineSensorState { + std::uint32_t id{}; + std::array outputs; + unsigned count{}; + MachineSensorStatus status{MachineSensorStatus::MissingHost}; + // An ideal crank pickup preserves provenance and unwrapped crossings. An + // arbitrary numeric RPM signal must never acquire timing authority from it. + std::uint32_t timing_shaft{}; + double unwrapped_angle{}; +}; +const char* machine_sensor_key(MachineSensorKind); +const char* machine_sensor_name(MachineSensorKind); +const char* machine_sensor_status_name(MachineSensorStatus); +unsigned machine_sensor_output_count(MachineSensorKind); +MachinePortDescriptor machine_sensor_output(const MachineSensor&,unsigned); +} diff --git a/include/FluidSim/Mechanics.hpp b/include/FluidSim/Mechanics.hpp new file mode 100644 index 0000000..1e263ea --- /dev/null +++ b/include/FluidSim/Mechanics.hpp @@ -0,0 +1,54 @@ +#pragma once +#include +#include + +namespace fluidsim::port { +// Rotational quantities use SI: rad, rad/s, N m, kg m2, J and W. +enum class DriveMode {Torque,Speed}; +struct ShaftParameters { + double inertia{.15},initial_speed{},initial_angle{}; + double friction_constant{},friction_viscous{},friction_quadratic{}; + bool forward_only{}; +}; +struct DriveParameters {DriveMode mode{DriveMode::Torque};double command{};bool enabled{true};}; +struct BrakeParameters {double constant{},viscous{},quadratic{};bool enabled{true};}; +struct ShaftState { + std::uint32_t id{}; + double angle{},previous_angle{},speed{},inertia{},kinetic_energy{}; + double net_torque{},source_torque{},external_torque{},friction_torque{},brake_torque{},constraint_torque{}; + double external_work{},drive_work{},friction_loss{},brake_loss{},initial_energy{}; + // initial_energy + external_work + drive_work = kinetic_energy + losses. +}; +struct DriveState { + std::uint32_t id{},shaft{}; + double torque{},power{},work{}; +}; +struct BrakeState { + std::uint32_t id{},shaft{}; + double torque{},power{},loss{}; // Torque on shaft signed; absorbed power >= 0. +}; +// Independent rigid shaft nodes. Gears and clutches will couple these nodes; +// direct attachment adds torque or inertia to one named node exactly once. +class MechanicalSystem { +public: + MechanicalSystem(); + ~MechanicalSystem(); + MechanicalSystem(const MechanicalSystem&)=delete; + MechanicalSystem& operator=(const MechanicalSystem&)=delete; + void add_shaft(std::uint32_t,const ShaftParameters&); + void add_flywheel(std::uint32_t id,std::uint32_t shaft,double inertia); + void add_drive(std::uint32_t id,std::uint32_t shaft,const DriveParameters&); + void add_brake(std::uint32_t id,std::uint32_t shaft,const BrakeParameters&); + void set_drive(std::uint32_t,const DriveParameters&); + void set_brake(std::uint32_t,const BrakeParameters&); + void set_friction(std::uint32_t shaft,double constant,double viscous,double quadratic); + void add_torque(std::uint32_t shaft,double torque); + void step(double dt); + const ShaftState& shaft(std::uint32_t)const; + const DriveState& drive(std::uint32_t)const; + const BrakeState& brake(std::uint32_t)const; +private: + struct Impl; + std::unique_ptr impl_; +}; +} diff --git a/include/FluidSim/PipeProfile.hpp b/include/FluidSim/PipeProfile.hpp index 26af987..3ddf1b4 100644 --- a/include/FluidSim/PipeProfile.hpp +++ b/include/FluidSim/PipeProfile.hpp @@ -35,4 +35,31 @@ inline Pipe1D make_profiled_pipe(std::span sections, float ma } return Pipe1D(std::move(areas), std::move(lengths)); } + +// Uniform cells over the full arclength, integrating every profile section that +// a cell crosses. Route/profile knots do not force short cells; gas volume is +// still the exact sum of the piecewise frustums at any mesh density. +inline Pipe1D make_uniform_profiled_pipe(std::span sections,float maximum_cell_length=.01f){ + if(!std::isfinite(maximum_cell_length)||maximum_cell_length<=0||sections.empty())throw std::invalid_argument("invalid pipe profile mesh"); + double total=0;float previous=sections.front().inlet_diameter; + for(const auto& s:sections){ + if(!std::isfinite(s.length)||!std::isfinite(s.inlet_diameter)||!std::isfinite(s.outlet_diameter)||s.length<=0||s.inlet_diameter<=0||s.outlet_diameter<=0||std::abs(s.inlet_diameter-previous)>1e-6f)throw std::invalid_argument("invalid/discontinuous pipe profile"); + total+=s.length;previous=s.outlet_diameter; + } + const int count=profile_section_cells(float(total),maximum_cell_length);const double dx=total/count; + std::vector areas;areas.reserve(count);std::vector lengths(count,float(dx)); + std::size_t section=0;double section_start=0,section_end=sections.front().length; + for(int i=0;i=section_end){section_start=section_end;section_end+=sections[++section].length;} + const auto& s=sections[section];const double stop=std::min(end,section_end); + const double d0=std::lerp(double(s.inlet_diameter),double(s.outlet_diameter),std::clamp((cursor-section_start)/s.length,0.,1.)); + const double d1=std::lerp(double(s.inlet_diameter),double(s.outlet_diameter),std::clamp((stop-section_start)/s.length,0.,1.)); + volume+=double(pi)/12*(stop-cursor)*(d0*d0+d0*d1+d1*d1);cursor=stop; + } + areas.push_back(float(volume/dx)); + } + return Pipe1D(std::move(areas),std::move(lengths)); +} } // namespace fluidsim::port diff --git a/include/FluidSim/SimulationRingBuffer.hpp b/include/FluidSim/SimulationRingBuffer.hpp index 2a68dc8..07ae9c0 100644 --- a/include/FluidSim/SimulationRingBuffer.hpp +++ b/include/FluidSim/SimulationRingBuffer.hpp @@ -19,6 +19,8 @@ public: bool write(float left, float right, std::stop_token stop = {}); bool wait_for_space(std::size_t target_fill, std::stop_token stop); void reset_consumer(); + // Only while both producer and audio consumer are stopped. + void discard(); std::size_t read_interpolated(std::span left, std::span right, double speed); private: diff --git a/machines/air-compressor.machine.toml b/machines/air-compressor.machine.toml new file mode 100644 index 0000000..81fe514 --- /dev/null +++ b/machines/air-compressor.machine.toml @@ -0,0 +1,1117 @@ +cells_per_meter = 20.0 +format = 8 +name = 'Air Compressor' +next_id = 21 +next_sensor_id = 2 +primary_shaft = 6 +scene = 0 +solver = 'muscl' + +[logic] +next_id = 1 +nodes = [] +wires = [] + +[[connections]] +a_part = 6 +a_port = 'shaft' +b_part = 7 +b_port = 'shaft' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'mechanical' + +[[connections]] +a_part = 9 +a_port = 'end' +b_part = 1 +b_port = 'face_x_negative' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 9 +a_port = 'start' +b_part = 8 +b_port = 'outlet' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 10 +a_port = 'end' +b_part = 8 +b_port = 'inlet' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 10 +a_port = 'start' +b_part = 15 +b_port = 'gas' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 16 +a_port = 'start' +b_part = 14 +b_port = 'inlet' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 16 +a_port = 'end' +b_part = 3 +b_port = 'gas' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 20 +a_port = 'start' +b_part = 18 +b_port = 'inlet' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 20 +a_port = 'end' +b_part = 14 +b_port = 'outlet' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 19 +a_port = 'end' +b_part = 18 +b_port = 'outlet' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 19 +a_port = 'start' +b_part = 1 +b_port = 'face_x_positive' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 6 +a_port = 'shaft' +b_part = 18 +b_port = 'shaft' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'mechanical' + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 1 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Chamber / tank 1' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.21000000834465027, 0.25, 0.0099999997764825821 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.13000006973743439, 0.12500005960464478, 0.14000004529953003 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 3 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pressure / atmosphere 3' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.21000000834465027, 0.25, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 6 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'shaft' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Main crankshaft' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.019999999552965164, 0.05000000074505806, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.25, 0.035000000149011612, 0.035000000149011612 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.shaft] + friction_nm = 1.0 + inertia_kg_m2 = 0.10000000149011612 + initial_angle_rad = 0.0 + initial_speed_rad_s = 94.247779846191406 + quadratic_nm_s2 = 0.0 + starter_nm = 15.0 + viscous_nm_s = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 7 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'drive_motor' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Bench speed drive' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.2800000011920929, 0.049999997019767761, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.090000003576278687, 0.090000003576278687 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.drive] + enabled = true + speed_control = true + target_speed_rad_s = 125.66371154785156 + torque_nm = 1.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 8 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'throttle' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Throttle valve 8' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.3399999737739563, 0.24999998509883881, 0.010000020265579224 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.05000000074505806, 0.05000000074505806 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 9 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 9' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.019999999552965164, 0.019999999552965164 ] +points_m = [ [ -0.0099999606609344482, -1.4901161193847656e-08, -1.4399999380111694 ], [ 0.034999966621398926, 0.0, -1.4399999380111694 ] ] +port_coordinates = [] +position_m = [ -0.31000000238418579, 0.25, 1.4499999284744263 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 10 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 10' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.019999999552965164, 0.019999999552965164 ] +points_m = [ [ 0.31999999284744263, -1.4901161193847656e-08, -0.070000000298023224 ], [ 0.38000002503395081, -1.4901161193847656e-08, -0.069999977946281433 ] ] +port_coordinates = [] +position_m = [ -0.74000000953674316, 0.25, 0.079999998211860657 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 14 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'check_valve' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Check valve 14' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.060000017285346985, 0.25, 0.010000016540288925 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 180.0, 5.0089556680177338e-06, 180.0 ] +size_m = [ 0.019999999552965164, 0.040000002831220627, 0.040000002831220627 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 15 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pressure / atmosphere 15' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.42000001668930054, 0.24999998509883881, 0.0099999960511922836 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 16 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 16' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.019999999552965164, 0.019999999552965164 ] +points_m = [ [ -0.52999997138977051, 0.0, -0.099999979138374329 ], [ -0.38999995589256287, 0.0, -0.10999999940395355 ] ] +port_coordinates = [] +position_m = [ 0.59999996423721313, 0.25, 0.10999999940395355 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 10.0 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 18 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'piston_pump' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Piston pump 18' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.029999997466802597, 0.1900000125169754, 0.0099999997764825821 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 180.0, 5.0089556680177338e-06, 180.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.10000000149011612 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 19 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 19' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.019999999552965164, 0.019999999552965164 ] +points_m = [ [ 0.11500000953674316, 0.0, 0.54999995231628418 ], [ 0.21274998784065247, -0.0044444352388381958, 0.54999995231628418 ] ] +port_coordinates = [] +position_m = [ -0.25999999046325684, 0.25, -0.53999996185302734 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 20 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 20' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.019999999552965164, 0.019999999552965164 ] +points_m = [ [ -0.31274998188018799, -0.0044444352388381958, 0.6799999475479126 ], [ -0.24999997019767761, 0.0, 0.6799999475479126 ] ] +port_coordinates = [] +position_m = [ 0.29999998211860657, 0.25, -0.66999995708465576 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[probes]] +generated_output = false +id = 1 +name = 'Pressure / Chamber / tank 1' +part = 1 +position = 0.5 +quantity = 0 +reference_afr = 14.699999809265137 + +[[sensors]] +angle_offset_rad = 0.0 +id = 1 +kind = 'pressure' +name = 'Pressure / MAP / Chamber / tank 1' + + [sensors.reference] + label = '' + part = 0 + position = 0.5 + + [sensors.target] + label = 'Chamber / tank 1' + part = 1 + position = 0.5 diff --git a/machines/assemblies/3-cyl-header.assembly.toml b/machines/assemblies/3-cyl-header.assembly.toml new file mode 100644 index 0000000..831e711 --- /dev/null +++ b/machines/assemblies/3-cyl-header.assembly.toml @@ -0,0 +1,422 @@ +cells_per_meter = 10.0 +drive_rpm = 0.0 +driven = false +format = 4 +friction_constant = 0.15000000596046448 +friction_quadratic = 0.0 +friction_viscous = 0.0020000000949949026 +load_torque = 0.0 +name = '3-cyl-header' +scene = 2 +shaft_inertia = 0.15000000596046448 +solver = 'rusanov' +starter_torque = 15.0 + +[[connections]] +a_part = 31 +a_port = 1 +b_part = 30 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 32 +a_port = 0 +b_part = 30 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 33 +a_port = 1 +b_part = 30 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 50 +a_port = 0 +b_part = 30 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 30 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 1 +kind = 'junction' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Junction 30' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.01974998414516449, -0.032500006258487701, 0.042499996721744537 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.059999998658895493, 0.059999998658895493, 0.059999998658895493 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 31 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 31' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.02500000037252903, 0.02500000037252903, 0.02500000037252903 ] +points_m = [ [ 0.13300000131130219, -0.29750001430511475, -0.59825778007507324 ], [ 0.15052753686904907, -0.33734250068664551, -0.59909915924072266 ], [ 0.18999999761581421, -0.37000000476837158, -0.60000002384185791 ] ] +port_coordinates = [] +position_m = [ -0.2097499817609787, 0.33749997615814209, 0.64249998331069946 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 32 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 32' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.02500000037252903, 0.02500000037252903, 0.02500000037252903, 0.02500000037252903 ] +points_m = [ [ -0.36999994516372681, -0.37000000476837158, -0.52000004053115845 ], [ -0.4761640727519989, -0.34520614147186279, -0.51906377077102661 ], [ -0.52133440971374512, -0.32712483406066895, -0.5185772180557251 ], [ -0.53999996185302734, -0.29750001430511475, -0.51825779676437378 ] ] +port_coordinates = [] +position_m = [ 0.35024997591972351, 0.33749997615814209, 0.5625 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 33 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 33' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.02500000037252903, 0.02500000037252903, 0.02500000037252903, 0.02500000037252903 ] +points_m = [ [ 0.16699999570846558, -0.29750001430511475, 0.14174219965934753 ], [ 0.21503587067127228, -0.35698139667510986, 0.14158914983272552 ], [ 0.25817635655403137, -0.36491745710372925, 0.14149604737758636 ], [ 0.44999998807907104, -0.37000000476837158, 0.13999998569488525 ] ] +port_coordinates = [] +position_m = [ -0.46974998712539673, 0.33749997615814209, -0.09749998152256012 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 50 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 50' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.029999999329447746, 0.029999999329447746 ] +points_m = [ [ -0.46999996900558472, -0.37000000476837158, -0.2200000137090683 ], [ -0.15000000596046448, -0.29999998211860657, -0.31000000238418579 ] ] +port_coordinates = [] +position_m = [ 0.45024999976158142, 0.33749997615814209, 0.26250001788139343 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[probes]] +id = 1 +name = 'Lambda / Pipe / reducer 50' +part = 50 +position = 0.5 +quantity = 4 +reference_afr = 14.699999809265137 diff --git a/machines/flat6-imported.machine.toml b/machines/flat6-imported.machine.toml new file mode 100644 index 0000000..defb15d --- /dev/null +++ b/machines/flat6-imported.machine.toml @@ -0,0 +1,3293 @@ +cells_per_meter = 10.0 +drive_rpm = 3000.0 +driven = false +format = 4 +friction_constant = 11.976249694824219 +friction_quadratic = 0.0 +friction_viscous = 0.011976259760558605 +load_torque = 0.0 +name = 'flat-six-carb' +scene = 0 +shaft_inertia = 0.20000000298023224 +solver = 'muscl' +starter_torque = 74.851593017578125 + +[[connections]] +a_part = 2 +a_port = 0 +b_part = 21 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 2 +a_port = 1 +b_part = 15 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 3 +a_port = 0 +b_part = 21 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 3 +a_port = 1 +b_part = 16 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 4 +a_port = 0 +b_part = 21 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 4 +a_port = 1 +b_part = 17 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 5 +a_port = 0 +b_part = 21 +b_port = 3 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 5 +a_port = 1 +b_part = 18 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 6 +a_port = 0 +b_part = 21 +b_port = 4 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 6 +a_port = 1 +b_part = 19 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 7 +a_port = 0 +b_part = 21 +b_port = 5 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 7 +a_port = 1 +b_part = 20 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 23 +a_port = 1 +b_part = 21 +b_port = 6 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 24 +a_port = 0 +b_part = 15 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 24 +a_port = 0 +b_part = 16 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 24 +a_port = 0 +b_part = 17 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 24 +a_port = 0 +b_part = 18 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 24 +a_port = 0 +b_part = 19 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 24 +a_port = 0 +b_part = 20 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 1 +a_port = 0 +b_part = 25 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 23 +a_port = 0 +b_part = 28 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 1 +a_port = 1 +b_part = 28 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 24 +a_port = 0 +b_part = 28 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 31 +a_port = 1 +b_part = 30 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 32 +a_port = 0 +b_part = 30 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 31 +a_port = 0 +b_part = 20 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 32 +a_port = 1 +b_part = 19 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 33 +a_port = 0 +b_part = 18 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 33 +a_port = 1 +b_part = 30 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 50 +a_port = 0 +b_part = 30 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 52 +a_port = 1 +b_part = 51 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 53 +a_port = 0 +b_part = 51 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 54 +a_port = 1 +b_part = 51 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 56 +a_port = 0 +b_part = 51 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 52 +a_port = 0 +b_part = 17 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 53 +a_port = 1 +b_part = 16 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 54 +a_port = 0 +b_part = 15 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 56 +a_port = 1 +b_part = 57 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 50 +a_port = 1 +b_part = 57 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 59 +a_port = 0 +b_part = 57 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 59 +a_port = 1 +b_part = 58 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 1 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 0 / flat6' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.039999999105930328, 0.039999999105930328 ] +points_m = [ [ -0.049999982118606567, 0.0, 0.0 ], [ 0.059500038623809814, 0.00085330009460449219, -1.862645149230957e-09 ] ] +port_coordinates = [] +position_m = [ -0.31950002908706665, 0.3091467022895813, 0.0 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 2 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 1 / flat6' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.035000000149011612, 0.035000000149011612 ] +points_m = [ [ 0.0, 0.125, 0.0 ], [ 0.0, -0.12499998509883881, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.11299999803304672, 0.16299998760223389, -0.1617421954870224 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 3 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 2 / flat6' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.035000000149011612, 0.035000000149011612 ] +points_m = [ [ 0.0, 0.125, 0.0 ], [ 0.0, -0.12499998509883881, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.0, 0.16299998760223389, -0.1617421954870224 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 4 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 3 / flat6' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.035000000149011612, 0.035000000149011612 ] +points_m = [ [ 0.0, 0.125, 0.0 ], [ 0.0, -0.12499998509883881, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.11299999803304672, 0.16299998760223389, -0.1617421954870224 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 5 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 4 / flat6' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.035000000149011612, 0.035000000149011612 ] +points_m = [ [ 0.0, 0.125, 0.0 ], [ 0.0, -0.12499998509883881, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.11299999803304672, 0.16299998760223389, 0.1617421954870224 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 6 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 5 / flat6' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.035000000149011612, 0.035000000149011612 ] +points_m = [ [ 0.0, 0.125, 0.0 ], [ 0.0, -0.12499998509883881, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.0, 0.16299998760223389, 0.1617421954870224 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 7 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 6 / flat6' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.035000000149011612, 0.035000000149011612 ] +points_m = [ [ 0.0, 0.125, 0.0 ], [ 0.0, -0.12499998509883881, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.11299999803304672, 0.16299998760223389, 0.1617421954870224 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.094999998807907104 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 10.0 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 390.0 +exhaust_open_deg = 130.0 +exhaust_profile = [ + [ 0.0, 0.0 ], + [ 130.0, 0.0 ], + [ 135.0, 0.00014399200154002756 ], + [ 140.0, 0.00054483400890603662 ], + [ 145.0, 0.0011558260302990675 ], + [ 150.0, 0.0019302689470350742 ], + [ 155.0, 0.0028214610647410154 ], + [ 160.0, 0.0037827040068805218 ], + [ 165.0, 0.0047672959044575691 ], + [ 170.0, 0.0057285390794277191 ], + [ 175.0, 0.0066197309643030167 ], + [ 180.0, 0.0073941741138696671 ], + [ 185.0, 0.0080051664263010025 ], + [ 190.0, 0.0084060076624155045 ], + [ 195.0, 0.0085500003769993782 ], + [ 325.0, 0.0085500003769993782 ], + [ 330.0, 0.0084060076624155045 ], + [ 335.0, 0.0080051664263010025 ], + [ 340.0, 0.0073941741138696671 ], + [ 345.0, 0.0066197309643030167 ], + [ 350.0, 0.0057285390794277191 ], + [ 355.0, 0.0047672959044575691 ], + [ 360.0, 0.0037827040068805218 ], + [ 365.0, 0.0028214610647410154 ], + [ 370.0, 0.0019302689470350742 ], + [ 375.0, 0.0011558260302990675 ], + [ 380.0, 0.00054483400890603662 ], + [ 385.0, 0.00014399200154002756 ], + [ 390.0, 0.0 ], + [ 720.0, 0.0 ] +] +exhaust_valve_count = 1 +exhaust_valve_m = 0.030400000512599945 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 15 +injection_enabled = true +intake_close_deg = 590.0 +intake_open_deg = 340.0 +intake_profile = [ + [ 0.0, 0.0 ], + [ 340.0, 0.0 ], + [ 345.0, 0.00017267199291381985 ], + [ 350.0, 0.0006517759757116437 ], + [ 355.0, 0.0013789440272375941 ], + [ 360.0, 0.0022958079352974892 ], + [ 365.0, 0.0033440000843256712 ], + [ 370.0, 0.0044651520438492298 ], + [ 375.0, 0.0056008961983025074 ], + [ 380.0, 0.0066928640007972717 ], + [ 385.0, 0.0076826880685985088 ], + [ 390.0, 0.0085119996219873428 ], + [ 395.0, 0.0091224322095513344 ], + [ 400.0, 0.0094556156545877457 ], + [ 402.5, 0.0094999996945261955 ], + [ 527.5, 0.0094999996945261955 ], + [ 532.5, 0.0093273278325796127 ], + [ 537.5, 0.0088482238352298737 ], + [ 542.5, 0.0081210555508732796 ], + [ 547.5, 0.0072041922248899937 ], + [ 552.5, 0.006155999843031168 ], + [ 557.5, 0.005034848116338253 ], + [ 562.5, 0.0038991039618849754 ], + [ 567.5, 0.0028071359265595675 ], + [ 572.5, 0.0018173119751736522 ], + [ 577.5, 0.00098799995612353086 ], + [ 582.5, 0.00037756800884380937 ], + [ 587.5, 4.4383999920682982e-05 ], + [ 590.0, 0.0 ], + [ 720.0, 0.0 ] +] +intake_valve_count = 1 +intake_valve_m = 0.036100000143051147 +junction_method = 0 +kind = 'cylinder' +maximum_lift_m = 0.0094999996945261955 +misfire_probability = 0.0 +name = 'Cylinder 1' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ 0.0, 0.39772698283195496, 0.40000006556510925 ], [ 0.0, 0.39772704243659973, -0.49999991059303284 ] ] +position_m = [ -0.11299999803304672, -5.7100670858289959e-09, -0.13063110411167145 ] +pressure_pa = 101325.0 +rod_m = 0.12671999633312225 +rotation_deg = [ -90.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.070399999618530273 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 75.0 +valve_spring_n_m = 38000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.094999998807907104 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 10.0 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 390.0 +exhaust_open_deg = 130.0 +exhaust_profile = [ + [ 0.0, 0.0 ], + [ 130.0, 0.0 ], + [ 135.0, 0.00014399200154002756 ], + [ 140.0, 0.00054483400890603662 ], + [ 145.0, 0.0011558260302990675 ], + [ 150.0, 0.0019302689470350742 ], + [ 155.0, 0.0028214610647410154 ], + [ 160.0, 0.0037827040068805218 ], + [ 165.0, 0.0047672959044575691 ], + [ 170.0, 0.0057285390794277191 ], + [ 175.0, 0.0066197309643030167 ], + [ 180.0, 0.0073941741138696671 ], + [ 185.0, 0.0080051664263010025 ], + [ 190.0, 0.0084060076624155045 ], + [ 195.0, 0.0085500003769993782 ], + [ 325.0, 0.0085500003769993782 ], + [ 330.0, 0.0084060076624155045 ], + [ 335.0, 0.0080051664263010025 ], + [ 340.0, 0.0073941741138696671 ], + [ 345.0, 0.0066197309643030167 ], + [ 350.0, 0.0057285390794277191 ], + [ 355.0, 0.0047672959044575691 ], + [ 360.0, 0.0037827040068805218 ], + [ 365.0, 0.0028214610647410154 ], + [ 370.0, 0.0019302689470350742 ], + [ 375.0, 0.0011558260302990675 ], + [ 380.0, 0.00054483400890603662 ], + [ 385.0, 0.00014399200154002756 ], + [ 390.0, 0.0 ], + [ 720.0, 0.0 ] +] +exhaust_valve_count = 1 +exhaust_valve_m = 0.030400000512599945 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 16 +injection_enabled = true +intake_close_deg = 590.0 +intake_open_deg = 340.0 +intake_profile = [ + [ 0.0, 0.0 ], + [ 340.0, 0.0 ], + [ 345.0, 0.00017267199291381985 ], + [ 350.0, 0.0006517759757116437 ], + [ 355.0, 0.0013789440272375941 ], + [ 360.0, 0.0022958079352974892 ], + [ 365.0, 0.0033440000843256712 ], + [ 370.0, 0.0044651520438492298 ], + [ 375.0, 0.0056008961983025074 ], + [ 380.0, 0.0066928640007972717 ], + [ 385.0, 0.0076826880685985088 ], + [ 390.0, 0.0085119996219873428 ], + [ 395.0, 0.0091224322095513344 ], + [ 400.0, 0.0094556156545877457 ], + [ 402.5, 0.0094999996945261955 ], + [ 527.5, 0.0094999996945261955 ], + [ 532.5, 0.0093273278325796127 ], + [ 537.5, 0.0088482238352298737 ], + [ 542.5, 0.0081210555508732796 ], + [ 547.5, 0.0072041922248899937 ], + [ 552.5, 0.006155999843031168 ], + [ 557.5, 0.005034848116338253 ], + [ 562.5, 0.0038991039618849754 ], + [ 567.5, 0.0028071359265595675 ], + [ 572.5, 0.0018173119751736522 ], + [ 577.5, 0.00098799995612353086 ], + [ 582.5, 0.00037756800884380937 ], + [ 587.5, 4.4383999920682982e-05 ], + [ 590.0, 0.0 ], + [ 720.0, 0.0 ] +] +intake_valve_count = 1 +intake_valve_m = 0.036100000143051147 +junction_method = 0 +kind = 'cylinder' +maximum_lift_m = 0.0094999996945261955 +misfire_probability = 0.0 +name = 'Cylinder 2' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 600.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ 0.0, 0.39772698283195496, 0.40000006556510925 ], [ 0.0, 0.39772704243659973, -0.49999991059303284 ] ] +position_m = [ 0.0, -5.7100670858289959e-09, -0.13063110411167145 ] +pressure_pa = 101325.0 +rod_m = 0.12671999633312225 +rotation_deg = [ -90.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.070399999618530273 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 75.0 +valve_spring_n_m = 38000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.094999998807907104 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 10.0 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 390.0 +exhaust_open_deg = 130.0 +exhaust_profile = [ + [ 0.0, 0.0 ], + [ 130.0, 0.0 ], + [ 135.0, 0.00014399200154002756 ], + [ 140.0, 0.00054483400890603662 ], + [ 145.0, 0.0011558260302990675 ], + [ 150.0, 0.0019302689470350742 ], + [ 155.0, 0.0028214610647410154 ], + [ 160.0, 0.0037827040068805218 ], + [ 165.0, 0.0047672959044575691 ], + [ 170.0, 0.0057285390794277191 ], + [ 175.0, 0.0066197309643030167 ], + [ 180.0, 0.0073941741138696671 ], + [ 185.0, 0.0080051664263010025 ], + [ 190.0, 0.0084060076624155045 ], + [ 195.0, 0.0085500003769993782 ], + [ 325.0, 0.0085500003769993782 ], + [ 330.0, 0.0084060076624155045 ], + [ 335.0, 0.0080051664263010025 ], + [ 340.0, 0.0073941741138696671 ], + [ 345.0, 0.0066197309643030167 ], + [ 350.0, 0.0057285390794277191 ], + [ 355.0, 0.0047672959044575691 ], + [ 360.0, 0.0037827040068805218 ], + [ 365.0, 0.0028214610647410154 ], + [ 370.0, 0.0019302689470350742 ], + [ 375.0, 0.0011558260302990675 ], + [ 380.0, 0.00054483400890603662 ], + [ 385.0, 0.00014399200154002756 ], + [ 390.0, 0.0 ], + [ 720.0, 0.0 ] +] +exhaust_valve_count = 1 +exhaust_valve_m = 0.030400000512599945 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 17 +injection_enabled = true +intake_close_deg = 590.0 +intake_open_deg = 340.0 +intake_profile = [ + [ 0.0, 0.0 ], + [ 340.0, 0.0 ], + [ 345.0, 0.00017267199291381985 ], + [ 350.0, 0.0006517759757116437 ], + [ 355.0, 0.0013789440272375941 ], + [ 360.0, 0.0022958079352974892 ], + [ 365.0, 0.0033440000843256712 ], + [ 370.0, 0.0044651520438492298 ], + [ 375.0, 0.0056008961983025074 ], + [ 380.0, 0.0066928640007972717 ], + [ 385.0, 0.0076826880685985088 ], + [ 390.0, 0.0085119996219873428 ], + [ 395.0, 0.0091224322095513344 ], + [ 400.0, 0.0094556156545877457 ], + [ 402.5, 0.0094999996945261955 ], + [ 527.5, 0.0094999996945261955 ], + [ 532.5, 0.0093273278325796127 ], + [ 537.5, 0.0088482238352298737 ], + [ 542.5, 0.0081210555508732796 ], + [ 547.5, 0.0072041922248899937 ], + [ 552.5, 0.006155999843031168 ], + [ 557.5, 0.005034848116338253 ], + [ 562.5, 0.0038991039618849754 ], + [ 567.5, 0.0028071359265595675 ], + [ 572.5, 0.0018173119751736522 ], + [ 577.5, 0.00098799995612353086 ], + [ 582.5, 0.00037756800884380937 ], + [ 587.5, 4.4383999920682982e-05 ], + [ 590.0, 0.0 ], + [ 720.0, 0.0 ] +] +intake_valve_count = 1 +intake_valve_m = 0.036100000143051147 +junction_method = 0 +kind = 'cylinder' +maximum_lift_m = 0.0094999996945261955 +misfire_probability = 0.0 +name = 'Cylinder 3' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 480.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ 0.0, 0.39772698283195496, 0.40000006556510925 ], [ 0.0, 0.39772704243659973, -0.49999991059303284 ] ] +position_m = [ 0.11299999803304672, -5.7100670858289959e-09, -0.13063110411167145 ] +pressure_pa = 101325.0 +rod_m = 0.12671999633312225 +rotation_deg = [ -90.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.070399999618530273 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 75.0 +valve_spring_n_m = 38000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.094999998807907104 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 10.0 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 390.0 +exhaust_open_deg = 130.0 +exhaust_profile = [ + [ 0.0, 0.0 ], + [ 130.0, 0.0 ], + [ 135.0, 0.00014399200154002756 ], + [ 140.0, 0.00054483400890603662 ], + [ 145.0, 0.0011558260302990675 ], + [ 150.0, 0.0019302689470350742 ], + [ 155.0, 0.0028214610647410154 ], + [ 160.0, 0.0037827040068805218 ], + [ 165.0, 0.0047672959044575691 ], + [ 170.0, 0.0057285390794277191 ], + [ 175.0, 0.0066197309643030167 ], + [ 180.0, 0.0073941741138696671 ], + [ 185.0, 0.0080051664263010025 ], + [ 190.0, 0.0084060076624155045 ], + [ 195.0, 0.0085500003769993782 ], + [ 325.0, 0.0085500003769993782 ], + [ 330.0, 0.0084060076624155045 ], + [ 335.0, 0.0080051664263010025 ], + [ 340.0, 0.0073941741138696671 ], + [ 345.0, 0.0066197309643030167 ], + [ 350.0, 0.0057285390794277191 ], + [ 355.0, 0.0047672959044575691 ], + [ 360.0, 0.0037827040068805218 ], + [ 365.0, 0.0028214610647410154 ], + [ 370.0, 0.0019302689470350742 ], + [ 375.0, 0.0011558260302990675 ], + [ 380.0, 0.00054483400890603662 ], + [ 385.0, 0.00014399200154002756 ], + [ 390.0, 0.0 ], + [ 720.0, 0.0 ] +] +exhaust_valve_count = 1 +exhaust_valve_m = 0.030400000512599945 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 18 +injection_enabled = true +intake_close_deg = 590.0 +intake_open_deg = 340.0 +intake_profile = [ + [ 0.0, 0.0 ], + [ 340.0, 0.0 ], + [ 345.0, 0.00017267199291381985 ], + [ 350.0, 0.0006517759757116437 ], + [ 355.0, 0.0013789440272375941 ], + [ 360.0, 0.0022958079352974892 ], + [ 365.0, 0.0033440000843256712 ], + [ 370.0, 0.0044651520438492298 ], + [ 375.0, 0.0056008961983025074 ], + [ 380.0, 0.0066928640007972717 ], + [ 385.0, 0.0076826880685985088 ], + [ 390.0, 0.0085119996219873428 ], + [ 395.0, 0.0091224322095513344 ], + [ 400.0, 0.0094556156545877457 ], + [ 402.5, 0.0094999996945261955 ], + [ 527.5, 0.0094999996945261955 ], + [ 532.5, 0.0093273278325796127 ], + [ 537.5, 0.0088482238352298737 ], + [ 542.5, 0.0081210555508732796 ], + [ 547.5, 0.0072041922248899937 ], + [ 552.5, 0.006155999843031168 ], + [ 557.5, 0.005034848116338253 ], + [ 562.5, 0.0038991039618849754 ], + [ 567.5, 0.0028071359265595675 ], + [ 572.5, 0.0018173119751736522 ], + [ 577.5, 0.00098799995612353086 ], + [ 582.5, 0.00037756800884380937 ], + [ 587.5, 4.4383999920682982e-05 ], + [ 590.0, 0.0 ], + [ 720.0, 0.0 ] +] +intake_valve_count = 1 +intake_valve_m = 0.036100000143051147 +junction_method = 0 +kind = 'cylinder' +maximum_lift_m = 0.0094999996945261955 +misfire_probability = 0.0 +name = 'Cylinder 4' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 360.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ 0.0, 0.39772698283195496, -0.40000006556510925 ], [ 0.0, 0.39772704243659973, 0.49999991059303284 ] ] +position_m = [ -0.11299999803304672, -5.7100670858289959e-09, 0.13063110411167145 ] +pressure_pa = 101325.0 +rod_m = 0.12671999633312225 +rotation_deg = [ 90.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.070399999618530273 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 75.0 +valve_spring_n_m = 38000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.094999998807907104 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 10.0 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 390.0 +exhaust_open_deg = 130.0 +exhaust_profile = [ + [ 0.0, 0.0 ], + [ 130.0, 0.0 ], + [ 135.0, 0.00014399200154002756 ], + [ 140.0, 0.00054483400890603662 ], + [ 145.0, 0.0011558260302990675 ], + [ 150.0, 0.0019302689470350742 ], + [ 155.0, 0.0028214610647410154 ], + [ 160.0, 0.0037827040068805218 ], + [ 165.0, 0.0047672959044575691 ], + [ 170.0, 0.0057285390794277191 ], + [ 175.0, 0.0066197309643030167 ], + [ 180.0, 0.0073941741138696671 ], + [ 185.0, 0.0080051664263010025 ], + [ 190.0, 0.0084060076624155045 ], + [ 195.0, 0.0085500003769993782 ], + [ 325.0, 0.0085500003769993782 ], + [ 330.0, 0.0084060076624155045 ], + [ 335.0, 0.0080051664263010025 ], + [ 340.0, 0.0073941741138696671 ], + [ 345.0, 0.0066197309643030167 ], + [ 350.0, 0.0057285390794277191 ], + [ 355.0, 0.0047672959044575691 ], + [ 360.0, 0.0037827040068805218 ], + [ 365.0, 0.0028214610647410154 ], + [ 370.0, 0.0019302689470350742 ], + [ 375.0, 0.0011558260302990675 ], + [ 380.0, 0.00054483400890603662 ], + [ 385.0, 0.00014399200154002756 ], + [ 390.0, 0.0 ], + [ 720.0, 0.0 ] +] +exhaust_valve_count = 1 +exhaust_valve_m = 0.030400000512599945 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 19 +injection_enabled = true +intake_close_deg = 590.0 +intake_open_deg = 340.0 +intake_profile = [ + [ 0.0, 0.0 ], + [ 340.0, 0.0 ], + [ 345.0, 0.00017267199291381985 ], + [ 350.0, 0.0006517759757116437 ], + [ 355.0, 0.0013789440272375941 ], + [ 360.0, 0.0022958079352974892 ], + [ 365.0, 0.0033440000843256712 ], + [ 370.0, 0.0044651520438492298 ], + [ 375.0, 0.0056008961983025074 ], + [ 380.0, 0.0066928640007972717 ], + [ 385.0, 0.0076826880685985088 ], + [ 390.0, 0.0085119996219873428 ], + [ 395.0, 0.0091224322095513344 ], + [ 400.0, 0.0094556156545877457 ], + [ 402.5, 0.0094999996945261955 ], + [ 527.5, 0.0094999996945261955 ], + [ 532.5, 0.0093273278325796127 ], + [ 537.5, 0.0088482238352298737 ], + [ 542.5, 0.0081210555508732796 ], + [ 547.5, 0.0072041922248899937 ], + [ 552.5, 0.006155999843031168 ], + [ 557.5, 0.005034848116338253 ], + [ 562.5, 0.0038991039618849754 ], + [ 567.5, 0.0028071359265595675 ], + [ 572.5, 0.0018173119751736522 ], + [ 577.5, 0.00098799995612353086 ], + [ 582.5, 0.00037756800884380937 ], + [ 587.5, 4.4383999920682982e-05 ], + [ 590.0, 0.0 ], + [ 720.0, 0.0 ] +] +intake_valve_count = 1 +intake_valve_m = 0.036100000143051147 +junction_method = 0 +kind = 'cylinder' +maximum_lift_m = 0.0094999996945261955 +misfire_probability = 0.0 +name = 'Cylinder 5' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 240.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ 0.0, 0.39772698283195496, -0.40000006556510925 ], [ 0.0, 0.39772704243659973, 0.49999991059303284 ] ] +position_m = [ 0.0, -5.7100670858289959e-09, 0.13063110411167145 ] +pressure_pa = 101325.0 +rod_m = 0.12671999633312225 +rotation_deg = [ 90.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.070399999618530273 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 75.0 +valve_spring_n_m = 38000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.094999998807907104 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 10.0 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 390.0 +exhaust_open_deg = 130.0 +exhaust_profile = [ + [ 0.0, 0.0 ], + [ 130.0, 0.0 ], + [ 135.0, 0.00014399200154002756 ], + [ 140.0, 0.00054483400890603662 ], + [ 145.0, 0.0011558260302990675 ], + [ 150.0, 0.0019302689470350742 ], + [ 155.0, 0.0028214610647410154 ], + [ 160.0, 0.0037827040068805218 ], + [ 165.0, 0.0047672959044575691 ], + [ 170.0, 0.0057285390794277191 ], + [ 175.0, 0.0066197309643030167 ], + [ 180.0, 0.0073941741138696671 ], + [ 185.0, 0.0080051664263010025 ], + [ 190.0, 0.0084060076624155045 ], + [ 195.0, 0.0085500003769993782 ], + [ 325.0, 0.0085500003769993782 ], + [ 330.0, 0.0084060076624155045 ], + [ 335.0, 0.0080051664263010025 ], + [ 340.0, 0.0073941741138696671 ], + [ 345.0, 0.0066197309643030167 ], + [ 350.0, 0.0057285390794277191 ], + [ 355.0, 0.0047672959044575691 ], + [ 360.0, 0.0037827040068805218 ], + [ 365.0, 0.0028214610647410154 ], + [ 370.0, 0.0019302689470350742 ], + [ 375.0, 0.0011558260302990675 ], + [ 380.0, 0.00054483400890603662 ], + [ 385.0, 0.00014399200154002756 ], + [ 390.0, 0.0 ], + [ 720.0, 0.0 ] +] +exhaust_valve_count = 1 +exhaust_valve_m = 0.030400000512599945 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 20 +injection_enabled = true +intake_close_deg = 590.0 +intake_open_deg = 340.0 +intake_profile = [ + [ 0.0, 0.0 ], + [ 340.0, 0.0 ], + [ 345.0, 0.00017267199291381985 ], + [ 350.0, 0.0006517759757116437 ], + [ 355.0, 0.0013789440272375941 ], + [ 360.0, 0.0022958079352974892 ], + [ 365.0, 0.0033440000843256712 ], + [ 370.0, 0.0044651520438492298 ], + [ 375.0, 0.0056008961983025074 ], + [ 380.0, 0.0066928640007972717 ], + [ 385.0, 0.0076826880685985088 ], + [ 390.0, 0.0085119996219873428 ], + [ 395.0, 0.0091224322095513344 ], + [ 400.0, 0.0094556156545877457 ], + [ 402.5, 0.0094999996945261955 ], + [ 527.5, 0.0094999996945261955 ], + [ 532.5, 0.0093273278325796127 ], + [ 537.5, 0.0088482238352298737 ], + [ 542.5, 0.0081210555508732796 ], + [ 547.5, 0.0072041922248899937 ], + [ 552.5, 0.006155999843031168 ], + [ 557.5, 0.005034848116338253 ], + [ 562.5, 0.0038991039618849754 ], + [ 567.5, 0.0028071359265595675 ], + [ 572.5, 0.0018173119751736522 ], + [ 577.5, 0.00098799995612353086 ], + [ 582.5, 0.00037756800884380937 ], + [ 587.5, 4.4383999920682982e-05 ], + [ 590.0, 0.0 ], + [ 720.0, 0.0 ] +] +intake_valve_count = 1 +intake_valve_m = 0.036100000143051147 +junction_method = 0 +kind = 'cylinder' +maximum_lift_m = 0.0094999996945261955 +misfire_probability = 0.0 +name = 'Cylinder 6' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 120.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ 0.0, 0.39772698283195496, -0.40000006556510925 ], [ 0.0, 0.39772704243659973, 0.49999991059303284 ] ] +position_m = [ 0.11299999803304672, -5.7100670858289959e-09, 0.13063110411167145 ] +pressure_pa = 101325.0 +rod_m = 0.12671999633312225 +rotation_deg = [ 90.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.070399999618530273 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 75.0 +valve_spring_n_m = 38000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 21 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'intake_plenum' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ + [ -0.3333333432674408, -0.50000035762786865, -0.38649517297744751 ], + [ 0.0, -0.50000035762786865, -0.38649517297744751 ], + [ 0.3333333432674408, -0.50000035762786865, -0.38649517297744751 ], + [ -0.3333333432674408, -0.50000035762786865, 0.38649517297744751 ], + [ 0.0, -0.50000035762786865, 0.38649517297744751 ], + [ 0.3333333432674408, -0.50000035762786865, 0.38649517297744751 ], + [ -0.5, 0.0, 0.0 ] +] +position_m = [ 0.0, 0.3091467022895813, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.33899998664855957, 0.042293399572372437, 0.4184843897819519 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 23 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Inlet metering stub' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.039999999105930328, 0.039999999105930328 ] +points_m = [ [ -0.23000000417232513, 0.31000000238418579, -1.862645149230957e-09 ], [ -0.17949999868869781, 0.3091467022895813, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.0099999997764825821, 0.0, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 24 +injection_enabled = false +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'ecu' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Injection / ignition ECU' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.0, 0.5591467022895813, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.10000000149011612, 0.02500000037252903, 0.075000002980232239 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.ecu] + fixed_idle = true + idle_base_opening = 0.060000002384185791 + idle_ki = 3.9999998989515007e-05 + idle_kp = 9.9999997473787516e-05 + idle_max_opening = 0.25 + idle_minimum_advance = -5.0 + idle_rpm = 800.0 + idle_spark_gain = 0.019999999552965164 + ignition_map = [ [ 0.0, 10.0 ], [ 15000.0, 40.0 ] ] + overrun_cut_rpm = 3500.0 + overrun_fuel_cut_enabled = true + overrun_ignition_cut_enabled = false + overrun_resume_rpm = 2400.0 + power_afr = 12.800000190734863 + rev_limit_cut_time = 0.0 + rev_limit_enabled = false + rev_limit_fuel_cut_enabled = true + rev_limit_ignition_cut_enabled = true + rev_limit_rpm = 8500.0 + rev_resume_rpm = 8300.0 + stoichiometric_afr = 14.699999809265137 + throttle_closing_rate = 0.0 + throttle_closing_time_constant = 0.0 + throttle_rate = 1000.0 + throttle_time_constant = 0.0010000000474974513 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 25 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Intake atmosphere' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.36950001120567322, 0.3091467022895813, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 27 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Exhaust atmosphere' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 1.6572357416152954, -0.054999999701976776, 0.25849220156669617 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 28 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'carburetor' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Carburetor 28' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.23999999463558197, 0.31000000238418579, -1.862645149230957e-09 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.05000000074505806, 0.05000000074505806 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.carburetor] + bowl_capacity = 0.020000001415610313 + bowl_head_pressure = 0.0 + bowl_refill_rate = 0.014999999664723873 + film_fraction = 0.2800000011920929 + film_time_constant = 0.10999999940395355 + fuel_density = 740.0 + main_discharge_coefficient = 0.72000002861022949 + main_jet_diameter = 0.0040000001899898052 + needle_jet_diameter = 0.0018500001169741154 + needle_low_scale = 1.0 + needle_mid_scale = 1.0 + needle_off_idle_scale = 1.0 + needle_quarter_scale = 1.0 + needle_tip_scale = 1.0 + needle_transition_scale = 1.0 + needle_upper_scale = 1.0 + overrun_exit_rpm = 3300.0 + overrun_rpm = 4200.0 + pilot_discharge_coefficient = 0.62000000476837158 + pilot_jet_diameter = 0.00024000003759283572 + pilot_tip_scale = 1.0 + pilot_transition_scale = 1.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 29 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 1 +kind = 'junction' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Junction 29' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.2199999988079071, 0.25, -0.08999999612569809 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.059999998658895493, 0.059999998658895493, 0.059999998658895493 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 30 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 1 +kind = 'junction' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Junction 30' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.17000000178813934, -0.11999999731779099, 0.15999998152256012 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.059999998658895493, 0.059999998658895493, 0.059999998658895493 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 31 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 31' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.02500000037252903, 0.02500000037252903, 0.02500000037252903 ] +points_m = [ [ 0.13300000131130219, -0.29750001430511475, -0.59825778007507324 ], [ 0.15052753686904907, -0.33734250068664551, -0.59909915924072266 ], [ 0.18999999761581421, -0.37000000476837158, -0.60000002384185791 ] ] +port_coordinates = [] +position_m = [ -0.019999999552965164, 0.25, 0.75999999046325684 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 32 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 32' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.02500000037252903, 0.02500000037252903, 0.02500000037252903, 0.02500000037252903 ] +points_m = [ [ -0.36999994516372681, -0.37000000476837158, -0.52000004053115845 ], [ -0.4761640727519989, -0.34520614147186279, -0.51906377077102661 ], [ -0.52133440971374512, -0.32712483406066895, -0.5185772180557251 ], [ -0.53999996185302734, -0.29750001430511475, -0.51825779676437378 ] ] +port_coordinates = [] +position_m = [ 0.53999996185302734, 0.25, 0.68000000715255737 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 33 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 33' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.02500000037252903, 0.02500000037252903, 0.02500000037252903, 0.02500000037252903 ] +points_m = [ [ 0.16699999570846558, -0.29750001430511475, 0.14174219965934753 ], [ 0.21503587067127228, -0.35698139667510986, 0.14158914983272552 ], [ 0.25817635655403137, -0.36491745710372925, 0.14149604737758636 ], [ 0.44999998807907104, -0.37000000476837158, 0.13999998569488525 ] ] +port_coordinates = [] +position_m = [ -0.2800000011920929, 0.25, 0.019999999552965164 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 50 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 50' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.029999999329447746, 0.029999999329447746 ] +points_m = [ [ -0.46999996900558472, -0.37000000476837158, -0.2200000137090683 ], [ -0.15000000596046448, -0.29999998211860657, -0.31000000238418579 ] ] +port_coordinates = [] +position_m = [ 0.63999998569488525, 0.25, 0.37999999523162842 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 51 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 1 +kind = 'junction' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Junction 30' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.17000000178813934, -0.11999999731779099, -0.16000001132488251 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.059999998658895493, 0.059999998658895493, 0.059999998658895493 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 52 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 31' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.02500000037252903, 0.02500000037252903, 0.02500000037252903 ] +points_m = [ [ 0.13300000131130219, -0.29750001430511475, -0.6017422080039978 ], [ 0.15052753686904907, -0.33734250068664551, -0.59909915924072266 ], [ 0.1900000125169754, -0.37000000476837158, -0.60000002384185791 ] ] +port_coordinates = [] +position_m = [ -0.020000005140900612, 0.25, 0.43999999761581421 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 53 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 32' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.02500000037252903, 0.02500000037252903, 0.02500000037252903, 0.02500000037252903 ] +points_m = [ [ -0.36999994516372681, -0.37000000476837158, -0.51999998092651367 ], [ -0.4761640727519989, -0.34520614147186279, -0.51906377077102661 ], [ -0.52133440971374512, -0.32712483406066895, -0.5185772180557251 ], [ -0.53999996185302734, -0.29750001430511475, -0.52174216508865356 ] ] +port_coordinates = [] +position_m = [ 0.53999996185302734, 0.25, 0.35999995470046997 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 54 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 33' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.02500000037252903, 0.02500000037252903, 0.02500000037252903, 0.02500000037252903 ] +points_m = [ [ 0.16699996590614319, -0.29750001430511475, 0.13825778663158417 ], [ 0.21503587067127228, -0.35698139667510986, 0.14158914983272552 ], [ 0.25817635655403137, -0.36491745710372925, 0.14149604737758636 ], [ 0.44999998807907104, -0.37000000476837158, 0.13999997079372406 ] ] +port_coordinates = [] +position_m = [ -0.27999997138977051, 0.25, -0.29999998211860657 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 56 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 50' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.029999999329447746, 0.029999999329447746 ] +points_m = [ [ -0.47999995946884155, -0.37000000476837158, -0.14000000059604645 ], [ -0.15999999642372131, -0.29999998211860657, 0.090000003576278687 ] ] +port_coordinates = [] +position_m = [ 0.64999997615814209, 0.25, -0.02000001072883606 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 57 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 1 +kind = 'junction' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Junction 57' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.48999997973442078, -0.049999982118606567, 0.069999992847442627 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.059999998658895493, 0.059999998658895493, 0.059999998658895493 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 58 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pressure / atmosphere 58' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 1.2399998903274536, -0.040000006556510925, 0.099999994039535522 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 59 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 59' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.039999999105930328, 0.039999999105930328, 0.059999998658895493, 0.059999998658895493, 0.039999999105930328, 0.039999999105930328 ] +points_m = [ + [ -0.25000002980232239, -0.29999998211860657, -0.48000001907348633 ], + [ 0.090410172939300537, -0.29491925239562988, -0.4647577702999115 ], + [ 0.11037562042474747, -0.29462125897407532, -0.4638637900352478 ], + [ 0.31505998969078064, -0.29156625270843506, -0.45469877123832703 ], + [ 0.33398273587226868, -0.29128384590148926, -0.45385152101516724 ], + [ 0.49999988079071045, -0.29000002145767212, -0.45000001788139343 ] +] +port_coordinates = [] +position_m = [ 0.74000000953674316, 0.25, 0.55000001192092896 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[probes]] +id = 1 +name = 'Fresh-charge AFR / Cylinder 4' +part = 18 +position = 0.5 +quantity = 5 +reference_afr = 14.699999809265137 + +[[probes]] +id = 3 +name = 'Bulk AFR / Inlet metering stub' +part = 23 +position = 0.5 +quantity = 3 +reference_afr = 14.699999809265137 + +[[probes]] +id = 4 +name = 'Bulk AFR / intake_plenum' +part = 21 +position = 0.5 +quantity = 3 +reference_afr = 14.699999809265137 + +[[probes]] +id = 5 +name = 'Lambda / Pipe / reducer 50' +part = 50 +position = 0.5 +quantity = 4 +reference_afr = 14.699999809265137 + +[[probes]] +id = 6 +name = 'Lambda / Pipe / reducer 50' +part = 56 +position = 0.5 +quantity = 4 +reference_afr = 14.699999809265137 diff --git a/machines/logic-compressor-bench.machine.toml b/machines/logic-compressor-bench.machine.toml new file mode 100644 index 0000000..98ae723 --- /dev/null +++ b/machines/logic-compressor-bench.machine.toml @@ -0,0 +1,1076 @@ +cells_per_meter = 40.0 +format = 8 +name = 'Automatic air supply / logic bench' +next_id = 8 +next_sensor_id = 3 +primary_shaft = 6 +scene = 2 +solver = 'rusanov' + +[logic] +next_id = 12 + + [[logic.nodes]] + below = false + binding_label = '' + boolean = false + clamp = true + command = 'motor_enable' + false_value = 0.0 + gain = 1.0 + high = 0.75 + id = 1 + initial = false + input_count = 2 + input_high = 1.0 + input_low = 0.0 + kind = 'sensor' + low = 0.25 + maximum = 1.0 + minimum = 0.0 + name = 'Tank pressure' + offset = 0.0 + output_high = 1.0 + output_low = 0.0 + output_pressure_reference = 'none' + output_quantity = 'scalar' + part = 0 + position = [ 0.0, 0.0 ] + pressure_reference = 'none' + quantity = 'scalar' + sensor = 1 + sensor_kind = 'pressure' + tolerance = 9.9999999999999995e-07 + true_value = 1.0 + value = 0.0 + + [[logic.nodes]] + below = true + binding_label = '' + boolean = false + clamp = true + command = 'motor_enable' + false_value = 0.0 + gain = 1.0 + high = 150000.0 + id = 2 + initial = true + input_count = 2 + input_high = 1.0 + input_low = 0.0 + kind = 'hysteresis' + low = 120000.0 + maximum = 1.0 + minimum = 0.0 + name = 'Compressor demand' + offset = 0.0 + output_high = 1.0 + output_low = 0.0 + output_pressure_reference = 'none' + output_quantity = 'scalar' + part = 0 + position = [ 300.0, 0.0 ] + pressure_reference = 'absolute' + quantity = 'pressure' + sensor = 0 + sensor_kind = 'pressure' + tolerance = 9.9999999999999995e-07 + true_value = 1.0 + value = 0.0 + + [[logic.nodes]] + below = false + binding_label = '' + boolean = true + clamp = true + command = 'motor_enable' + false_value = 0.0 + gain = 1.0 + high = 0.75 + id = 3 + initial = false + input_count = 2 + input_high = 1.0 + input_low = 0.0 + kind = 'switch' + low = 0.25 + maximum = 1.0 + minimum = 0.0 + name = 'Master enable' + offset = 0.0 + output_high = 1.0 + output_low = 0.0 + output_pressure_reference = 'none' + output_quantity = 'scalar' + part = 0 + position = [ 300.0, 240.0 ] + pressure_reference = 'none' + quantity = 'scalar' + sensor = 0 + sensor_kind = 'pressure' + tolerance = 9.9999999999999995e-07 + true_value = 1.0 + value = 0.0 + + [[logic.nodes]] + below = false + binding_label = '' + boolean = false + clamp = true + command = 'motor_enable' + false_value = 0.0 + gain = 1.0 + high = 0.75 + id = 4 + initial = false + input_count = 2 + input_high = 1.0 + input_low = 0.0 + kind = 'and' + low = 0.25 + maximum = 1.0 + minimum = 0.0 + name = 'Enabled demand' + offset = 0.0 + output_high = 1.0 + output_low = 0.0 + output_pressure_reference = 'none' + output_quantity = 'scalar' + part = 0 + position = [ 600.0, 0.0 ] + pressure_reference = 'none' + quantity = 'scalar' + sensor = 0 + sensor_kind = 'pressure' + tolerance = 9.9999999999999995e-07 + true_value = 1.0 + value = 0.0 + + [[logic.nodes]] + below = false + binding_label = 'Bench speed drive' + boolean = false + clamp = true + command = 'motor_enable' + false_value = 0.0 + gain = 1.0 + high = 0.75 + id = 5 + initial = false + input_count = 2 + input_high = 1.0 + input_low = 0.0 + kind = 'command' + low = 0.25 + maximum = 1.0 + minimum = 0.0 + name = 'Compressor motor enable' + offset = 0.0 + output_high = 1.0 + output_low = 0.0 + output_pressure_reference = 'none' + output_quantity = 'scalar' + part = 7 + position = [ 900.0, 0.0 ] + pressure_reference = 'none' + quantity = 'scalar' + sensor = 0 + sensor_kind = 'pressure' + tolerance = 9.9999999999999995e-07 + true_value = 1.0 + value = 0.0 + + [[logic.nodes]] + below = false + binding_label = '' + boolean = false + clamp = true + command = 'motor_enable' + false_value = 0.0 + gain = 1.0 + high = 0.75 + id = 6 + initial = false + input_count = 2 + input_high = 1.0 + input_low = 0.0 + kind = 'slider' + low = 0.25 + maximum = 300.0 + minimum = 0.0 + name = 'Compressor speed (rad/s)' + offset = 0.0 + output_high = 1.0 + output_low = 0.0 + output_pressure_reference = 'none' + output_quantity = 'scalar' + part = 0 + position = [ 600.0, 260.0 ] + pressure_reference = 'none' + quantity = 'angular_speed' + sensor = 0 + sensor_kind = 'pressure' + tolerance = 9.9999999999999995e-07 + true_value = 1.0 + value = 94.247782230377197 + + [[logic.nodes]] + below = false + binding_label = '' + boolean = false + clamp = true + command = 'motor_speed' + false_value = 0.0 + gain = 1.0 + high = 0.75 + id = 7 + initial = false + input_count = 2 + input_high = 1.0 + input_low = 0.0 + kind = 'command' + low = 0.25 + maximum = 1.0 + minimum = 0.0 + name = 'Motor speed target' + offset = 0.0 + output_high = 1.0 + output_low = 0.0 + output_pressure_reference = 'none' + output_quantity = 'scalar' + part = 7 + position = [ 900.0, 260.0 ] + pressure_reference = 'none' + quantity = 'scalar' + sensor = 0 + sensor_kind = 'pressure' + tolerance = 9.9999999999999995e-07 + true_value = 1.0 + value = 0.0 + + [[logic.nodes]] + below = false + binding_label = '' + boolean = false + clamp = true + command = 'motor_enable' + false_value = 0.0 + gain = 1.0 + high = 0.75 + id = 8 + initial = false + input_count = 2 + input_high = 1.0 + input_low = 0.0 + kind = 'readout' + low = 0.25 + maximum = 1.0 + minimum = 0.0 + name = 'Tank pressure readout' + offset = 0.0 + output_high = 1.0 + output_low = 0.0 + output_pressure_reference = 'none' + output_quantity = 'scalar' + part = 0 + position = [ 300.0, -180.0 ] + pressure_reference = 'absolute' + quantity = 'pressure' + sensor = 0 + sensor_kind = 'pressure' + tolerance = 9.9999999999999995e-07 + true_value = 1.0 + value = 0.0 + + [[logic.nodes]] + below = false + binding_label = '' + boolean = false + clamp = true + command = 'motor_enable' + false_value = 0.0 + gain = 1.0 + high = 0.75 + id = 9 + initial = false + input_count = 2 + input_high = 1.0 + input_low = 0.0 + kind = 'lamp' + low = 0.25 + maximum = 1.0 + minimum = 0.0 + name = 'Compressor running demand' + offset = 0.0 + output_high = 1.0 + output_low = 0.0 + output_pressure_reference = 'none' + output_quantity = 'scalar' + part = 0 + position = [ 900.0, -180.0 ] + pressure_reference = 'none' + quantity = 'scalar' + sensor = 0 + sensor_kind = 'pressure' + tolerance = 9.9999999999999995e-07 + true_value = 1.0 + value = 0.0 + + [[logic.nodes]] + below = false + binding_label = '' + boolean = false + clamp = true + command = 'motor_enable' + false_value = 0.0 + gain = 1.0 + high = 0.75 + id = 10 + initial = false + input_count = 2 + input_high = 1.0 + input_low = 0.0 + kind = 'sensor' + low = 0.25 + maximum = 1.0 + minimum = 0.0 + name = 'Compressor crank pickup' + offset = 0.0 + output_high = 1.0 + output_low = 0.0 + output_pressure_reference = 'none' + output_quantity = 'scalar' + part = 0 + position = [ 0.0, 380.0 ] + pressure_reference = 'none' + quantity = 'scalar' + sensor = 2 + sensor_kind = 'crank' + tolerance = 9.9999999999999995e-07 + true_value = 1.0 + value = 0.0 + + [[logic.nodes]] + below = false + binding_label = '' + boolean = false + clamp = true + command = 'motor_enable' + false_value = 0.0 + gain = 1.0 + high = 0.75 + id = 11 + initial = false + input_count = 2 + input_high = 1.0 + input_low = 0.0 + kind = 'readout' + low = 0.25 + maximum = 1.0 + minimum = 0.0 + name = 'Measured angular speed' + offset = 0.0 + output_high = 1.0 + output_low = 0.0 + output_pressure_reference = 'none' + output_quantity = 'scalar' + part = 0 + position = [ 300.0, 420.0 ] + pressure_reference = 'none' + quantity = 'angular_speed' + sensor = 0 + sensor_kind = 'pressure' + tolerance = 9.9999999999999995e-07 + true_value = 1.0 + value = 0.0 + + [[logic.wires]] + from_node = 1 + from_port = 'value' + to_node = 2 + to_port = 'in' + + [[logic.wires]] + from_node = 2 + from_port = 'out' + to_node = 4 + to_port = 'in1' + + [[logic.wires]] + from_node = 3 + from_port = 'out' + to_node = 4 + to_port = 'in2' + + [[logic.wires]] + from_node = 4 + from_port = 'out' + to_node = 5 + to_port = 'in' + + [[logic.wires]] + from_node = 6 + from_port = 'out' + to_node = 7 + to_port = 'in' + + [[logic.wires]] + from_node = 1 + from_port = 'value' + to_node = 8 + to_port = 'in' + + [[logic.wires]] + from_node = 4 + from_port = 'out' + to_node = 9 + to_port = 'in' + + [[logic.wires]] + from_node = 10 + from_port = 'speed' + to_node = 11 + to_port = 'in' + +[[connections]] +a_part = 4 +a_port = 'start' +b_part = 3 +b_port = 'gas' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 4 +a_port = 'end' +b_part = 2 +b_port = 'inlet' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 5 +a_port = 'start' +b_part = 2 +b_port = 'outlet' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 5 +a_port = 'end' +b_part = 1 +b_port = 'face_x_negative' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 6 +a_port = 'shaft' +b_part = 2 +b_port = 'shaft' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'mechanical' + +[[connections]] +a_part = 6 +a_port = 'shaft' +b_part = 7 +b_port = 'shaft' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'mechanical' + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 1 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Compressed air tank' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.40000000596046448, 0.2800000011920929, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 2 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'piston_pump' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Piston pump 2' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.0, 0.20000000298023224, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 3 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Intake atmosphere' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.40000000596046448, 0.31999999284744263, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 4 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 4' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.032000001519918442, 0.032000001519918442 ] +points_m = [ [ -0.24137499928474426, 0.061380922794342041, 0.0 ], [ 0.14137500524520874, -0.021380960941314697, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.15862500667572021, 0.25861907005310059, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 5 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 5' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.032000001519918442, 0.032000001519918442 ] +points_m = [ [ -0.1913750022649765, -0.041380941867828369, 0.0 ], [ 0.13137499988079071, 0.0013809502124786377, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.20862500369548798, 0.27861905097961426, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 6 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'shaft' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Main crankshaft' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.0, 0.049999997019767761, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.25, 0.035000000149011612, 0.035000000149011612 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.shaft] + friction_nm = 0.15000000596046448 + inertia_kg_m2 = 0.0099999997764825821 + initial_angle_rad = 0.0 + initial_speed_rad_s = 0.0 + quadratic_nm_s2 = 0.0 + starter_nm = 0.0 + viscous_nm_s = 0.0020000000949949026 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 7 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'drive_motor' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Bench speed drive' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.2800000011920929, 0.049999997019767761, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.090000003576278687, 0.090000003576278687 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.drive] + enabled = true + speed_control = true + target_speed_rad_s = 94.247779846191406 + torque_nm = 1.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[probes]] +generated_output = false +id = 1 +name = 'Tank absolute pressure' +part = 1 +position = 0.5 +quantity = 0 +reference_afr = 14.699999809265137 + +[[probes]] +generated_output = false +id = 2 +name = 'Compressor shaft RPM' +part = 6 +position = 0.5 +quantity = 6 +reference_afr = 14.699999809265137 + +[[probes]] +generated_output = false +id = 3 +name = 'Drive power' +part = 7 +position = 0.5 +quantity = 8 +reference_afr = 14.699999809265137 + +[[sensors]] +angle_offset_rad = 0.0 +id = 1 +kind = 'pressure' +name = 'Tank absolute pressure' + + [sensors.reference] + label = '' + part = 0 + position = 0.5 + + [sensors.target] + label = 'Compressed air tank' + part = 1 + position = 0.5 + +[[sensors]] +angle_offset_rad = 0.0 +id = 2 +kind = 'crank' +name = 'Crank speed / position / Main crankshaft' + + [sensors.reference] + label = '' + part = 0 + position = 0.5 + + [sensors.target] + label = 'Main crankshaft' + part = 6 + position = 0.5 diff --git a/machines/paralell-twin.machine.toml b/machines/paralell-twin.machine.toml new file mode 100644 index 0000000..d8426b3 --- /dev/null +++ b/machines/paralell-twin.machine.toml @@ -0,0 +1,1465 @@ +cells_per_meter = 30.0 +drive_rpm = 2000.0 +driven = false +format = 2 +friction_constant = 5.0 +friction_quadratic = 0.0 +friction_viscous = 0.0030000000260770321 +load_torque = 0.0 +name = '600 cc parallel twin / editable' +scene = 2 +shaft_inertia = 0.039999999105930328 +solver = 'muscl' +starter_torque = 18.0 + +[[connections]] +a_part = 2 +a_port = 0 +b_part = 9 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 2 +a_port = 1 +b_part = 7 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 4 +a_port = 0 +b_part = 7 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 3 +a_port = 0 +b_part = 9 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 3 +a_port = 1 +b_part = 8 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 5 +a_port = 0 +b_part = 8 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 1 +a_port = 1 +b_part = 10 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 11 +a_port = 0 +b_part = 10 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 11 +a_port = 1 +b_part = 9 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 12 +a_port = 0 +b_part = 7 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 12 +a_port = 0 +b_part = 8 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 12 +a_port = 0 +b_part = 10 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 1 +a_port = 0 +b_part = 13 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 4 +a_port = 1 +b_part = 14 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 5 +a_port = 1 +b_part = 14 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 6 +a_port = 0 +b_part = 14 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 6 +a_port = 1 +b_part = 15 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.027499999850988388 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 1 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 0 / twin' +opening = 1.0 +outlet_diameter_m = 0.027499999850988388 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.030000001192092896, 0.0, 0.0 ], [ -0.004999995231628418, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.12299999594688416, 0.14447620511054993, -0.2175000011920929 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.027499999850988388 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 2 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 1 / twin' +opening = 1.0 +outlet_diameter_m = 0.027499999850988388 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ 0.0, 0.0, -0.060000002384185791 ], [ 0.0, 0.0, 0.060000002384185791 ] ] +port_coordinates = [] +position_m = [ -0.046500001102685928, 0.14447620511054993, -0.090000003576278687 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.027499999850988388 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 3 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 2 / twin' +opening = 1.0 +outlet_diameter_m = 0.027499999850988388 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ 0.0, 0.0, -0.060000002384185791 ], [ 0.0, 0.0, 0.060000002384185791 ] ] +port_coordinates = [] +position_m = [ 0.046500001102685928, 0.14447620511054993, -0.090000003576278687 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.028000000864267349 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 4 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 3 / twin' +opening = 1.0 +outlet_diameter_m = 0.028000000864267349 +pedal_control = true +phase_deg = 0.0 +points_m = [ + [ -0.068752430379390717, 0.066082403063774109, -0.06390891969203949 ], + [ -0.06732632964849472, 0.06305280327796936, -0.049996320158243179 ], + [ -0.063295826315879822, 0.058525398373603821, -0.038123220205307007 ], + [ -0.057033229619264603, 0.052598997950553894, -0.028132118284702301 ], + [ -0.048910431563854218, 0.04537229984998703, -0.019865423440933228 ], + [ -0.039299428462982178, 0.036943994462490082, -0.01316562294960022 ], + [ -0.028572328388690948, 0.02741289883852005, -0.0078751221299171448 ], + [ -0.01710112951695919, 0.016877695918083191, -0.0038362219929695129 ], + [ -0.0052579287439584732, 0.0054371953010559082, -0.00089152157306671143 ], + [ 0.0065852701663970947, -0.0068098008632659912, 0.0011165812611579895 ], + [ 0.018056470900774002, -0.019764602184295654, 0.0023457780480384827 ], + [ 0.028783570975065231, -0.033328503370285034, 0.0029534772038459778 ], + [ 0.038394469767808914, -0.047402702271938324, 0.0030973777174949646 ], + [ 0.046517368406057358, -0.061888404190540314, 0.0029350817203521729 ], + [ 0.052779968827962875, -0.076686903834342957, 0.0026239752769470215 ], + [ 0.056810367852449417, -0.091699503362178802, 0.002321779727935791 ], + [ 0.05823657289147377, -0.10682730376720428, 0.0021859779953956604 ] +] +port_coordinates = [] +position_m = [ 0.022252429276704788, 0.078393802046775818, 0.10140892118215561 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.028000000864267349 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 5 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 4 / twin' +opening = 1.0 +outlet_diameter_m = 0.028000000864267349 +pedal_control = true +phase_deg = 0.0 +points_m = [ + [ -0.025599349290132523, 0.071668267250061035, -0.059992104768753052 ], + [ -0.025078549981117249, 0.068801760673522949, -0.048053707927465439 ], + [ -0.023606650531291962, 0.06413036584854126, -0.037646006792783737 ], + [ -0.021319650113582611, 0.057814061641693115, -0.028664007782936096 ], + [ -0.018353249877691269, 0.050012663006782532, -0.021002806723117828 ], + [ -0.014843448996543884, 0.040886163711547852, -0.014557406306266785 ], + [ -0.010926049202680588, 0.03059425950050354, -0.0092230066657066345 ], + [ -0.00673694908618927, 0.019296959042549133, -0.0048946067690849304 ], + [ -0.0024119541049003601, 0.0071541592478752136, -0.0014673098921775818 ], + [ 0.0019130483269691467, -0.0056743398308753967, 0.0011637955904006958 ], + [ 0.0061021521687507629, -0.019028536975383759, 0.003103695809841156 ], + [ 0.010019548237323761, -0.032748639583587646, 0.0044572949409484863 ], + [ 0.013529449701309204, -0.046674840152263641, 0.0053294971585273743 ], + [ 0.016495749354362488, -0.060647036880254745, 0.0058251917362213135 ], + [ 0.018782749772071838, -0.074505440890789032, 0.0060493946075439453 ], + [ 0.020254649221897125, -0.088090240955352783, 0.0061069950461387634 ], + [ 0.020775452256202698, -0.10124143958091736, 0.0061027929186820984 ] +] +port_coordinates = [] +position_m = [ 0.072099350392818451, 0.072807937860488892, 0.097492106258869171 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.028000000864267349 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 6 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 5 / twin' +opening = 1.0 +outlet_diameter_m = 0.028000000864267349 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.19999989867210388, 0.0, 0.0 ], [ 0.19999992847442627, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.32656639814376831, -0.054999999701976776, 0.11375000327825546 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 390.0 +exhaust_open_deg = 130.0 +exhaust_profile = [ + [ 0.0, 0.0 ], + [ 130.0, 0.0 ], + [ 135.0, 0.0001263089943677187 ], + [ 140.0, 0.00047792401164770126 ], + [ 145.0, 0.0010138830402866006 ], + [ 150.0, 0.0016932180151343346 ], + [ 155.0, 0.0024749659933149815 ], + [ 160.0, 0.0033181610051542521 ], + [ 165.0, 0.0041818390600383282 ], + [ 170.0, 0.0050250338390469551 ], + [ 175.0, 0.005806781817227602 ], + [ 180.0, 0.0064861169084906578 ], + [ 185.0, 0.0070220758207142353 ], + [ 190.0, 0.0073736910708248615 ], + [ 195.0, 0.0074999998323619366 ], + [ 325.0, 0.0074999998323619366 ], + [ 330.0, 0.0073736910708248615 ], + [ 335.0, 0.0070220758207142353 ], + [ 340.0, 0.0064861169084906578 ], + [ 345.0, 0.005806781817227602 ], + [ 350.0, 0.0050250338390469551 ], + [ 355.0, 0.0041818390600383282 ], + [ 360.0, 0.0033181610051542521 ], + [ 365.0, 0.0024749659933149815 ], + [ 370.0, 0.0016932180151343346 ], + [ 375.0, 0.0010138830402866006 ], + [ 380.0, 0.00047792401164770126 ], + [ 385.0, 0.0001263089943677187 ], + [ 390.0, 0.0 ], + [ 720.0, 0.0 ] +] +exhaust_valve_count = 1 +exhaust_valve_m = 0.026000000536441803 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 7 +injection_enabled = true +intake_close_deg = 590.0 +intake_open_deg = 340.0 +intake_profile = [ + [ 0.0, 0.0 ], + [ 340.0, 0.0 ], + [ 345.0, 0.00014540800475515425 ], + [ 350.0, 0.00054886401630938053 ], + [ 355.0, 0.0011612159432843328 ], + [ 360.0, 0.0019333120435476303 ], + [ 365.0, 0.0028160000219941139 ], + [ 370.0, 0.0037601280491799116 ], + [ 375.0, 0.0047165439464151859 ], + [ 380.0, 0.0056360960006713867 ], + [ 385.0, 0.0064696320332586765 ], + [ 390.0, 0.0071680000983178616 ], + [ 395.0, 0.0076820477843284607 ], + [ 400.0, 0.0079626236110925674 ], + [ 402.5, 0.0080000003799796104 ], + [ 527.5, 0.0080000003799796104 ], + [ 532.5, 0.0078545920550823212 ], + [ 537.5, 0.0074511361308395863 ], + [ 542.5, 0.0068387840874493122 ], + [ 547.5, 0.0060666878707706928 ], + [ 552.5, 0.0051839998923242092 ], + [ 557.5, 0.0042398720979690552 ], + [ 562.5, 0.0032834559679031372 ], + [ 567.5, 0.0023639039136469364 ], + [ 572.5, 0.0015303679974749684 ], + [ 577.5, 0.00083199999062344432 ], + [ 582.5, 0.00031795201357454062 ], + [ 587.5, 3.7376001273514703e-05 ], + [ 590.0, 0.0 ], + [ 720.0, 0.0 ] +] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'cylinder' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Cylinder 1' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ 0.0, 0.39258331060409546, -0.39999997615814209 ], [ 0.0, 0.39258331060409546, 0.5 ] ] +position_m = [ -0.046500001102685928, 0.11523809283971786, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 75.0 +valve_spring_n_m = 38000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 390.0 +exhaust_open_deg = 130.0 +exhaust_profile = [ + [ 0.0, 0.0 ], + [ 130.0, 0.0 ], + [ 135.0, 0.0001263089943677187 ], + [ 140.0, 0.00047792401164770126 ], + [ 145.0, 0.0010138830402866006 ], + [ 150.0, 0.0016932180151343346 ], + [ 155.0, 0.0024749659933149815 ], + [ 160.0, 0.0033181610051542521 ], + [ 165.0, 0.0041818390600383282 ], + [ 170.0, 0.0050250338390469551 ], + [ 175.0, 0.005806781817227602 ], + [ 180.0, 0.0064861169084906578 ], + [ 185.0, 0.0070220758207142353 ], + [ 190.0, 0.0073736910708248615 ], + [ 195.0, 0.0074999998323619366 ], + [ 325.0, 0.0074999998323619366 ], + [ 330.0, 0.0073736910708248615 ], + [ 335.0, 0.0070220758207142353 ], + [ 340.0, 0.0064861169084906578 ], + [ 345.0, 0.005806781817227602 ], + [ 350.0, 0.0050250338390469551 ], + [ 355.0, 0.0041818390600383282 ], + [ 360.0, 0.0033181610051542521 ], + [ 365.0, 0.0024749659933149815 ], + [ 370.0, 0.0016932180151343346 ], + [ 375.0, 0.0010138830402866006 ], + [ 380.0, 0.00047792401164770126 ], + [ 385.0, 0.0001263089943677187 ], + [ 390.0, 0.0 ], + [ 720.0, 0.0 ] +] +exhaust_valve_count = 1 +exhaust_valve_m = 0.026000000536441803 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 8 +injection_enabled = true +intake_close_deg = 590.0 +intake_open_deg = 340.0 +intake_profile = [ + [ 0.0, 0.0 ], + [ 340.0, 0.0 ], + [ 345.0, 0.00014540800475515425 ], + [ 350.0, 0.00054886401630938053 ], + [ 355.0, 0.0011612159432843328 ], + [ 360.0, 0.0019333120435476303 ], + [ 365.0, 0.0028160000219941139 ], + [ 370.0, 0.0037601280491799116 ], + [ 375.0, 0.0047165439464151859 ], + [ 380.0, 0.0056360960006713867 ], + [ 385.0, 0.0064696320332586765 ], + [ 390.0, 0.0071680000983178616 ], + [ 395.0, 0.0076820477843284607 ], + [ 400.0, 0.0079626236110925674 ], + [ 402.5, 0.0080000003799796104 ], + [ 527.5, 0.0080000003799796104 ], + [ 532.5, 0.0078545920550823212 ], + [ 537.5, 0.0074511361308395863 ], + [ 542.5, 0.0068387840874493122 ], + [ 547.5, 0.0060666878707706928 ], + [ 552.5, 0.0051839998923242092 ], + [ 557.5, 0.0042398720979690552 ], + [ 562.5, 0.0032834559679031372 ], + [ 567.5, 0.0023639039136469364 ], + [ 572.5, 0.0015303679974749684 ], + [ 577.5, 0.00083199999062344432 ], + [ 582.5, 0.00031795201357454062 ], + [ 587.5, 3.7376001273514703e-05 ], + [ 590.0, 0.0 ], + [ 720.0, 0.0 ] +] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'cylinder' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Cylinder 2' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 180.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ 0.0, 0.39258331060409546, -0.39999997615814209 ], [ 0.0, 0.39258331060409546, 0.5 ] ] +position_m = [ 0.046500001102685928, 0.11523809283971786, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 75.0 +valve_spring_n_m = 38000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 9 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'intake_plenum' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ -0.25, 0.0, 0.49999994039535522 ], [ 0.25, 0.0, 0.49999994039535522 ], [ -0.5, 0.0, 0.0 ] ] +position_m = [ 0.0, 0.14447620511054993, -0.2175000011920929 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.18600000441074371, 0.039824798703193665, 0.13500000536441803 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.026000000536441803 +discharge = 0.69999998807907104 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 10 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'throttle' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Electronic throttle' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.11299999803304672, 0.14447620511054993, -0.2175000011920929 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, -0.0, 0.0 ] +size_m = [ 0.029999997466802597, 0.027499999850988388, 0.027499999850988388 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.027499999850988388 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 11 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Inlet metering stub' +opening = 1.0 +outlet_diameter_m = 0.027499999850988388 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.097999997437000275, 0.14447620511054993, -0.2175000011920929 ], [ -0.093000002205371857, 0.14447620511054993, -0.2175000011920929 ] ] +port_coordinates = [] +position_m = [ 0.0, 0.0, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 12 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'ecu' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Injection / ignition ECU' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.0, 0.39447620511054993, -0.2175000011920929 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.10000000149011612, 0.02500000037252903, 0.075000002980232239 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.ecu] + fixed_idle = false + idle_base_opening = 0.079999998211860657 + idle_ki = 3.9999998989515007e-05 + idle_kp = 9.9999997473787516e-05 + idle_max_opening = 0.25 + idle_minimum_advance = -5.0 + idle_rpm = 1800.0 + idle_spark_gain = 0.0 + ignition_map = [ [ 0.0, 10.0 ], [ 15000.0, 40.0 ] ] + overrun_cut_rpm = 3500.0 + overrun_fuel_cut_enabled = true + overrun_ignition_cut_enabled = false + overrun_resume_rpm = 2400.0 + power_afr = 12.800000190734863 + rev_limit_cut_time = 0.0 + rev_limit_enabled = true + rev_limit_fuel_cut_enabled = true + rev_limit_ignition_cut_enabled = true + rev_limit_rpm = 10000.0 + rev_resume_rpm = 9750.0 + stoichiometric_afr = 14.699999809265137 + throttle_closing_rate = 0.0 + throttle_closing_time_constant = 0.0 + throttle_rate = 1000.0 + throttle_time_constant = 0.0010000000474974513 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 13 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Intake atmosphere' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.15299999713897705, 0.14447620511054993, -0.2175000011920929 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 14 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'exhaust_collector' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ -0.36721128225326538, 0.50000089406967163, -0.19112648069858551 ], [ -0.13410145044326782, 0.50000089406967163, -0.19112648069858551 ], [ 0.50000089406967163, 0.0, 0.0 ] ] +position_m = [ 0.10000000149011612, -0.054999999701976776, 0.11375000327825546 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.053132899105548859, 0.053132899105548859, 0.053132899105548859 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 700.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 15 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Exhaust atmosphere' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.52656632661819458, -0.054999999701976776, 0.11375000327825546 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 diff --git a/machines/sensor-alpha-n-bench.machine.toml b/machines/sensor-alpha-n-bench.machine.toml new file mode 100644 index 0000000..febf65e --- /dev/null +++ b/machines/sensor-alpha-n-bench.machine.toml @@ -0,0 +1,1399 @@ +cells_per_meter = 40.0 +format = 9 +name = 'Sensor Alpha-N EFI bench' +next_id = 15 +next_sensor_id = 3 +primary_shaft = 13 +scene = 2 +solver = 'rusanov' + +[logic] +next_id = 4 + + [[logic.nodes]] + below = false + binding_label = '' + boolean = false + clamp = true + command = 'motor_enable' + ecu_strategy = 'ignition' + false_value = 0.0 + gain = 1.0 + high = 0.75 + id = 1 + initial = false + input_count = 2 + input_high = 1.0 + input_low = 0.0 + kind = 'sensor' + low = 0.25 + maximum = 1.0 + minimum = 0.0 + name = 'Crank pickup' + offset = 0.0 + output_high = 1.0 + output_low = 0.0 + output_pressure_reference = 'none' + output_quantity = 'scalar' + part = 0 + position = [ 0.0, 0.0 ] + pressure_reference = 'none' + quantity = 'scalar' + sensor = 1 + sensor_kind = 'crank' + tolerance = 9.9999999999999995e-07 + true_value = 1.0 + value = 0.0 + + [[logic.nodes]] + below = false + binding_label = 'Alpha-N ECU' + boolean = false + clamp = true + command = 'motor_enable' + ecu_strategy = 'alpha_n' + false_value = 0.0 + gain = 1.0 + high = 0.75 + id = 2 + initial = false + input_count = 2 + input_high = 1.0 + input_low = 0.0 + kind = 'ecu' + low = 0.25 + maximum = 1.0 + minimum = 0.0 + name = 'Engine ECU inputs' + offset = 0.0 + output_high = 1.0 + output_low = 0.0 + output_pressure_reference = 'none' + output_quantity = 'scalar' + part = 7 + position = [ 340.0, 0.0 ] + pressure_reference = 'none' + quantity = 'scalar' + sensor = 0 + sensor_kind = 'pressure' + tolerance = 9.9999999999999995e-07 + true_value = 1.0 + value = 0.0 + + [[logic.nodes]] + below = false + binding_label = '' + boolean = false + clamp = true + command = 'motor_enable' + ecu_strategy = 'ignition' + false_value = 0.0 + gain = 1.0 + high = 0.75 + id = 3 + initial = false + input_count = 2 + input_high = 1.0 + input_low = 0.0 + kind = 'sensor' + low = 0.25 + maximum = 1.0 + minimum = 0.0 + name = 'Actual throttle position' + offset = 0.0 + output_high = 1.0 + output_low = 0.0 + output_pressure_reference = 'none' + output_quantity = 'scalar' + part = 0 + position = [ 0.0, 230.0 ] + pressure_reference = 'none' + quantity = 'scalar' + sensor = 2 + sensor_kind = 'throttle_position' + tolerance = 9.9999999999999995e-07 + true_value = 1.0 + value = 0.0 + + [[logic.wires]] + from_node = 1 + from_port = 'speed' + to_node = 2 + to_port = 'speed' + + [[logic.wires]] + from_node = 1 + from_port = 'angle' + to_node = 2 + to_port = 'angle' + + [[logic.wires]] + from_node = 3 + from_port = 'value' + to_node = 2 + to_port = 'tps' + +[[connections]] +a_part = 7 +a_port = 'assignments' +b_part = 4 +b_port = 'ecu_assignment' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'ecu_assignment' + +[[connections]] +a_part = 8 +a_port = 'start' +b_part = 1 +b_port = 'gas' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 8 +a_port = 'end' +b_part = 2 +b_port = 'inlet' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 9 +a_port = 'start' +b_part = 2 +b_port = 'outlet' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 9 +a_port = 'end' +b_part = 3 +b_port = 'face_x_negative' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 10 +a_port = 'start' +b_part = 3 +b_port = 'face_x_positive' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 10 +a_port = 'end' +b_part = 4 +b_port = 'intake' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 11 +a_port = 'start' +b_part = 4 +b_port = 'exhaust' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 11 +a_port = 'end' +b_part = 5 +b_port = 'face_x_negative' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 12 +a_port = 'start' +b_part = 5 +b_port = 'face_x_positive' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 12 +a_port = 'end' +b_part = 6 +b_port = 'gas' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 13 +a_port = 'shaft' +b_part = 4 +b_port = 'shaft' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'mechanical' + +[[connections]] +a_part = 13 +a_port = 'shaft' +b_part = 14 +b_port = 'shaft' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'mechanical' + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 1 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pressure / atmosphere 1' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.64999997615814209, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 2 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'throttle' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Throttle valve 2' +opening = 0.5 +outlet_diameter_m = 0.039999999105930328 +pedal_control = false +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.40000000596046448, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.05000000074505806, 0.05000000074505806 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 3 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Chamber / tank 3' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.20000000298023224, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.10000000149011612, 0.079999998211860657, 0.079999998211860657 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.injector] + cylinders = 1 + ecu = 7 + ecu_label = 'Alpha-N ECU' + finite_supply = true + initial_kg = 0.10000000149011612 + maximum_kg_s = 0.0010000000474974513 + temperature_k = 300.0 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 4 +ignition_tdc_deg = 0.0 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'cylinder' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Engine cylinder 4' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.0, 0.20000000298023224, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 5 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Chamber / tank 5' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.20000000298023224, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.079999998211860657, 0.079999998211860657, 0.079999998211860657 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 6 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pressure / atmosphere 6' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.64999997615814209, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 7 +injection_enabled = false +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'ecu' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Alpha-N ECU' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.0, 0.5, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.10000000149011612, 0.02500000037252903, 0.075000002980232239 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.ecu] + fixed_idle = false + idle_base_opening = 0.079999998211860657 + idle_ki = 3.9999998989515007e-05 + idle_kp = 9.9999997473787516e-05 + idle_max_opening = 0.25 + idle_minimum_advance = -5.0 + idle_rpm = 1800.0 + idle_spark_gain = 0.0 + ignition_map = [ [ 0.0, 10.0 ], [ 15000.0, 40.0 ] ] + overrun_cut_rpm = 3500.0 + overrun_fuel_cut_enabled = true + overrun_ignition_cut_enabled = false + overrun_resume_rpm = 2400.0 + power_afr = 12.800000190734863 + rev_limit_cut_time = 0.019999999552965164 + rev_limit_enabled = false + rev_limit_fuel_cut_enabled = true + rev_limit_ignition_cut_enabled = true + rev_limit_rpm = 10000.0 + rev_resume_rpm = 9750.0 + stoichiometric_afr = 14.699999809265137 + throttle_closing_rate = 0.0 + throttle_closing_time_constant = 0.0 + throttle_rate = 1000.0 + throttle_time_constant = 0.0010000000474974513 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.sensor_ecu] + enabled = true + fuel_kg_cycle = [ 1.9999999949504854e-06, 3.9999998989515007e-05, 1.9999999949504854e-06, 3.9999998989515007e-05 ] + fuel_rpm = [ 0.0, 6000.0 ] + fuel_tps = [ 0.0, 1.0 ] + strategy = 'alpha_n' + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 8 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 8' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.032000001519918442, 0.032000001519918442 ] +points_m = [ [ -0.11500000953674316, 0.0, 0.0 ], [ 0.11499994993209839, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.53499996662139893, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 9 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 9' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.032000001519918442, 0.032000001519918442 ] +points_m = [ [ -0.064999997615814209, 0.0, 0.0 ], [ 0.064999997615814209, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.31499999761581421, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 10 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 10' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.032000001519918442, 0.032000001519918442 ] +points_m = [ [ -0.066375002264976501, 0.031380951404571533, 0.0 ], [ 0.066375002264976501, -0.031380951404571533, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.083625003695487976, 0.26861906051635742, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 11 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 11' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.032000001519918442, 0.032000001519918442 ] +points_m = [ [ -0.071374997496604919, -0.031380951404571533, 0.0 ], [ 0.071374997496604919, 0.031380951404571533, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.088624998927116394, 0.26861906051635742, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 12 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 12' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.032000001519918442, 0.032000001519918442 ] +points_m = [ [ -0.20499998331069946, 0.0, 0.0 ], [ 0.20499998331069946, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.44499999284744263, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 13 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'shaft' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Main crankshaft' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.0, 0.049999997019767761, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.25, 0.035000000149011612, 0.035000000149011612 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.shaft] + friction_nm = 0.15000000596046448 + inertia_kg_m2 = 0.15000000596046448 + initial_angle_rad = 0.0 + initial_speed_rad_s = 0.0 + quadratic_nm_s2 = 0.0 + starter_nm = 15.0 + viscous_nm_s = 0.0020000000949949026 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 14 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'drive_motor' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = '3000 RPM test drive' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.30000001192092896, 0.049999997019767761, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.090000003576278687, 0.090000003576278687 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.drive] + enabled = true + speed_control = true + target_speed_rad_s = 314.15927124023438 + torque_nm = 1.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[probes]] +generated_output = false +id = 1 +name = 'Cylinder pressure' +part = 4 +position = 0.5 +quantity = 0 +reference_afr = 14.699999809265137 + +[[probes]] +generated_output = false +id = 2 +name = 'Crank RPM' +part = 13 +position = 0.5 +quantity = 6 +reference_afr = 14.699999809265137 + +[[probes]] +generated_output = false +id = 3 +name = 'Charge AFR' +part = 4 +position = 0.5 +quantity = 5 +reference_afr = 14.699999809265137 + +[[sensors]] +angle_offset_rad = 0.0 +id = 1 +kind = 'crank' +name = 'Crank speed / position / Main crankshaft' + + [sensors.reference] + label = '' + part = 0 + position = 0.5 + + [sensors.target] + label = 'Main crankshaft' + part = 13 + position = 0.5 + +[[sensors]] +angle_offset_rad = 0.0 +id = 2 +kind = 'throttle_position' +name = 'Throttle position / Throttle valve 2' + + [sensors.reference] + label = '' + part = 0 + position = 0.5 + + [sensors.target] + label = 'Throttle valve 2' + part = 2 + position = 0.5 diff --git a/machines/sensor-bench.machine.toml b/machines/sensor-bench.machine.toml new file mode 100644 index 0000000..d08fa2e --- /dev/null +++ b/machines/sensor-bench.machine.toml @@ -0,0 +1,165 @@ +format = 7 +name = 'Ideal sensor bench' +primary_shaft = 6 +next_id = 8 +next_sensor_id = 7 +cells_per_meter = 40.0 +solver = 'rusanov' +scene = 0 + +[[parts]] +id = 1 +kind = 'reservoir' +name = 'Compressed air supply' +position_m = [-0.5, 0.25, 0.0] +pressure_pa = 200000.0 +temperature_k = 300.0 + +[[parts]] +id = 2 +kind = 'throttle' +name = 'Fixed opening valve' +position_m = [-0.1, 0.25, 0.0] +size_m = [0.04, 0.05, 0.05] +pedal_control = false +opening = 0.45 + +[[parts]] +id = 3 +kind = 'chamber' +name = 'Receiving tank' +position_m = [0.3, 0.25, 0.0] +size_m = [0.12, 0.12, 0.12] + +[[parts]] +id = 4 +kind = 'pipe' +name = 'Supply pipe' +position_m = [0.0, 0.0, 0.0] +points_m = [[-0.5, 0.25, 0.0], [-0.12, 0.25, 0.0]] +point_diameters_m = [0.04, 0.04] + +[[parts]] +id = 5 +kind = 'pipe' +name = 'Measured filling pipe' +position_m = [0.0, 0.0, 0.0] +points_m = [[-0.08, 0.25, 0.0], [0.24, 0.25, 0.0]] +point_diameters_m = [0.04, 0.04] + +[[parts]] +id = 6 +kind = 'shaft' +name = 'Pickup demonstration shaft' +position_m = [0.1, 0.2, 0.3] +size_m = [0.25, 0.035, 0.035] +[parts.shaft] +inertia_kg_m2 = 0.15 + +[[parts]] +id = 7 +kind = 'drive_motor' +name = 'Ideal speed drive' +position_m = [-0.25, 0.2, 0.3] +size_m = [0.12, 0.09, 0.09] +[parts.drive] +speed_control = true +target_speed_rad_s = 188.49556 +enabled = true + +[[connections]] +a_part = 4 +a_port = 'start' +b_part = 1 +b_port = 'gas' +method = 'orifice' + +[[connections]] +a_part = 4 +a_port = 'end' +b_part = 2 +b_port = 'inlet' +method = 'orifice' + +[[connections]] +a_part = 5 +a_port = 'start' +b_part = 2 +b_port = 'outlet' +method = 'orifice' + +[[connections]] +a_part = 5 +a_port = 'end' +b_part = 3 +b_port = 'face_x_negative' +method = 'orifice' + +[[connections]] +a_part = 6 +a_port = 'shaft' +b_part = 7 +b_port = 'shaft' +method = 'mechanical' + +[[sensors]] +id = 1 +kind = 'pressure' +name = 'Tank absolute pressure' +target = {part = 3, position = 0.5, label = 'Receiving tank'} +reference = {part = 0, position = 0.5, label = ''} + +[[sensors]] +id = 2 +kind = 'gas_temperature' +name = 'Tank gas temperature' +target = {part = 3, position = 0.5, label = 'Receiving tank'} +reference = {part = 0, position = 0.5, label = ''} + +[[sensors]] +id = 3 +kind = 'differential_pressure' +name = 'Supply minus tank pressure' +target = {part = 1, position = 0.5, label = 'Compressed air supply'} +reference = {part = 3, position = 0.5, label = 'Receiving tank'} + +[[sensors]] +id = 4 +kind = 'mass_flow' +name = 'Signed total filling flow' +target = {part = 5, position = 0.5, label = 'Measured filling pipe'} +reference = {part = 0, position = 0.5, label = ''} + +[[sensors]] +id = 5 +kind = 'crank' +name = 'Shaft speed and crank position' +angle_offset_rad = 0.0 +target = {part = 6, position = 0.5, label = 'Pickup demonstration shaft'} +reference = {part = 0, position = 0.5, label = ''} + +[[sensors]] +id = 6 +kind = 'throttle_position' +name = 'Valve mechanism opening' +target = {part = 2, position = 0.5, label = 'Fixed opening valve'} +reference = {part = 0, position = 0.5, label = ''} + +[[probes]] +id = 1 +part = 3 +name = 'Tank pressure' +quantity = 0 + +[[probes]] +id = 2 +part = 5 +name = 'Total filling flow' +quantity = 2 +position = 0.5 + +[[probes]] +id = 3 +part = 6 +name = 'Demonstration shaft RPM' +quantity = 6 diff --git a/machines/sensor-ignition-bench.machine.toml b/machines/sensor-ignition-bench.machine.toml new file mode 100644 index 0000000..e7a2b2a --- /dev/null +++ b/machines/sensor-ignition-bench.machine.toml @@ -0,0 +1,1357 @@ +cells_per_meter = 40.0 +format = 9 +name = 'Sensor crank ignition / carburetor bench' +next_id = 15 +next_sensor_id = 2 +primary_shaft = 13 +scene = 2 +solver = 'rusanov' + +[logic] +next_id = 3 + + [[logic.nodes]] + below = false + binding_label = '' + boolean = false + clamp = true + command = 'motor_enable' + ecu_strategy = 'ignition' + false_value = 0.0 + gain = 1.0 + high = 0.75 + id = 1 + initial = false + input_count = 2 + input_high = 1.0 + input_low = 0.0 + kind = 'sensor' + low = 0.25 + maximum = 1.0 + minimum = 0.0 + name = 'Crank pickup' + offset = 0.0 + output_high = 1.0 + output_low = 0.0 + output_pressure_reference = 'none' + output_quantity = 'scalar' + part = 0 + position = [ 0.0, 0.0 ] + pressure_reference = 'none' + quantity = 'scalar' + sensor = 1 + sensor_kind = 'crank' + tolerance = 9.9999999999999995e-07 + true_value = 1.0 + value = 0.0 + + [[logic.nodes]] + below = false + binding_label = 'Crank ignition ECU' + boolean = false + clamp = true + command = 'motor_enable' + ecu_strategy = 'ignition' + false_value = 0.0 + gain = 1.0 + high = 0.75 + id = 2 + initial = false + input_count = 2 + input_high = 1.0 + input_low = 0.0 + kind = 'ecu' + low = 0.25 + maximum = 1.0 + minimum = 0.0 + name = 'Engine ECU inputs' + offset = 0.0 + output_high = 1.0 + output_low = 0.0 + output_pressure_reference = 'none' + output_quantity = 'scalar' + part = 7 + position = [ 340.0, 0.0 ] + pressure_reference = 'none' + quantity = 'scalar' + sensor = 0 + sensor_kind = 'pressure' + tolerance = 9.9999999999999995e-07 + true_value = 1.0 + value = 0.0 + + [[logic.wires]] + from_node = 1 + from_port = 'speed' + to_node = 2 + to_port = 'speed' + + [[logic.wires]] + from_node = 1 + from_port = 'angle' + to_node = 2 + to_port = 'angle' + +[[connections]] +a_part = 7 +a_port = 'assignments' +b_part = 4 +b_port = 'ecu_assignment' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'ecu_assignment' + +[[connections]] +a_part = 8 +a_port = 'start' +b_part = 1 +b_port = 'gas' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 8 +a_port = 'end' +b_part = 2 +b_port = 'inlet' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 9 +a_port = 'start' +b_part = 2 +b_port = 'outlet' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 9 +a_port = 'end' +b_part = 3 +b_port = 'face_x_negative' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 10 +a_port = 'start' +b_part = 3 +b_port = 'face_x_positive' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 10 +a_port = 'end' +b_part = 4 +b_port = 'intake' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 11 +a_port = 'start' +b_part = 4 +b_port = 'exhaust' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 11 +a_port = 'end' +b_part = 5 +b_port = 'face_x_negative' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 12 +a_port = 'start' +b_part = 5 +b_port = 'face_x_positive' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 12 +a_port = 'end' +b_part = 6 +b_port = 'gas' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 13 +a_port = 'shaft' +b_part = 4 +b_port = 'shaft' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'mechanical' + +[[connections]] +a_part = 13 +a_port = 'shaft' +b_part = 14 +b_port = 'shaft' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'mechanical' + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 1 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pressure / atmosphere 1' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.64999997615814209, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 2 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'carburetor' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Physical carburetor' +opening = 0.5 +outlet_diameter_m = 0.039999999105930328 +pedal_control = false +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.40000000596046448, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.05000000074505806, 0.05000000074505806 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.carburetor] + bowl_capacity = 0.0080000003799796104 + bowl_head_pressure = 0.0 + bowl_refill_rate = 0.0024999999441206455 + film_fraction = 0.2800000011920929 + film_time_constant = 0.10999999940395355 + fuel_density = 740.0 + main_discharge_coefficient = 0.72000002861022949 + main_jet_diameter = 0.001500000013038516 + needle_jet_diameter = 0.0019000000320374966 + needle_low_scale = 1.0 + needle_mid_scale = 1.0 + needle_off_idle_scale = 1.0 + needle_quarter_scale = 1.0 + needle_tip_scale = 1.0 + needle_transition_scale = 1.0 + needle_upper_scale = 1.0 + overrun_exit_rpm = 3300.0 + overrun_rpm = 4200.0 + pilot_discharge_coefficient = 0.62000000476837158 + pilot_jet_diameter = 0.00039999998989515007 + pilot_tip_scale = 1.0 + pilot_transition_scale = 1.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 3 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Chamber / tank 3' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.20000000298023224, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.10000000149011612, 0.079999998211860657, 0.079999998211860657 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 4 +ignition_tdc_deg = 0.0 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'cylinder' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Engine cylinder 4' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.0, 0.20000000298023224, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 5 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Chamber / tank 5' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.20000000298023224, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.079999998211860657, 0.079999998211860657, 0.079999998211860657 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 6 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pressure / atmosphere 6' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.64999997615814209, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 7 +injection_enabled = false +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'ecu' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Crank ignition ECU' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.0, 0.5, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.10000000149011612, 0.02500000037252903, 0.075000002980232239 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.ecu] + fixed_idle = false + idle_base_opening = 0.079999998211860657 + idle_ki = 3.9999998989515007e-05 + idle_kp = 9.9999997473787516e-05 + idle_max_opening = 0.25 + idle_minimum_advance = -5.0 + idle_rpm = 1800.0 + idle_spark_gain = 0.0 + ignition_map = [ [ 0.0, 10.0 ], [ 15000.0, 40.0 ] ] + overrun_cut_rpm = 3500.0 + overrun_fuel_cut_enabled = true + overrun_ignition_cut_enabled = false + overrun_resume_rpm = 2400.0 + power_afr = 12.800000190734863 + rev_limit_cut_time = 0.019999999552965164 + rev_limit_enabled = false + rev_limit_fuel_cut_enabled = true + rev_limit_ignition_cut_enabled = true + rev_limit_rpm = 10000.0 + rev_resume_rpm = 9750.0 + stoichiometric_afr = 14.699999809265137 + throttle_closing_rate = 0.0 + throttle_closing_time_constant = 0.0 + throttle_rate = 1000.0 + throttle_time_constant = 0.0010000000474974513 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.sensor_ecu] + enabled = true + fuel_kg_cycle = [ 1.9999999949504854e-06, 1.9999999494757503e-05, 1.9999999949504854e-06, 1.9999999494757503e-05 ] + fuel_rpm = [ 0.0, 6000.0 ] + fuel_tps = [ 0.0, 1.0 ] + strategy = 'ignition' + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 8 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 8' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.032000001519918442, 0.032000001519918442 ] +points_m = [ [ -0.11500000953674316, 0.0, 0.0 ], [ 0.11499994993209839, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.53499996662139893, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 9 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 9' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.032000001519918442, 0.032000001519918442 ] +points_m = [ [ -0.064999997615814209, 0.0, 0.0 ], [ 0.064999997615814209, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.31499999761581421, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 10 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 10' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.032000001519918442, 0.032000001519918442 ] +points_m = [ [ -0.066375002264976501, 0.031380951404571533, 0.0 ], [ 0.066375002264976501, -0.031380951404571533, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.083625003695487976, 0.26861906051635742, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 11 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 11' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.032000001519918442, 0.032000001519918442 ] +points_m = [ [ -0.071374997496604919, -0.031380951404571533, 0.0 ], [ 0.071374997496604919, 0.031380951404571533, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.088624998927116394, 0.26861906051635742, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 12 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 12' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.032000001519918442, 0.032000001519918442 ] +points_m = [ [ -0.20499998331069946, 0.0, 0.0 ], [ 0.20499998331069946, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.44499999284744263, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 13 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'shaft' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Main crankshaft' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.0, 0.049999997019767761, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.25, 0.035000000149011612, 0.035000000149011612 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.shaft] + friction_nm = 0.15000000596046448 + inertia_kg_m2 = 0.15000000596046448 + initial_angle_rad = 0.0 + initial_speed_rad_s = 0.0 + quadratic_nm_s2 = 0.0 + starter_nm = 15.0 + viscous_nm_s = 0.0020000000949949026 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 14 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'drive_motor' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = '3000 RPM test drive' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.30000001192092896, 0.049999997019767761, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.090000003576278687, 0.090000003576278687 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.drive] + enabled = true + speed_control = true + target_speed_rad_s = 314.15927124023438 + torque_nm = 1.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[probes]] +generated_output = false +id = 1 +name = 'Cylinder pressure' +part = 4 +position = 0.5 +quantity = 0 +reference_afr = 14.699999809265137 + +[[probes]] +generated_output = false +id = 2 +name = 'Crank RPM' +part = 13 +position = 0.5 +quantity = 6 +reference_afr = 14.699999809265137 + +[[probes]] +generated_output = false +id = 3 +name = 'Charge AFR' +part = 4 +position = 0.5 +quantity = 5 +reference_afr = 14.699999809265137 + +[[sensors]] +angle_offset_rad = 0.0 +id = 1 +kind = 'crank' +name = 'Crank speed / position / Main crankshaft' + + [sensors.reference] + label = '' + part = 0 + position = 0.5 + + [sensors.target] + label = 'Main crankshaft' + part = 13 + position = 0.5 diff --git a/machines/sensor-speed-density-bench.machine.toml b/machines/sensor-speed-density-bench.machine.toml new file mode 100644 index 0000000..e58a863 --- /dev/null +++ b/machines/sensor-speed-density-bench.machine.toml @@ -0,0 +1,1462 @@ +cells_per_meter = 40.0 +format = 10 +name = 'Sensor speed-density EFI bench' +next_id = 15 +next_sensor_id = 5 +primary_shaft = 13 +scene = 2 +solver = 'rusanov' + +[logic] +next_id = 6 + + [[logic.nodes]] + below = false + binding_label = '' + boolean = false + clamp = true + command = 'motor_enable' + ecu_strategy = 'ignition' + false_value = 0.0 + gain = 1.0 + high = 0.75 + id = 1 + initial = false + input_count = 2 + input_high = 1.0 + input_low = 0.0 + kind = 'sensor' + low = 0.25 + maximum = 1.0 + minimum = 0.0 + name = 'Crank pickup' + offset = 0.0 + output_high = 1.0 + output_low = 0.0 + output_pressure_reference = 'none' + output_quantity = 'scalar' + part = 0 + position = [ 0.0, 0.0 ] + pressure_reference = 'none' + quantity = 'scalar' + sensor = 1 + sensor_kind = 'crank' + tolerance = 9.9999999999999995e-07 + true_value = 1.0 + value = 0.0 + + [[logic.nodes]] + below = false + binding_label = 'Speed-density ECU' + boolean = false + clamp = true + command = 'motor_enable' + ecu_strategy = 'speed_density' + false_value = 0.0 + gain = 1.0 + high = 0.75 + id = 2 + initial = false + input_count = 2 + input_high = 1.0 + input_low = 0.0 + kind = 'ecu' + low = 0.25 + maximum = 1.0 + minimum = 0.0 + name = 'Engine ECU inputs' + offset = 0.0 + output_high = 1.0 + output_low = 0.0 + output_pressure_reference = 'none' + output_quantity = 'scalar' + part = 7 + position = [ 340.0, 80.0 ] + pressure_reference = 'none' + quantity = 'scalar' + sensor = 0 + sensor_kind = 'pressure' + tolerance = 9.9999999999999995e-07 + true_value = 1.0 + value = 0.0 + + [[logic.nodes]] + below = false + binding_label = '' + boolean = false + clamp = true + command = 'motor_enable' + ecu_strategy = 'ignition' + false_value = 0.0 + gain = 1.0 + high = 0.75 + id = 4 + initial = false + input_count = 2 + input_high = 1.0 + input_low = 0.0 + kind = 'sensor' + low = 0.25 + maximum = 1.0 + minimum = 0.0 + name = 'Measured manifold pressure' + offset = 0.0 + output_high = 1.0 + output_low = 0.0 + output_pressure_reference = 'none' + output_quantity = 'scalar' + part = 0 + position = [ 0.0, 240.0 ] + pressure_reference = 'none' + quantity = 'scalar' + sensor = 3 + sensor_kind = 'pressure' + tolerance = 9.9999999999999995e-07 + true_value = 1.0 + value = 0.0 + + [[logic.nodes]] + below = false + binding_label = '' + boolean = false + clamp = true + command = 'motor_enable' + ecu_strategy = 'ignition' + false_value = 0.0 + gain = 1.0 + high = 0.75 + id = 5 + initial = false + input_count = 2 + input_high = 1.0 + input_low = 0.0 + kind = 'sensor' + low = 0.25 + maximum = 1.0 + minimum = 0.0 + name = 'Measured intake temperature' + offset = 0.0 + output_high = 1.0 + output_low = 0.0 + output_pressure_reference = 'none' + output_quantity = 'scalar' + part = 0 + position = [ 0.0, 460.0 ] + pressure_reference = 'none' + quantity = 'scalar' + sensor = 4 + sensor_kind = 'gas_temperature' + tolerance = 9.9999999999999995e-07 + true_value = 1.0 + value = 0.0 + + [[logic.wires]] + from_node = 1 + from_port = 'speed' + to_node = 2 + to_port = 'speed' + + [[logic.wires]] + from_node = 1 + from_port = 'angle' + to_node = 2 + to_port = 'angle' + + [[logic.wires]] + from_node = 4 + from_port = 'value' + to_node = 2 + to_port = 'map' + + [[logic.wires]] + from_node = 5 + from_port = 'value' + to_node = 2 + to_port = 'iat' + +[[connections]] +a_part = 7 +a_port = 'assignments' +b_part = 4 +b_port = 'ecu_assignment' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'ecu_assignment' + +[[connections]] +a_part = 8 +a_port = 'start' +b_part = 1 +b_port = 'gas' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 8 +a_port = 'end' +b_part = 2 +b_port = 'inlet' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 9 +a_port = 'start' +b_part = 2 +b_port = 'outlet' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 9 +a_port = 'end' +b_part = 3 +b_port = 'face_x_negative' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 10 +a_port = 'start' +b_part = 3 +b_port = 'face_x_positive' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 10 +a_port = 'end' +b_part = 4 +b_port = 'intake' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 11 +a_port = 'start' +b_part = 4 +b_port = 'exhaust' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 11 +a_port = 'end' +b_part = 5 +b_port = 'face_x_negative' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 12 +a_port = 'start' +b_part = 5 +b_port = 'face_x_positive' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 12 +a_port = 'end' +b_part = 6 +b_port = 'gas' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'orifice' + +[[connections]] +a_part = 13 +a_port = 'shaft' +b_part = 4 +b_port = 'shaft' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'mechanical' + +[[connections]] +a_part = 13 +a_port = 'shaft' +b_part = 14 +b_port = 'shaft' +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 'mechanical' + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 1 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pressure / atmosphere 1' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.64999997615814209, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 2 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'throttle' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Throttle valve 2' +opening = 0.5 +outlet_diameter_m = 0.039999999105930328 +pedal_control = false +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.40000000596046448, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.05000000074505806, 0.05000000074505806 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 3 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Chamber / tank 3' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.20000000298023224, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.10000000149011612, 0.079999998211860657, 0.079999998211860657 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.injector] + cylinders = 1 + ecu = 7 + ecu_label = 'Speed-density ECU' + finite_supply = true + initial_kg = 0.10000000149011612 + maximum_kg_s = 0.0010000000474974513 + temperature_k = 300.0 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 4 +ignition_tdc_deg = 0.0 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'cylinder' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Engine cylinder 4' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.0, 0.20000000298023224, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 5 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Chamber / tank 5' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.20000000298023224, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.079999998211860657, 0.079999998211860657, 0.079999998211860657 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 6 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pressure / atmosphere 6' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.64999997615814209, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 7 +injection_enabled = false +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'ecu' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Speed-density ECU' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.0, 0.5, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.10000000149011612, 0.02500000037252903, 0.075000002980232239 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.ecu] + fixed_idle = false + idle_base_opening = 0.079999998211860657 + idle_ki = 3.9999998989515007e-05 + idle_kp = 9.9999997473787516e-05 + idle_max_opening = 0.25 + idle_minimum_advance = -5.0 + idle_rpm = 1800.0 + idle_spark_gain = 0.0 + ignition_map = [ [ 0.0, 10.0 ], [ 15000.0, 40.0 ] ] + overrun_cut_rpm = 3500.0 + overrun_fuel_cut_enabled = true + overrun_ignition_cut_enabled = false + overrun_resume_rpm = 2400.0 + power_afr = 12.800000190734863 + rev_limit_cut_time = 0.019999999552965164 + rev_limit_enabled = false + rev_limit_fuel_cut_enabled = true + rev_limit_ignition_cut_enabled = true + rev_limit_rpm = 10000.0 + rev_resume_rpm = 9750.0 + stoichiometric_afr = 14.699999809265137 + throttle_closing_rate = 0.0 + throttle_closing_time_constant = 0.0 + throttle_rate = 1000.0 + throttle_time_constant = 0.0010000000474974513 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.sensor_ecu] + air_r_j_kg_k = 287.0 + displacement_m3 = 0.00030041483114473522 + enabled = true + fuel_kg_cycle = [ 1.9999999949504854e-06, 3.9999998989515007e-05, 1.9999999949504854e-06, 3.9999998989515007e-05 ] + fuel_rpm = [ 0.0, 6000.0 ] + fuel_tps = [ 0.0, 1.0 ] + strategy = 'speed_density' + target_afr = 14.699999809265137 + ve_absolute_pa = [ 20000.0, 100000.0 ] + ve_rpm = [ 0.0, 6000.0 ] + volumetric_efficiency = [ 0.89999997615814209, 0.89999997615814209, 0.89999997615814209, 0.89999997615814209 ] + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 8 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 8' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.032000001519918442, 0.032000001519918442 ] +points_m = [ [ -0.11500000953674316, 0.0, 0.0 ], [ 0.11499994993209839, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.53499996662139893, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 9 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 9' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.032000001519918442, 0.032000001519918442 ] +points_m = [ [ -0.064999997615814209, 0.0, 0.0 ], [ 0.064999997615814209, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.31499999761581421, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 10 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 10' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.032000001519918442, 0.032000001519918442 ] +points_m = [ [ -0.066375002264976501, 0.031380951404571533, 0.0 ], [ 0.066375002264976501, -0.031380951404571533, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.083625003695487976, 0.26861906051635742, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 11 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 11' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.032000001519918442, 0.032000001519918442 ] +points_m = [ [ -0.071374997496604919, -0.031380951404571533, 0.0 ], [ 0.071374997496604919, 0.031380951404571533, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.088624998927116394, 0.26861906051635742, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 12 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe / reducer 12' +opening = 1.0 +pedal_control = true +phase_deg = 0.0 +point_diameters_m = [ 0.032000001519918442, 0.032000001519918442 ] +points_m = [ [ -0.20499998331069946, 0.0, 0.0 ], [ 0.20499998331069946, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.44499999284744263, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 13 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'shaft' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Main crankshaft' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.0, 0.049999997019767761, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.25, 0.035000000149011612, 0.035000000149011612 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.shaft] + friction_nm = 0.15000000596046448 + inertia_kg_m2 = 0.15000000596046448 + initial_angle_rad = 0.0 + initial_speed_rad_s = 0.0 + quadratic_nm_s2 = 0.0 + starter_nm = 15.0 + viscous_nm_s = 0.0020000000949949026 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 14 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'drive_motor' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = '3000 RPM test drive' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.30000001192092896, 0.049999997019767761, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.090000003576278687, 0.090000003576278687 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.drive] + enabled = true + speed_control = true + target_speed_rad_s = 314.15927124023438 + torque_nm = 1.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[probes]] +generated_output = false +id = 1 +name = 'Cylinder pressure' +part = 4 +position = 0.5 +quantity = 0 +reference_afr = 14.699999809265137 + +[[probes]] +generated_output = false +id = 2 +name = 'Crank RPM' +part = 13 +position = 0.5 +quantity = 6 +reference_afr = 14.699999809265137 + +[[probes]] +generated_output = false +id = 3 +name = 'Charge AFR' +part = 4 +position = 0.5 +quantity = 5 +reference_afr = 14.699999809265137 + +[[sensors]] +angle_offset_rad = 0.0 +id = 1 +kind = 'crank' +name = 'Crank speed / position / Main crankshaft' + + [sensors.reference] + label = '' + part = 0 + position = 0.5 + + [sensors.target] + label = 'Main crankshaft' + part = 13 + position = 0.5 + +[[sensors]] +angle_offset_rad = 0.0 +id = 3 +kind = 'pressure' +name = 'Intake MAP / absolute' + + [sensors.reference] + label = '' + part = 0 + position = 0.5 + + [sensors.target] + label = 'Chamber / tank 3' + part = 3 + position = 0.5 + +[[sensors]] +angle_offset_rad = 0.0 +id = 4 +kind = 'gas_temperature' +name = 'Intake gas temperature / IAT' + + [sensors.reference] + label = '' + part = 0 + position = 0.5 + + [sensors.target] + label = 'Chamber / tank 3' + part = 3 + position = 0.5 diff --git a/machines/shaft-drive-bench.machine.toml b/machines/shaft-drive-bench.machine.toml new file mode 100644 index 0000000..9130421 --- /dev/null +++ b/machines/shaft-drive-bench.machine.toml @@ -0,0 +1,93 @@ +format = 6 +name = 'Shaft, flywheel and brake bench' +primary_shaft = 1 +cells_per_meter = 40.0 +solver = 'rusanov' +scene = 0 + +[[parts]] +id = 1 +kind = 'shaft' +name = 'Bench shaft' +position_m = [0.0, 0.22, 0.0] +size_m = [0.25, 0.035, 0.035] +[parts.shaft] +inertia_kg_m2 = 0.15 +friction_nm = 0.15 +viscous_nm_s = 0.002 + +[[parts]] +id = 2 +kind = 'flywheel' +name = 'Flywheel' +position_m = [0.32, 0.22, 0.0] +size_m = [0.025, 0.16, 0.16] +[parts.shaft] +inertia_kg_m2 = 0.35 + +[[parts]] +id = 3 +kind = 'drive_motor' +name = 'Torque motor' +position_m = [-0.32, 0.22, 0.0] +size_m = [0.12, 0.09, 0.09] +[parts.drive] +speed_control = false +torque_nm = 10.0 +enabled = true + +[[parts]] +id = 4 +kind = 'brake' +name = 'Adjustable brake' +position_m = [0.0, 0.22, 0.24] +size_m = [0.07, 0.1, 0.1] +[parts.brake] +constant_nm = 2.0 +viscous_nm_s = 0.01 +enabled = true + +[[connections]] +a_part = 1 +a_port = 'shaft' +b_part = 2 +b_port = 'shaft' +method = 'mechanical' + +[[connections]] +a_part = 1 +a_port = 'shaft' +b_part = 3 +b_port = 'shaft' +method = 'mechanical' + +[[connections]] +a_part = 1 +a_port = 'shaft' +b_part = 4 +b_port = 'shaft' +method = 'mechanical' + +[[probes]] +id = 1 +part = 1 +name = 'Bench RPM' +quantity = 6 + +[[probes]] +id = 2 +part = 3 +name = 'Motor input power' +quantity = 8 + +[[probes]] +id = 3 +part = 4 +name = 'Brake absorbed power' +quantity = 8 + +[[probes]] +id = 4 +part = 1 +name = 'Shaft net torque' +quantity = 7 diff --git a/machines/single-450-imported.machine.toml b/machines/single-450-imported.machine.toml new file mode 100644 index 0000000..b13bf02 --- /dev/null +++ b/machines/single-450-imported.machine.toml @@ -0,0 +1,1134 @@ +cells_per_meter = 20.0 +drive_rpm = 1960.0 +driven = false +format = 2 +friction_constant = 1.7950810194015503 +friction_quadratic = 0.0 +friction_viscous = 0.0017950809560716152 +load_torque = 0.0 +name = '450 cc / carbureted four-stroke thumper / editable' +scene = 2 +shaft_inertia = 0.05385243147611618 +solver = 'rusanov' +starter_torque = 11.219260215759277 + +[[connections]] +a_part = 2 +a_port = 0 +b_part = 6 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 2 +a_port = 1 +b_part = 5 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 3 +a_port = 0 +b_part = 5 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 1 +a_port = 1 +b_part = 7 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 8 +a_port = 0 +b_part = 7 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 8 +a_port = 1 +b_part = 6 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 9 +a_port = 0 +b_part = 5 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 9 +a_port = 0 +b_part = 7 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 1 +a_port = 0 +b_part = 10 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 3 +a_port = 1 +b_part = 11 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 4 +a_port = 0 +b_part = 11 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 4 +a_port = 1 +b_part = 12 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.075000002980232239 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 1 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 0 / single-450' +opening = 1.0 +outlet_diameter_m = 0.075000002980232239 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.099999994039535522, 0.0, 0.0 ], [ 0.01000000536441803, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.15700000524520874, 0.14148889482021332, -0.37479999661445618 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.035000000149011612 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 2 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 1 / single-450' +opening = 1.0 +outlet_diameter_m = 0.035000000149011612 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ 0.0, 0.0, -0.125 ], [ 0.0, 0.0, 0.1249999925494194 ] ] +port_coordinates = [] +position_m = [ 0.0, 0.14148889482021332, -0.16339999437332153 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.037999998778104782 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 3 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 2 / single-450' +opening = 1.0 +outlet_diameter_m = 0.037999998778104782 +pedal_control = true +phase_deg = 0.0 +points_m = [ + [ -0.04536660760641098, 0.064742617309093475, -0.064842469990253448 ], + [ -0.044423606246709824, 0.061924122273921967, -0.052323371171951294 ], + [ -0.041758507490158081, 0.05754021555185318, -0.041306070983409882 ], + [ -0.037617508322000504, 0.051712222397327423, -0.031694769859313965 ], + [ -0.032246407121419907, 0.044561423361301422, -0.023393668234348297 ], + [ -0.025891207158565521, 0.036209225654602051, -0.016307070851325989 ], + [ -0.018798107281327248, 0.026777021586894989, -0.010339073836803436 ], + [ -0.011213008314371109, 0.016386225819587708, -0.0053939670324325562 ], + [ -0.0033818073570728302, 0.0051580220460891724, -0.0013760700821876526 ], + [ 0.0044492930173873901, -0.0067861750721931458, 0.0018104314804077148 ], + [ 0.012034490704536438, -0.019324876368045807, 0.0042614266276359558 ], + [ 0.019127592444419861, -0.032336976379156113, 0.0060725286602973938 ], + [ 0.025482691824436188, -0.045700877904891968, 0.0073396265506744385 ], + [ 0.030853793025016785, -0.059295278042554855, 0.0081584304571151733 ], + [ 0.034994892776012421, -0.072998881340026855, 0.008624732494354248 ], + [ 0.037659890949726105, -0.086690276861190796, 0.0088342279195785522 ], + [ 0.03860299289226532, -0.10024827718734741, 0.0088828280568122864 ] +] +port_coordinates = [] +position_m = [ 0.04536660760641098, 0.076746277511119843, 0.11284247040748596 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.059999998658895493 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 4 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 3 / single-450' +opening = 1.0 +outlet_diameter_m = 0.059999998658895493 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.75, 0.0, 0.0 ], [ 0.75, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.88149809837341309, -0.054999999701976776, 0.13580000400543213 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.096000000834465027 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 10.0 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 145.0 +exhaust_profile = [ + [ 0.0, 0.0 ], + [ 145.0, 0.0 ], + [ 150.0, 0.00019266399613115937 ], + [ 155.0, 0.00072210998041555285 ], + [ 160.0, 0.0015155200380831957 ], + [ 165.0, 0.002500077011063695 ], + [ 170.0, 0.0036029629409313202 ], + [ 175.0, 0.0047513609752058983 ], + [ 180.0, 0.0058724507689476013 ], + [ 185.0, 0.0068934168666601181 ], + [ 190.0, 0.0077414410188794136 ], + [ 195.0, 0.0083437049761414528 ], + [ 200.0, 0.0086273914203047752 ], + [ 201.25, 0.0086400005966424942 ], + [ 313.75, 0.0086400005966424942 ], + [ 318.75, 0.0084473369643092155 ], + [ 323.75, 0.0079178912565112114 ], + [ 328.75, 0.0071244807913899422 ], + [ 333.75, 0.0061399238184094429 ], + [ 338.75, 0.0050370381213724613 ], + [ 343.75, 0.0038886400870978832 ], + [ 348.75, 0.0027675500605255365 ], + [ 353.75, 0.0017465839628130198 ], + [ 358.75, 0.00089855998521670699 ], + [ 363.75, 0.00029629599885083735 ], + [ 368.75, 1.261000033991877e-05 ], + [ 370.0, 0.0 ], + [ 720.0, 0.0 ] +] +exhaust_valve_count = 1 +exhaust_valve_m = 0.030719999223947525 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 5 +injection_enabled = true +intake_close_deg = 565.0 +intake_open_deg = 355.0 +intake_profile = [ + [ 0.0, 0.0 ], + [ 355.0, 0.0 ], + [ 360.0, 0.00024463899899274111 ], + [ 365.0, 0.00091221299953758717 ], + [ 370.0, 0.001903207041323185 ], + [ 375.0, 0.0031181080266833305 ], + [ 380.0, 0.0044574020430445671 ], + [ 385.0, 0.0058215740136802197 ], + [ 390.0, 0.0071111111901700497 ], + [ 395.0, 0.0082264980301260948 ], + [ 400.0, 0.0090682217851281166 ], + [ 405.0, 0.00953676737844944 ], + [ 407.5, 0.0096000004559755325 ], + [ 512.5, 0.0096000004559755325 ], + [ 517.5, 0.0093553606420755386 ], + [ 522.5, 0.0086877867579460144 ], + [ 527.5, 0.0076967929489910603 ], + [ 532.5, 0.0064818919636309147 ], + [ 537.5, 0.0051425979472696781 ], + [ 542.5, 0.0037784259766340256 ], + [ 547.5, 0.0024888890329748392 ], + [ 552.5, 0.0013735019601881504 ], + [ 557.5, 0.00053177797235548496 ], + [ 562.5, 6.3232997490558773e-05 ], + [ 565.0, 0.0 ], + [ 720.0, 0.0 ] +] +intake_valve_count = 1 +intake_valve_m = 0.036479998379945755 +junction_method = 0 +kind = 'cylinder' +maximum_lift_m = 0.0096000004559755325 +misfire_probability = 0.0 +name = 'Cylinder 1' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ 0.0, 0.38387095928192139, -0.40000000596046448 ], [ 0.0, 0.38387095928192139, 0.5 ] ] +position_m = [ 0.0, 0.11504445225000381, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.11159999668598175 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.061999998986721039 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 75.0 +valve_spring_n_m = 38000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 6 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'intake_plenum' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ 0.0, 0.0, 0.5 ], [ -0.49999997019767761, 0.0, 0.0 ] ] +position_m = [ 0.0, 0.14148889482021332, -0.37479999661445618 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11400000005960464, 0.050763498991727829, 0.17280000448226929 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.69999998807907104 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 7 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'carburetor' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Carburetor' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.12700000405311584, 0.14148889482021332, -0.37479999661445618 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, -0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.075000002980232239, 0.075000002980232239 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.carburetor] + bowl_capacity = 0.10000000149011612 + bowl_head_pressure = 0.0 + bowl_refill_rate = 0.10000000149011612 + film_fraction = 0.2800000011920929 + film_time_constant = 0.10999999940395355 + fuel_density = 740.0 + main_discharge_coefficient = 0.72000002861022949 + main_jet_diameter = 0.0020918289665132761 + needle_jet_diameter = 0.0021528399083763361 + needle_low_scale = 0.9440000057220459 + needle_mid_scale = 0.56400001049041748 + needle_off_idle_scale = 0.99500000476837158 + needle_quarter_scale = 0.83499997854232788 + needle_tip_scale = 0.97000002861022949 + needle_transition_scale = 1.6000000238418579 + needle_upper_scale = 0.64999997615814209 + overrun_exit_rpm = 3300.0 + overrun_rpm = 4200.0 + pilot_discharge_coefficient = 0.62000000476837158 + pilot_jet_diameter = 9.3495640612673014e-05 + pilot_tip_scale = 1.0 + pilot_transition_scale = 1.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.075000002980232239 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 8 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Inlet metering stub' +opening = 1.0 +outlet_diameter_m = 0.075000002980232239 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.10700000822544098, 0.14148889482021332, -0.37479999661445618 ], [ -0.056999996304512024, 0.14148889482021332, -0.37479999661445618 ] ] +port_coordinates = [] +position_m = [ 0.0, 0.0, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 9 +injection_enabled = false +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'ecu' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Ignition / idle ECU' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.0, 0.39148890972137451, -0.37479999661445618 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.10000000149011612, 0.02500000037252903, 0.075000002980232239 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.ecu] + fixed_idle = true + idle_base_opening = 0.045000001788139343 + idle_ki = 3.9999998989515007e-05 + idle_kp = 9.9999997473787516e-05 + idle_max_opening = 0.25 + idle_minimum_advance = -5.0 + idle_rpm = 1400.0 + idle_spark_gain = 0.019999999552965164 + ignition_map = [ [ 0.0, 10.0 ], [ 15000.0, 40.0 ] ] + overrun_cut_rpm = 3500.0 + overrun_fuel_cut_enabled = false + overrun_ignition_cut_enabled = true + overrun_resume_rpm = 2400.0 + power_afr = 12.800000190734863 + rev_limit_cut_time = 0.0 + rev_limit_enabled = true + rev_limit_fuel_cut_enabled = false + rev_limit_ignition_cut_enabled = true + rev_limit_rpm = 13000.0 + rev_resume_rpm = 12990.0 + stoichiometric_afr = 14.699999809265137 + throttle_closing_rate = 0.0 + throttle_closing_time_constant = 0.0 + throttle_rate = 1000.0 + throttle_time_constant = 0.0010000000474974513 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 10 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Intake atmosphere' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.25699999928474426, 0.14148889482021332, -0.37479999661445618 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 11 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'exhaust_collector' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ -0.25446656346321106, 0.49999922513961792, -0.22342187166213989 ], [ 0.50000077486038208, 0.0, 0.0 ] ] +position_m = [ 0.10000000149011612, -0.054999999701976776, 0.13580000400543213 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.062996096909046173, 0.062996096909046173, 0.062996096909046173 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 700.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 12 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Exhaust atmosphere' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 1.6314980983734131, -0.054999999701976776, 0.13580000400543213 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 diff --git a/machines/single-cylinder.machine.toml b/machines/single-cylinder.machine.toml new file mode 100644 index 0000000..3ab44d3 --- /dev/null +++ b/machines/single-cylinder.machine.toml @@ -0,0 +1,560 @@ +cells_per_meter = 40.0 +drive_rpm = 1800.0 +driven = true +format = 1 +load_torque = 0.0 +name = 'Single cylinder engine bench' +scene = 2 +shaft_inertia = 0.15000000596046448 +solver = 'rusanov' + +[[connections]] +a_part = 7 +a_port = 0 +b_part = 1 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 7 +a_port = 1 +b_part = 2 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 8 +a_port = 0 +b_part = 2 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 8 +a_port = 1 +b_part = 3 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 9 +a_port = 0 +b_part = 3 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 9 +a_port = 1 +b_part = 4 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 10 +a_port = 0 +b_part = 4 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 10 +a_port = 1 +b_part = 5 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 11 +a_port = 0 +b_part = 5 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 11 +a_port = 1 +b_part = 6 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[parts]] +afr = 14.699999809265137 +bore_m = 0.075000002980232239 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +heat_transfer = 100.0 +id = 1 +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +name = 'Pressure / atmosphere 1' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +position_m = [ -0.64999997615814209, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 + +[[parts]] +afr = 14.699999809265137 +bore_m = 0.075000002980232239 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +heat_transfer = 100.0 +id = 2 +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'throttle' +maximum_lift_m = 0.0080000003799796104 +name = 'Throttle valve 2' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +position_m = [ -0.40000000596046448, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.05000000074505806, 0.05000000074505806 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 + +[[parts]] +afr = 14.699999809265137 +bore_m = 0.075000002980232239 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +heat_transfer = 100.0 +id = 3 +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +name = 'Chamber / tank 3' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +position_m = [ -0.20000000298023224, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.10000000149011612, 0.079999998211860657, 0.079999998211860657 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 + +[[parts]] +afr = 14.699999809265137 +bore_m = 0.075000002980232239 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +heat_transfer = 100.0 +id = 4 +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'cylinder' +maximum_lift_m = 0.0080000003799796104 +name = 'Engine cylinder 4' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +position_m = [ 0.0, 0.20000000298023224, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 + +[[parts]] +afr = 14.699999809265137 +bore_m = 0.075000002980232239 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +heat_transfer = 100.0 +id = 5 +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +name = 'Chamber / tank 5' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +position_m = [ 0.20000000298023224, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.079999998211860657, 0.079999998211860657, 0.079999998211860657 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 + +[[parts]] +afr = 14.699999809265137 +bore_m = 0.075000002980232239 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +heat_transfer = 100.0 +id = 6 +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +name = 'Pressure / atmosphere 6' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +position_m = [ 0.64999997615814209, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 + +[[parts]] +afr = 14.699999809265137 +bore_m = 0.075000002980232239 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.032000001519918442 +discharge = 0.80000001192092896 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +heat_transfer = 100.0 +id = 7 +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +name = 'Pipe / reducer 7' +opening = 1.0 +outlet_diameter_m = 0.032000001519918442 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.11500000953674316, 0.0, 0.0 ], [ 0.11499994993209839, 0.0, 0.0 ] ] +position_m = [ -0.53499996662139893, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 + +[[parts]] +afr = 14.699999809265137 +bore_m = 0.075000002980232239 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.032000001519918442 +discharge = 0.80000001192092896 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +heat_transfer = 100.0 +id = 8 +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +name = 'Pipe / reducer 8' +opening = 1.0 +outlet_diameter_m = 0.032000001519918442 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.064999997615814209, 0.0, 0.0 ], [ 0.064999997615814209, 0.0, 0.0 ] ] +position_m = [ -0.31499999761581421, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 + +[[parts]] +afr = 14.699999809265137 +bore_m = 0.075000002980232239 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.032000001519918442 +discharge = 0.80000001192092896 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +heat_transfer = 100.0 +id = 9 +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +name = 'Pipe / reducer 9' +opening = 1.0 +outlet_diameter_m = 0.032000001519918442 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.066375002264976501, 0.031380951404571533, 0.0 ], [ 0.066375002264976501, -0.031380951404571533, 0.0 ] ] +position_m = [ -0.083625003695487976, 0.26861906051635742, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 + +[[parts]] +afr = 14.699999809265137 +bore_m = 0.075000002980232239 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.032000001519918442 +discharge = 0.80000001192092896 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +heat_transfer = 100.0 +id = 10 +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +name = 'Pipe / reducer 10' +opening = 1.0 +outlet_diameter_m = 0.032000001519918442 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.071374997496604919, -0.031380951404571533, 0.0 ], [ 0.071374997496604919, 0.031380951404571533, 0.0 ] ] +position_m = [ 0.088624998927116394, 0.26861906051635742, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 + +[[parts]] +afr = 14.699999809265137 +bore_m = 0.075000002980232239 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.032000001519918442 +discharge = 0.80000001192092896 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +heat_transfer = 100.0 +id = 11 +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +name = 'Pipe / reducer 11' +opening = 1.0 +outlet_diameter_m = 0.032000001519918442 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.20499998331069946, 0.0, 0.0 ], [ 0.20499998331069946, 0.0, 0.0 ] ] +position_m = [ 0.44499999284744263, 0.30000001192092896, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 diff --git a/machines/twin-imported.machine.toml b/machines/twin-imported.machine.toml new file mode 100644 index 0000000..e300a63 --- /dev/null +++ b/machines/twin-imported.machine.toml @@ -0,0 +1,1465 @@ +cells_per_meter = 50.0 +drive_rpm = 2000.0 +driven = false +format = 2 +friction_constant = 5.0 +friction_quadratic = 0.0 +friction_viscous = 0.0030000000260770321 +load_torque = 0.0 +name = '600 cc parallel twin / editable' +scene = 2 +shaft_inertia = 0.039999999105930328 +solver = 'rusanov' +starter_torque = 18.0 + +[[connections]] +a_part = 2 +a_port = 0 +b_part = 9 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 2 +a_port = 1 +b_part = 7 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 4 +a_port = 0 +b_part = 7 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 3 +a_port = 0 +b_part = 9 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 3 +a_port = 1 +b_part = 8 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 5 +a_port = 0 +b_part = 8 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 1 +a_port = 1 +b_part = 10 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 11 +a_port = 0 +b_part = 10 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 11 +a_port = 1 +b_part = 9 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 12 +a_port = 0 +b_part = 7 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 12 +a_port = 0 +b_part = 8 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 12 +a_port = 0 +b_part = 10 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 1 +a_port = 0 +b_part = 13 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 4 +a_port = 1 +b_part = 14 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 5 +a_port = 1 +b_part = 14 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 6 +a_port = 0 +b_part = 14 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 6 +a_port = 1 +b_part = 15 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.027499999850988388 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 1 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 0 / twin' +opening = 1.0 +outlet_diameter_m = 0.027499999850988388 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.030000001192092896, 0.0, 0.0 ], [ -0.015000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.12299999594688416, 0.14447620511054993, -0.2175000011920929 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.027499999850988388 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 2 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 1 / twin' +opening = 1.0 +outlet_diameter_m = 0.027499999850988388 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ 0.0, 0.0, -0.060000002384185791 ], [ 0.0, 0.0, 0.060000002384185791 ] ] +port_coordinates = [] +position_m = [ -0.046500001102685928, 0.14447620511054993, -0.090000003576278687 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.027499999850988388 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 3 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 2 / twin' +opening = 1.0 +outlet_diameter_m = 0.027499999850988388 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ 0.0, 0.0, -0.060000002384185791 ], [ 0.0, 0.0, 0.060000002384185791 ] ] +port_coordinates = [] +position_m = [ 0.046500001102685928, 0.14447620511054993, -0.090000003576278687 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.028000000864267349 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 4 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 3 / twin' +opening = 1.0 +outlet_diameter_m = 0.028000000864267349 +pedal_control = true +phase_deg = 0.0 +points_m = [ + [ -0.068752430379390717, 0.066082403063774109, -0.06390891969203949 ], + [ -0.06732632964849472, 0.06305280327796936, -0.049996320158243179 ], + [ -0.063295826315879822, 0.058525398373603821, -0.038123220205307007 ], + [ -0.057033229619264603, 0.052598997950553894, -0.028132118284702301 ], + [ -0.048910431563854218, 0.04537229984998703, -0.019865423440933228 ], + [ -0.039299428462982178, 0.036943994462490082, -0.01316562294960022 ], + [ -0.028572328388690948, 0.02741289883852005, -0.0078751221299171448 ], + [ -0.01710112951695919, 0.016877695918083191, -0.0038362219929695129 ], + [ -0.0052579287439584732, 0.0054371953010559082, -0.00089152157306671143 ], + [ 0.0065852701663970947, -0.0068098008632659912, 0.0011165812611579895 ], + [ 0.018056470900774002, -0.019764602184295654, 0.0023457780480384827 ], + [ 0.028783570975065231, -0.033328503370285034, 0.0029534772038459778 ], + [ 0.038394469767808914, -0.047402702271938324, 0.0030973777174949646 ], + [ 0.046517368406057358, -0.061888404190540314, 0.0029350817203521729 ], + [ 0.052779968827962875, -0.076686903834342957, 0.0026239752769470215 ], + [ 0.056810367852449417, -0.091699503362178802, 0.002321779727935791 ], + [ 0.05823657289147377, -0.10682730376720428, 0.0021859779953956604 ] +] +port_coordinates = [] +position_m = [ 0.022252429276704788, 0.078393802046775818, 0.10140892118215561 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.028000000864267349 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 5 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 4 / twin' +opening = 1.0 +outlet_diameter_m = 0.028000000864267349 +pedal_control = true +phase_deg = 0.0 +points_m = [ + [ -0.025599349290132523, 0.071668267250061035, -0.059992104768753052 ], + [ -0.025078549981117249, 0.068801760673522949, -0.048053707927465439 ], + [ -0.023606650531291962, 0.06413036584854126, -0.037646006792783737 ], + [ -0.021319650113582611, 0.057814061641693115, -0.028664007782936096 ], + [ -0.018353249877691269, 0.050012663006782532, -0.021002806723117828 ], + [ -0.014843448996543884, 0.040886163711547852, -0.014557406306266785 ], + [ -0.010926049202680588, 0.03059425950050354, -0.0092230066657066345 ], + [ -0.00673694908618927, 0.019296959042549133, -0.0048946067690849304 ], + [ -0.0024119541049003601, 0.0071541592478752136, -0.0014673098921775818 ], + [ 0.0019130483269691467, -0.0056743398308753967, 0.0011637955904006958 ], + [ 0.0061021521687507629, -0.019028536975383759, 0.003103695809841156 ], + [ 0.010019548237323761, -0.032748639583587646, 0.0044572949409484863 ], + [ 0.013529449701309204, -0.046674840152263641, 0.0053294971585273743 ], + [ 0.016495749354362488, -0.060647036880254745, 0.0058251917362213135 ], + [ 0.018782749772071838, -0.074505440890789032, 0.0060493946075439453 ], + [ 0.020254649221897125, -0.088090240955352783, 0.0061069950461387634 ], + [ 0.020775452256202698, -0.10124143958091736, 0.0061027929186820984 ] +] +port_coordinates = [] +position_m = [ 0.072099350392818451, 0.072807937860488892, 0.097492106258869171 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.028000000864267349 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 6 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 5 / twin' +opening = 1.0 +outlet_diameter_m = 0.028000000864267349 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.19999989867210388, 0.0, 0.0 ], [ 0.19999992847442627, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.32656639814376831, -0.054999999701976776, 0.11375000327825546 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 390.0 +exhaust_open_deg = 130.0 +exhaust_profile = [ + [ 0.0, 0.0 ], + [ 130.0, 0.0 ], + [ 135.0, 0.0001263089943677187 ], + [ 140.0, 0.00047792401164770126 ], + [ 145.0, 0.0010138830402866006 ], + [ 150.0, 0.0016932180151343346 ], + [ 155.0, 0.0024749659933149815 ], + [ 160.0, 0.0033181610051542521 ], + [ 165.0, 0.0041818390600383282 ], + [ 170.0, 0.0050250338390469551 ], + [ 175.0, 0.005806781817227602 ], + [ 180.0, 0.0064861169084906578 ], + [ 185.0, 0.0070220758207142353 ], + [ 190.0, 0.0073736910708248615 ], + [ 195.0, 0.0074999998323619366 ], + [ 325.0, 0.0074999998323619366 ], + [ 330.0, 0.0073736910708248615 ], + [ 335.0, 0.0070220758207142353 ], + [ 340.0, 0.0064861169084906578 ], + [ 345.0, 0.005806781817227602 ], + [ 350.0, 0.0050250338390469551 ], + [ 355.0, 0.0041818390600383282 ], + [ 360.0, 0.0033181610051542521 ], + [ 365.0, 0.0024749659933149815 ], + [ 370.0, 0.0016932180151343346 ], + [ 375.0, 0.0010138830402866006 ], + [ 380.0, 0.00047792401164770126 ], + [ 385.0, 0.0001263089943677187 ], + [ 390.0, 0.0 ], + [ 720.0, 0.0 ] +] +exhaust_valve_count = 1 +exhaust_valve_m = 0.026000000536441803 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 7 +injection_enabled = true +intake_close_deg = 590.0 +intake_open_deg = 340.0 +intake_profile = [ + [ 0.0, 0.0 ], + [ 340.0, 0.0 ], + [ 345.0, 0.00014540800475515425 ], + [ 350.0, 0.00054886401630938053 ], + [ 355.0, 0.0011612159432843328 ], + [ 360.0, 0.0019333120435476303 ], + [ 365.0, 0.0028160000219941139 ], + [ 370.0, 0.0037601280491799116 ], + [ 375.0, 0.0047165439464151859 ], + [ 380.0, 0.0056360960006713867 ], + [ 385.0, 0.0064696320332586765 ], + [ 390.0, 0.0071680000983178616 ], + [ 395.0, 0.0076820477843284607 ], + [ 400.0, 0.0079626236110925674 ], + [ 402.5, 0.0080000003799796104 ], + [ 527.5, 0.0080000003799796104 ], + [ 532.5, 0.0078545920550823212 ], + [ 537.5, 0.0074511361308395863 ], + [ 542.5, 0.0068387840874493122 ], + [ 547.5, 0.0060666878707706928 ], + [ 552.5, 0.0051839998923242092 ], + [ 557.5, 0.0042398720979690552 ], + [ 562.5, 0.0032834559679031372 ], + [ 567.5, 0.0023639039136469364 ], + [ 572.5, 0.0015303679974749684 ], + [ 577.5, 0.00083199999062344432 ], + [ 582.5, 0.00031795201357454062 ], + [ 587.5, 3.7376001273514703e-05 ], + [ 590.0, 0.0 ], + [ 720.0, 0.0 ] +] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'cylinder' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Cylinder 1' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ 0.0, 0.39258331060409546, -0.39999997615814209 ], [ 0.0, 0.39258331060409546, 0.5 ] ] +position_m = [ -0.046500001102685928, 0.11523809283971786, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 75.0 +valve_spring_n_m = 38000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 390.0 +exhaust_open_deg = 130.0 +exhaust_profile = [ + [ 0.0, 0.0 ], + [ 130.0, 0.0 ], + [ 135.0, 0.0001263089943677187 ], + [ 140.0, 0.00047792401164770126 ], + [ 145.0, 0.0010138830402866006 ], + [ 150.0, 0.0016932180151343346 ], + [ 155.0, 0.0024749659933149815 ], + [ 160.0, 0.0033181610051542521 ], + [ 165.0, 0.0041818390600383282 ], + [ 170.0, 0.0050250338390469551 ], + [ 175.0, 0.005806781817227602 ], + [ 180.0, 0.0064861169084906578 ], + [ 185.0, 0.0070220758207142353 ], + [ 190.0, 0.0073736910708248615 ], + [ 195.0, 0.0074999998323619366 ], + [ 325.0, 0.0074999998323619366 ], + [ 330.0, 0.0073736910708248615 ], + [ 335.0, 0.0070220758207142353 ], + [ 340.0, 0.0064861169084906578 ], + [ 345.0, 0.005806781817227602 ], + [ 350.0, 0.0050250338390469551 ], + [ 355.0, 0.0041818390600383282 ], + [ 360.0, 0.0033181610051542521 ], + [ 365.0, 0.0024749659933149815 ], + [ 370.0, 0.0016932180151343346 ], + [ 375.0, 0.0010138830402866006 ], + [ 380.0, 0.00047792401164770126 ], + [ 385.0, 0.0001263089943677187 ], + [ 390.0, 0.0 ], + [ 720.0, 0.0 ] +] +exhaust_valve_count = 1 +exhaust_valve_m = 0.026000000536441803 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 8 +injection_enabled = true +intake_close_deg = 590.0 +intake_open_deg = 340.0 +intake_profile = [ + [ 0.0, 0.0 ], + [ 340.0, 0.0 ], + [ 345.0, 0.00014540800475515425 ], + [ 350.0, 0.00054886401630938053 ], + [ 355.0, 0.0011612159432843328 ], + [ 360.0, 0.0019333120435476303 ], + [ 365.0, 0.0028160000219941139 ], + [ 370.0, 0.0037601280491799116 ], + [ 375.0, 0.0047165439464151859 ], + [ 380.0, 0.0056360960006713867 ], + [ 385.0, 0.0064696320332586765 ], + [ 390.0, 0.0071680000983178616 ], + [ 395.0, 0.0076820477843284607 ], + [ 400.0, 0.0079626236110925674 ], + [ 402.5, 0.0080000003799796104 ], + [ 527.5, 0.0080000003799796104 ], + [ 532.5, 0.0078545920550823212 ], + [ 537.5, 0.0074511361308395863 ], + [ 542.5, 0.0068387840874493122 ], + [ 547.5, 0.0060666878707706928 ], + [ 552.5, 0.0051839998923242092 ], + [ 557.5, 0.0042398720979690552 ], + [ 562.5, 0.0032834559679031372 ], + [ 567.5, 0.0023639039136469364 ], + [ 572.5, 0.0015303679974749684 ], + [ 577.5, 0.00083199999062344432 ], + [ 582.5, 0.00031795201357454062 ], + [ 587.5, 3.7376001273514703e-05 ], + [ 590.0, 0.0 ], + [ 720.0, 0.0 ] +] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'cylinder' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Cylinder 2' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 180.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ 0.0, 0.39258331060409546, -0.39999997615814209 ], [ 0.0, 0.39258331060409546, 0.5 ] ] +position_m = [ 0.046500001102685928, 0.11523809283971786, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 75.0 +valve_spring_n_m = 38000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 9 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'intake_plenum' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ -0.25, 0.0, 0.49999994039535522 ], [ 0.25, 0.0, 0.49999994039535522 ], [ -0.5, 0.0, 0.0 ] ] +position_m = [ 0.0, 0.14447620511054993, -0.2175000011920929 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.18600000441074371, 0.039824798703193665, 0.13500000536441803 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.026000000536441803 +discharge = 0.69999998807907104 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 10 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'throttle' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Electronic throttle' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.12299999594688416, 0.14447620511054993, -0.2175000011920929 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, -0.0, 0.0 ] +size_m = [ 0.029999997466802597, 0.027499999850988388, 0.027499999850988388 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.027499999850988388 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 11 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Inlet metering stub' +opening = 1.0 +outlet_diameter_m = 0.027499999850988388 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.10799999535083771, 0.14447620511054993, -0.2175000011920929 ], [ -0.093000002205371857, 0.14447620511054993, -0.2175000011920929 ] ] +port_coordinates = [] +position_m = [ 0.0, 0.0, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 12 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'ecu' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Injection / ignition ECU' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.0, 0.39447620511054993, -0.2175000011920929 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.10000000149011612, 0.02500000037252903, 0.075000002980232239 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.ecu] + fixed_idle = false + idle_base_opening = 0.079999998211860657 + idle_ki = 3.9999998989515007e-05 + idle_kp = 9.9999997473787516e-05 + idle_max_opening = 0.25 + idle_minimum_advance = -5.0 + idle_rpm = 1800.0 + idle_spark_gain = 0.0 + ignition_map = [ [ 0.0, 10.0 ], [ 15000.0, 40.0 ] ] + overrun_cut_rpm = 3500.0 + overrun_fuel_cut_enabled = true + overrun_ignition_cut_enabled = false + overrun_resume_rpm = 2400.0 + power_afr = 12.800000190734863 + rev_limit_cut_time = 0.0 + rev_limit_enabled = true + rev_limit_fuel_cut_enabled = true + rev_limit_ignition_cut_enabled = true + rev_limit_rpm = 10000.0 + rev_resume_rpm = 9750.0 + stoichiometric_afr = 14.699999809265137 + throttle_closing_rate = 0.0 + throttle_closing_time_constant = 0.0 + throttle_rate = 1000.0 + throttle_time_constant = 0.0010000000474974513 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 13 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Intake atmosphere' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.15299999713897705, 0.14447620511054993, -0.2175000011920929 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 14 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'exhaust_collector' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ -0.36721128225326538, 0.50000089406967163, -0.19112648069858551 ], [ -0.13410145044326782, 0.50000089406967163, -0.19112648069858551 ], [ 0.50000089406967163, 0.0, 0.0 ] ] +position_m = [ 0.10000000149011612, -0.054999999701976776, 0.11375000327825546 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.053132899105548859, 0.053132899105548859, 0.053132899105548859 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 700.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 15 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Exhaust atmosphere' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.52656632661819458, -0.054999999701976776, 0.11375000327825546 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 diff --git a/machines/v8-h-imported.machine.toml b/machines/v8-h-imported.machine.toml new file mode 100644 index 0000000..14f00f8 --- /dev/null +++ b/machines/v8-h-imported.machine.toml @@ -0,0 +1,4957 @@ +cells_per_meter = 20.0 +drive_rpm = 1000.0 +driven = false +format = 2 +friction_constant = 22.0 +friction_quadratic = 0.0 +friction_viscous = 0.02500000037252903 +load_torque = 0.0 +name = 'American 5.0 / 4-2-1 + H / editable' +scene = 2 +shaft_inertia = 1.1000000238418579 +solver = 'rusanov' +starter_torque = 180.0 + +[[connections]] +a_part = 2 +a_port = 0 +b_part = 35 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 2 +a_port = 1 +b_part = 27 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 10 +a_port = 0 +b_part = 27 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 3 +a_port = 0 +b_part = 35 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 3 +a_port = 1 +b_part = 28 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 11 +a_port = 0 +b_part = 28 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 4 +a_port = 0 +b_part = 35 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 4 +a_port = 1 +b_part = 29 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 12 +a_port = 0 +b_part = 29 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 5 +a_port = 0 +b_part = 35 +b_port = 3 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 5 +a_port = 1 +b_part = 30 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 13 +a_port = 0 +b_part = 30 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 6 +a_port = 0 +b_part = 35 +b_port = 4 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 6 +a_port = 1 +b_part = 31 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 14 +a_port = 0 +b_part = 31 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 7 +a_port = 0 +b_part = 35 +b_port = 5 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 7 +a_port = 1 +b_part = 32 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 15 +a_port = 0 +b_part = 32 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 8 +a_port = 0 +b_part = 35 +b_port = 6 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 8 +a_port = 1 +b_part = 33 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 16 +a_port = 0 +b_part = 33 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 9 +a_port = 0 +b_part = 35 +b_port = 7 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 9 +a_port = 1 +b_part = 34 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 17 +a_port = 0 +b_part = 34 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 1 +a_port = 1 +b_part = 36 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 37 +a_port = 0 +b_part = 36 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 37 +a_port = 1 +b_part = 35 +b_port = 8 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 38 +a_port = 0 +b_part = 27 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 38 +a_port = 0 +b_part = 28 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 38 +a_port = 0 +b_part = 29 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 38 +a_port = 0 +b_part = 30 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 38 +a_port = 0 +b_part = 31 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 38 +a_port = 0 +b_part = 32 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 38 +a_port = 0 +b_part = 33 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 38 +a_port = 0 +b_part = 34 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 38 +a_port = 0 +b_part = 36 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 4 + +[[connections]] +a_part = 1 +a_port = 0 +b_part = 39 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 10 +a_port = 1 +b_part = 40 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 11 +a_port = 1 +b_part = 40 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 18 +a_port = 0 +b_part = 40 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 12 +a_port = 1 +b_part = 41 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 13 +a_port = 1 +b_part = 41 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 19 +a_port = 0 +b_part = 41 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 14 +a_port = 1 +b_part = 42 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 16 +a_port = 1 +b_part = 42 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 20 +a_port = 0 +b_part = 42 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 15 +a_port = 1 +b_part = 43 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 17 +a_port = 1 +b_part = 43 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 21 +a_port = 0 +b_part = 43 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 18 +a_port = 1 +b_part = 44 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 19 +a_port = 1 +b_part = 44 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 22 +a_port = 0 +b_part = 44 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 20 +a_port = 1 +b_part = 45 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 21 +a_port = 1 +b_part = 45 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 23 +a_port = 0 +b_part = 45 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 22 +a_port = 1 +b_part = 46 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 24 +a_port = 0 +b_part = 46 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 26 +a_port = 0 +b_part = 46 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 23 +a_port = 1 +b_part = 47 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 25 +a_port = 0 +b_part = 47 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 26 +a_port = 1 +b_part = 47 +b_port = 2 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 24 +a_port = 1 +b_part = 48 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 25 +a_port = 1 +b_part = 49 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.070000000298023224 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 1 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 0 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.070000000298023224 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.090000003576278687, 0.0, 0.0 ], [ 0.0, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.32919999957084656, 0.31264111399650574, 0.0 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.037999998778104782 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 2 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 1 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.037999998778104782 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ 0.0, 0.060000002384185791, 0.0 ], [ 0.0, -0.059999987483024597, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.1793999969959259, 0.23076969385147095, -0.044772200286388397 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.037999998778104782 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 3 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 2 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.037999998778104782 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ 0.0, 0.060000002384185791, 0.0 ], [ 0.0, -0.059999987483024597, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.059799998998641968, 0.23076969385147095, -0.044772200286388397 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.037999998778104782 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 4 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 3 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.037999998778104782 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ 0.0, 0.060000002384185791, 0.0 ], [ 0.0, -0.059999987483024597, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.059799998998641968, 0.23076969385147095, -0.044772200286388397 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.037999998778104782 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 5 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 4 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.037999998778104782 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ 0.0, 0.060000002384185791, 0.0 ], [ 0.0, -0.059999987483024597, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.1793999969959259, 0.23076969385147095, -0.044772200286388397 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.037999998778104782 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 6 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 5 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.037999998778104782 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ 0.0, 0.060000002384185791, 0.0 ], [ 0.0, -0.059999987483024597, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.1793999969959259, 0.23076969385147095, 0.044772200286388397 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.037999998778104782 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 7 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 6 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.037999998778104782 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ 0.0, 0.060000002384185791, 0.0 ], [ 0.0, -0.059999987483024597, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.059799998998641968, 0.23076969385147095, 0.044772200286388397 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.037999998778104782 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 8 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 7 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.037999998778104782 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ 0.0, 0.060000002384185791, 0.0 ], [ 0.0, -0.059999987483024597, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.059799998998641968, 0.23076969385147095, 0.044772200286388397 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.037999998778104782 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 9 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 8 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.037999998778104782 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ 0.0, 0.060000002384185791, 0.0 ], [ 0.0, -0.059999987483024597, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.1793999969959259, 0.23076969385147095, 0.044772200286388397 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.037999998778104782 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 10 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 9 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.037999998778104782 +pedal_control = true +phase_deg = 0.0 +points_m = [ + [ -0.062627054750919342, 0.06172189861536026, 0.053094565868377686 ], + [ -0.061234660446643829, 0.053818292915821075, 0.04284435510635376 ], + [ -0.05729956179857254, 0.046149693429470062, 0.033775553107261658 ], + [ -0.051184959709644318, 0.038633592426776886, 0.025816455483436584 ], + [ -0.043254263699054718, 0.031187690794467926, 0.018895164132118225 ], + [ -0.033870451152324677, 0.023729495704174042, 0.012940064072608948 ], + [ -0.023397050797939301, 0.016176596283912659, 0.0078793615102767944 ], + [ -0.012197159230709076, 0.0084464922547340393, 0.0036412626504898071 ], + [ -0.00063395500183105469, 0.0004567950963973999, 0.00015395879745483398 ], + [ 0.01092924177646637, -0.0078750066459178925, -0.0026542395353317261 ], + [ 0.022129140794277191, -0.016631204634904861, -0.0048552453517913818 ], + [ 0.032602541148662567, -0.025894306600093842, -0.0065207332372665405 ], + [ 0.041986338794231415, -0.035746805369853973, -0.0077224373817443848 ], + [ 0.049917042255401611, -0.046271108090877533, -0.0085321366786956787 ], + [ 0.056031640619039536, -0.057549707591533661, -0.0090216398239135742 ], + [ 0.059966742992401123, -0.069664806127548218, -0.0092627406120300293 ], + [ 0.061359141021966934, -0.082699105143547058, -0.0093271434307098389 ] +] +port_coordinates = [] +position_m = [ -0.11677294224500656, 0.066119305789470673, -0.17860355973243713 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.037999998778104782 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 11 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 10 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.037999998778104782 +pedal_control = true +phase_deg = 0.0 +points_m = [ + [ -0.0090911500155925751, 0.064310669898986816, 0.048663437366485596 ], + [ -0.0088899508118629456, 0.057723067700862885, 0.040413737297058105 ], + [ -0.0083213523030281067, 0.050700768828392029, 0.032829731702804565 ], + [ -0.0074378512799739838, 0.04326716810464859, 0.025893136858940125 ], + [ -0.006291951984167099, 0.035445868968963623, 0.01958543062210083 ], + [ -0.0049360506236553192, 0.027260266244411469, 0.013888135552406311 ], + [ -0.0034227520227432251, 0.018733866512775421, 0.0087830275297164917 ], + [ -0.0018044523894786835, 0.0098901689052581787, 0.004251435399055481 ], + [ -0.00013365224003791809, 0.00075256824493408203, 0.00027513504028320312 ], + [ 0.0015371479094028473, -0.0086554363369941711, -0.0031643658876419067 ], + [ 0.0031554475426673889, -0.018310334533452988, -0.0060855746269226074 ], + [ 0.0046687498688697815, -0.028188634663820267, -0.00850677490234375 ], + [ 0.0060246475040912628, -0.038266934454441071, -0.010446369647979736 ], + [ 0.007170550525188446, -0.048521734774112701, -0.011922970414161682 ], + [ 0.0080540478229522705, -0.058929532766342163, -0.012954875826835632 ], + [ 0.0086226500570774078, -0.069466933608055115, -0.013560473918914795 ], + [ 0.0088238492608070374, -0.080110333859920502, -0.013758271932601929 ] +] +port_coordinates = [] +position_m = [ -0.050708848983049393, 0.063530534505844116, -0.17417243123054504 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.037999998778104782 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 12 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 11 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.037999998778104782 +pedal_control = true +phase_deg = 0.0 +points_m = [ + [ -0.062627062201499939, 0.06172189861536026, 0.053094565868377686 ], + [ -0.061234660446643829, 0.053818292915821075, 0.04284435510635376 ], + [ -0.05729956179857254, 0.046149693429470062, 0.033775553107261658 ], + [ -0.051184959709644318, 0.038633592426776886, 0.025816455483436584 ], + [ -0.043254263699054718, 0.031187690794467926, 0.018895164132118225 ], + [ -0.033870458602905273, 0.023729495704174042, 0.012940064072608948 ], + [ -0.023397058248519897, 0.016176596283912659, 0.0078793615102767944 ], + [ -0.012197159230709076, 0.0084464922547340393, 0.0036412626504898071 ], + [ -0.00063396245241165161, 0.0004567950963973999, 0.00015395879745483398 ], + [ 0.01092924177646637, -0.0078750066459178925, -0.0026542395353317261 ], + [ 0.022129133343696594, -0.016631204634904861, -0.0048552453517913818 ], + [ 0.03260253369808197, -0.025894306600093842, -0.0065207332372665405 ], + [ 0.041986346244812012, -0.035746805369853973, -0.0077224373817443848 ], + [ 0.049917042255401611, -0.046271108090877533, -0.0085321366786956787 ], + [ 0.056031644344329834, -0.057549707591533661, -0.0090216398239135742 ], + [ 0.059966742992401123, -0.069664806127548218, -0.0092627406120300293 ], + [ 0.061359137296676636, -0.082699105143547058, -0.0093271434307098389 ] +] +port_coordinates = [] +position_m = [ 0.12242706120014191, 0.066119305789470673, -0.17860355973243713 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.037999998778104782 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 13 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 12 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.037999998778104782 +pedal_control = true +phase_deg = 0.0 +points_m = [ + [ -0.0090911537408828735, 0.064310669898986816, 0.048663437366485596 ], + [ -0.0088899433612823486, 0.057723067700862885, 0.040413737297058105 ], + [ -0.0083213448524475098, 0.050700768828392029, 0.032829731702804565 ], + [ -0.0074378550052642822, 0.04326716810464859, 0.025893136858940125 ], + [ -0.0062919557094573975, 0.035445868968963623, 0.01958543062210083 ], + [ -0.0049360543489456177, 0.027260266244411469, 0.013888135552406311 ], + [ -0.0034227520227432251, 0.018733866512775421, 0.0087830275297164917 ], + [ -0.0018044561147689819, 0.0098901689052581787, 0.004251435399055481 ], + [ -0.00013364851474761963, 0.00075256824493408203, 0.00027513504028320312 ], + [ 0.0015371441841125488, -0.0086554363369941711, -0.0031643658876419067 ], + [ 0.0031554549932479858, -0.018310334533452988, -0.0060855746269226074 ], + [ 0.0046687424182891846, -0.028188634663820267, -0.00850677490234375 ], + [ 0.0060246437788009644, -0.038266934454441071, -0.010446369647979736 ], + [ 0.0071705430746078491, -0.048521734774112701, -0.011922970414161682 ], + [ 0.0080540478229522705, -0.058929532766342163, -0.012954875826835632 ], + [ 0.0086226463317871094, -0.069466933608055115, -0.013560473918914795 ], + [ 0.0088238567113876343, -0.080110333859920502, -0.013758271932601929 ] +] +port_coordinates = [] +position_m = [ 0.18849115073680878, 0.063530534505844116, -0.17417243123054504 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.037999998778104782 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 14 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 13 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.037999998778104782 +pedal_control = true +phase_deg = 0.0 +points_m = [ + [ -0.060823783278465271, 0.09583534300327301, -0.058749586343765259 ], + [ -0.059747785329818726, 0.085615247488021851, -0.046476885676383972 ], + [ -0.056617885828018188, 0.074570447206497192, -0.035872578620910645 ], + [ -0.051581487059593201, 0.062905743718147278, -0.026815786957740784 ], + [ -0.044785782694816589, 0.050825741142034531, -0.019185572862625122 ], + [ -0.036378279328346252, 0.03853544220328331, -0.012860983610153198 ], + [ -0.026506185531616211, 0.026239443570375443, -0.0077209770679473877 ], + [ -0.015316784381866455, 0.014142643660306931, -0.0036445856094360352 ], + [ -0.0029575899243354797, 0.0024497434496879578, -0.00051097571849822998 ], + [ 0.010424315929412842, -0.0086343567818403244, 0.0018010139465332031 ], + [ 0.024681515991687775, -0.01890505850315094, 0.0034122169017791748 ], + [ 0.039666712284088135, -0.028157457709312439, 0.0044437199831008911 ], + [ 0.055232614278793335, -0.036186758428812027, 0.005016326904296875 ], + [ 0.071231715381145477, -0.04278825968503952, 0.0052511245012283325 ], + [ 0.087516911327838898, -0.047757156193256378, 0.0052691251039505005 ], + [ 0.10394081473350525, -0.050888657569885254, 0.005191117525100708 ], + [ 0.12035601586103439, -0.051977958530187607, 0.0051383227109909058 ] +] +port_coordinates = [] +position_m = [ -0.11857621371746063, 0.032005857676267624, 0.18425858020782471 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.037999998778104782 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 15 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 14 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.037999998778104782 +pedal_control = true +phase_deg = 0.0 +points_m = [ + [ -0.060823783278465271, 0.09583534300327301, -0.058749586343765259 ], + [ -0.059747785329818726, 0.085615232586860657, -0.046476885676383972 ], + [ -0.056617885828018188, 0.074570432305335999, -0.035872578620910645 ], + [ -0.051581479609012604, 0.062905728816986084, -0.026815786957740784 ], + [ -0.044785782694816589, 0.050825733691453934, -0.019185572862625122 ], + [ -0.036378279328346252, 0.038535434752702713, -0.012860983610153198 ], + [ -0.026506181806325912, 0.026239436119794846, -0.0077209770679473877 ], + [ -0.015316782519221306, 0.014142636209726334, -0.0036445856094360352 ], + [ -0.0029575824737548828, 0.0024497359991073608, -0.00051097571849822998 ], + [ 0.010424317792057991, -0.0086343642324209213, 0.0018010139465332031 ], + [ 0.024681517854332924, -0.018905065953731537, 0.0034122169017791748 ], + [ 0.039666719734668732, -0.028157465159893036, 0.0044437199831008911 ], + [ 0.055232517421245575, -0.036186765879392624, 0.005016326904296875 ], + [ 0.071231715381145477, -0.042788267135620117, 0.0052511245012283325 ], + [ 0.087516918778419495, -0.047757163643836975, 0.0052691251039505005 ], + [ 0.10394081473350525, -0.050888665020465851, 0.005191117525100708 ], + [ 0.12035601586103439, -0.051977965980768204, 0.0051383227109909058 ] +] +port_coordinates = [] +position_m = [ 0.001023782417178154, 0.032005865126848221, 0.18425858020782471 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.037999998778104782 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 16 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 15 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.037999998778104782 +pedal_control = true +phase_deg = 0.0 +points_m = [ + [ 0.017814148217439651, 0.064125552773475647, -0.048961341381072998 ], + [ 0.01741974800825119, 0.057448446750640869, -0.040575534105300903 ], + [ 0.016305048018693924, 0.050382047891616821, -0.032890528440475464 ], + [ 0.014573048800230026, 0.042942948639392853, -0.025884330272674561 ], + [ 0.012326549738645554, 0.035147145390510559, -0.019534841179847717 ], + [ 0.009668547660112381, 0.027011051774024963, -0.013820037245750427 ], + [ 0.0067017488181591034, 0.018550850450992584, -0.0087179392576217651 ], + [ 0.0035292506217956543, 0.0097829475998878479, -0.0042063295841217041 ], + [ 0.00025384873151779175, 0.00072345137596130371, -0.00026322901248931885 ], + [ -0.0030215494334697723, -0.0086112506687641144, 0.0031332671642303467 ], + [ -0.0061940513551235199, -0.018205050379037857, 0.0060054659843444824 ], + [ -0.009160749614238739, -0.028041552752256393, 0.0083751678466796875 ], + [ -0.011818850412964821, -0.038104452192783356, 0.010264664888381958 ], + [ -0.014065250754356384, -0.048377551138401031, 0.01169586181640625 ], + [ -0.01579735055565834, -0.058844652026891708, 0.012690767645835876 ], + [ -0.016911949962377548, -0.069489352405071259, 0.01327165961265564 ], + [ -0.017306450754404068, -0.080295450985431671, 0.013460367918014526 ] +] +port_coordinates = [] +position_m = [ 0.041985850781202316, 0.063715651631355286, 0.17447033524513245 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.037999998778104782 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 17 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 16 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.037999998778104782 +pedal_control = true +phase_deg = 0.0 +points_m = [ + [ 0.017814159393310547, 0.064125567674636841, -0.048961356282234192 ], + [ 0.017419755458831787, 0.057448364794254303, -0.040575549006462097 ], + [ 0.016305059194564819, 0.050382062792778015, -0.032890543341636658 ], + [ 0.014573067426681519, 0.042942963540554047, -0.025884345173835754 ], + [ 0.012326568365097046, 0.035147160291671753, -0.019534856081008911 ], + [ 0.0096685588359832764, 0.027011066675186157, -0.013820052146911621 ], + [ 0.0067017674446105957, 0.018550865352153778, -0.008717954158782959 ], + [ 0.0035292655229568481, 0.0097829625010490417, -0.0042063444852828979 ], + [ 0.00025385618209838867, 0.00072346627712249756, -0.0002632439136505127 ], + [ -0.003021538257598877, -0.0086112357676029205, 0.0031332522630691528 ], + [ -0.0061940401792526245, -0.018205035477876663, 0.0060054510831832886 ], + [ -0.0091607421636581421, -0.0280415378510952, 0.0083751529455184937 ], + [ -0.011818841099739075, -0.038104437291622162, 0.010264649987220764 ], + [ -0.014065340161323547, -0.048377536237239838, 0.011695846915245056 ], + [ -0.015797331929206848, -0.058844637125730515, 0.012690752744674683 ], + [ -0.016911938786506653, -0.069489337503910065, 0.013271644711494446 ], + [ -0.017306432127952576, -0.080295436084270477, 0.013460353016853333 ] +] +port_coordinates = [] +position_m = [ 0.16158583760261536, 0.063715636730194092, 0.17447035014629364 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.045000001788139343 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 18 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 17 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.045000001788139343 +pedal_control = true +phase_deg = 0.0 +points_m = [ + [ -0.17097978293895721, 0.020311798900365829, 0.0 ], + [ -0.14807169139385223, 0.019855495542287827, 0.0 ], + [ -0.12577769160270691, 0.018566198647022247, 0.0 ], + [ -0.10401038825511932, 0.016562797129154205, 0.0 ], + [ -0.082681789994239807, 0.013964299112558365, 0.0 ], + [ -0.061704188585281372, 0.010889798402786255, 0.0 ], + [ -0.040989890694618225, 0.0074581988155841827, 0.0 ], + [ -0.020451083779335022, 0.0037885978817939758, 0.0 ], + [ 1.4901161193847656e-08, -3.7252902984619141e-09, 0.0 ], + [ 0.020451009273529053, -0.0037886016070842743, 0.0 ], + [ 0.040989816188812256, -0.0074582025408744812, 0.0 ], + [ 0.06170421838760376, -0.010889805853366852, 0.0 ], + [ 0.082681804895401001, -0.013964302837848663, 0.0 ], + [ 0.10401041805744171, -0.016562804579734802, 0.0 ], + [ 0.1257777065038681, -0.018566206097602844, 0.0 ], + [ 0.14807160198688507, -0.019855499267578125, 0.0 ], + [ 0.1709798127412796, -0.020311802625656128, 0.0 ] +] +port_coordinates = [] +position_m = [ 0.14979998767375946, -0.057499997317790985, -0.19589219987392426 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.045000001788139343 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 19 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 18 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.045000001788139343 +pedal_control = true +phase_deg = 0.0 +points_m = [ + [ -0.051379799842834473, 0.016562297940254211, 0.0 ], + [ -0.044247597455978394, 0.016190297901630402, 0.0 ], + [ -0.037399202585220337, 0.015138998627662659, 0.0 ], + [ -0.030794203281402588, 0.013505395501852036, 0.0 ], + [ -0.024391993880271912, 0.011386595666408539, 0.0 ], + [ -0.018152087926864624, 0.0088795982301235199, 0.0 ], + [ -0.012033790349960327, 0.0060814991593360901, 0.0 ], + [ -0.0059966146945953369, 0.0030892975628376007, 0.0 ], + [ 0.0, -3.7252902984619141e-09, 0.0 ], + [ 0.0059965848922729492, -0.0030892044305801392, 0.0 ], + [ 0.012033790349960327, -0.0060814991593360901, 0.0 ], + [ 0.018151998519897461, -0.0088796019554138184, 0.0 ], + [ 0.024392008781433105, -0.011386603116989136, 0.0 ], + [ 0.030794203281402588, -0.013505399227142334, 0.0 ], + [ 0.037399202585220337, -0.015138901770114899, 0.0 ], + [ 0.044247597455978394, -0.016190305352210999, 0.0 ], + [ 0.051379799842834473, -0.016562305390834808, 0.0 ] +] +port_coordinates = [] +position_m = [ 0.26940000057220459, -0.057499997317790985, -0.19589219987392426 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.045000001788139343 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 20 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 19 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.045000001788139343 +pedal_control = true +phase_deg = 0.0 +points_m = [ + [ -0.14107978343963623, 0.019901499152183533, 0.0 ], + [ -0.12213288247585297, 0.019454497843980789, 0.0 ], + [ -0.10371068865060806, 0.018191296607255936, 0.0 ], + [ -0.085738390684127808, 0.016228295862674713, 0.0 ], + [ -0.068140886723995209, 0.013682298362255096, 0.0 ], + [ -0.050843283534049988, 0.010669898241758347, 0.0 ], + [ -0.033770591020584106, 0.0073075965046882629, 0.0 ], + [ -0.016847789287567139, 0.0037120990455150604, 0.0 ], + [ 1.4901161193847656e-08, -3.7252902984619141e-09, 0.0 ], + [ 0.016847819089889526, -0.0037121027708053589, 0.0 ], + [ 0.033770516514778137, -0.0073076039552688599, 0.0 ], + [ 0.050843313336372375, -0.010669901967048645, 0.0 ], + [ 0.068140909075737, -0.013682305812835693, 0.0 ], + [ 0.085738301277160645, -0.01622830331325531, 0.0 ], + [ 0.10371071100234985, -0.018191300332546234, 0.0 ], + [ 0.12213292717933655, -0.019454501569271088, 0.0 ], + [ 0.14107981324195862, -0.019901499152183533, 0.0 ] +] +port_coordinates = [] +position_m = [ 0.17969998717308044, -0.057499997317790985, 0.19589219987392426 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.045000001788139343 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 21 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 20 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.045000001788139343 +pedal_control = true +phase_deg = 0.0 +points_m = [ + [ -0.081279799342155457, 0.0183430016040802, 0.0 ], + [ -0.070226699113845825, 0.017930999398231506, 0.0 ], + [ -0.059530898928642273, 0.01676660031080246, 0.0 ], + [ -0.049141302704811096, 0.014957401901483536, 0.0 ], + [ -0.039006799459457397, 0.012610800564289093, 0.0 ], + [ -0.029076501727104187, 0.0098343007266521454, 0.0 ], + [ -0.019299298524856567, 0.0067353025078773499, 0.0 ], + [ -0.009624093770980835, 0.0034213997423648834, 0.0 ], + [ 0.0, 0.0, 0.0 ], + [ 0.009624093770980835, -0.0034213997423648834, 0.0 ], + [ 0.019299313426017761, -0.0067352987825870514, 0.0 ], + [ 0.029076501727104187, -0.0098343007266521454, 0.0 ], + [ 0.039006784558296204, -0.012610796838998795, 0.0 ], + [ 0.049141213297843933, -0.014957401901483536, 0.0 ], + [ 0.059530898928642273, -0.016766596585512161, 0.0 ], + [ 0.070226684212684631, -0.017930995672941208, 0.0 ], + [ 0.081279799342155457, -0.018342997878789902, 0.0 ] +] +port_coordinates = [] +position_m = [ 0.23950000107288361, -0.057500001043081284, 0.19589219987392426 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.05000000074505806 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 22 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 21 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.05000000074505806 +pedal_control = true +phase_deg = 0.0 +points_m = [ + [ -0.25657984614372253, 0.0, 0.099146053194999695 ], + [ -0.22035494446754456, 0.0, 0.096919149160385132 ], + [ -0.18579113483428955, 0.0, 0.090625748038291931 ], + [ -0.15265092253684998, 0.0, 0.080846652388572693 ], + [ -0.12069705128669739, 0.0, 0.0681629478931427 ], + [ -0.089692354202270508, 0.0, 0.053155452013015747 ], + [ -0.059399425983428955, 0.0, 0.03640514612197876 ], + [ -0.029581010341644287, 0.0, 0.018493056297302246 ], + [ 5.9604644775390625e-08, 0.0, -5.9604644775390625e-08 ], + [ 0.029581069946289062, 0.0, -0.018493145704269409 ], + [ 0.05939948558807373, 0.0, -0.036405235528945923 ], + [ 0.089692234992980957, 0.0, -0.05315554141998291 ], + [ 0.12069708108901978, 0.0, -0.068163037300109863 ], + [ 0.15265089273452759, 0.0, -0.08084675669670105 ], + [ 0.18579113483428955, 0.0, -0.090625852346420288 ], + [ 0.22035497426986694, 0.0, -0.096919357776641846 ], + [ 0.25657975673675537, 0.0, -0.099146246910095215 ] +] +port_coordinates = [] +position_m = [ 0.6141999363899231, -0.079999998211860657, -0.30215615034103394 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.05000000074505806 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 23 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 22 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.05000000074505806 +pedal_control = true +phase_deg = 0.0 +points_m = [ + [ -0.25657984614372253, 0.0, -0.099146053194999695 ], + [ -0.22035494446754456, 0.0, -0.096919149160385132 ], + [ -0.18579113483428955, 0.0, -0.090625748038291931 ], + [ -0.15265092253684998, 0.0, -0.080846652388572693 ], + [ -0.12069705128669739, 0.0, -0.0681629478931427 ], + [ -0.089692354202270508, 0.0, -0.053155452013015747 ], + [ -0.059399425983428955, 0.0, -0.03640514612197876 ], + [ -0.029581010341644287, 0.0, -0.018493056297302246 ], + [ 5.9604644775390625e-08, 0.0, 5.9604644775390625e-08 ], + [ 0.029581069946289062, 0.0, 0.018493145704269409 ], + [ 0.05939948558807373, 0.0, 0.036405235528945923 ], + [ 0.089692234992980957, 0.0, 0.05315554141998291 ], + [ 0.12069708108901978, 0.0, 0.068163037300109863 ], + [ 0.15265089273452759, 0.0, 0.08084675669670105 ], + [ 0.18579113483428955, 0.0, 0.090625852346420288 ], + [ 0.22035497426986694, 0.0, 0.096919357776641846 ], + [ 0.25657975673675537, 0.0, 0.099146246910095215 ] +] +port_coordinates = [] +position_m = [ 0.6141999363899231, -0.079999998211860657, 0.30215615034103394 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.05000000074505806 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 24 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 23 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.05000000074505806 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.62499994039535522, 0.0, 0.0 ], [ 0.625, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 1.5326199531555176, -0.079999998211860657, -0.40842020511627197 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.05000000074505806 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 25 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 24 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.05000000074505806 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.62499994039535522, 0.0, 0.0 ], [ 0.625, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 1.5326199531555176, -0.079999998211860657, 0.40842020511627197 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.037999998778104782 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 26 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Pipe 25 / v8-h' +opening = 1.0 +outlet_diameter_m = 0.037999998778104782 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ 0.0, 0.0, -0.38999998569488525 ], [ 0.0, 0.0, 0.38999998569488525 ] ] +port_coordinates = [] +position_m = [ 0.88919991254806519, -0.079999998211860657, 0.0 ] +pressure_pa = 101324.9921875 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 288.20306396484375 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.10159999877214432 +burn_duration_deg = 65.0 +burn_start_deg = 5.0 +compression = 7.0 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 140.0 +exhaust_profile = [ + [ 0.0, 0.0 ], + [ 140.0, 0.0 ], + [ 145.0, 0.00021369299793150276 ], + [ 150.0, 0.00080217001959681511 ], + [ 155.0, 0.0016865290235728025 ], + [ 160.0, 0.0027878689579665661 ], + [ 165.0, 0.0040272870101034641 ], + [ 170.0, 0.0053258808329701424 ], + [ 175.0, 0.006604751106351614 ], + [ 180.0, 0.0077849919907748699 ], + [ 185.0, 0.008787703700363636 ], + [ 190.0, 0.009533984586596489 ], + [ 195.0, 0.0099449325352907181 ], + [ 197.5, 0.0099999997764825821 ], + [ 312.5, 0.0099999997764825821 ], + [ 317.5, 0.0097863068804144859 ], + [ 322.5, 0.0091978302225470543 ], + [ 327.5, 0.0083134714514017105 ], + [ 332.5, 0.007212130818516016 ], + [ 337.5, 0.0059727132320404053 ], + [ 342.5, 0.0046741189435124397 ], + [ 347.5, 0.0033952489029616117 ], + [ 352.5, 0.0022150080185383558 ], + [ 357.5, 0.0012122959597036242 ], + [ 362.5, 0.00046601498615927994 ], + [ 367.5, 5.5067001085262746e-05 ], + [ 370.0, 0.0 ], + [ 720.0, 0.0 ] +] +exhaust_valve_count = 1 +exhaust_valve_m = 0.037000000476837158 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 27 +injection_enabled = true +intake_close_deg = 570.0 +intake_open_deg = 350.0 +intake_profile = [ + [ 0.0, 0.0 ], + [ 350.0, 0.0 ], + [ 355.0, 0.00023290800163522363 ], + [ 360.0, 0.00087152497144415975 ], + [ 365.0, 0.0018256950424984097 ], + [ 370.0, 0.003005258971825242 ], + [ 375.0, 0.0043200599029660225 ], + [ 380.0, 0.0056799398735165596 ], + [ 385.0, 0.0069947410374879837 ], + [ 390.0, 0.0081743048503994942 ], + [ 395.0, 0.0091284746304154396 ], + [ 400.0, 0.0097670918330550194 ], + [ 405.0, 0.0099999997764825821 ], + [ 515.0, 0.0099999997764825821 ], + [ 520.0, 0.0097670918330550194 ], + [ 525.0, 0.0091284746304154396 ], + [ 530.0, 0.0081743048503994942 ], + [ 535.0, 0.0069947410374879837 ], + [ 540.0, 0.0056799398735165596 ], + [ 545.0, 0.0043200599029660225 ], + [ 550.0, 0.003005258971825242 ], + [ 555.0, 0.0018256950424984097 ], + [ 560.0, 0.00087152497144415975 ], + [ 565.0, 0.00023290800163522363 ], + [ 570.0, 0.0 ], + [ 720.0, 0.0 ] +] +intake_valve_count = 1 +intake_valve_m = 0.045000001788139343 +junction_method = 0 +kind = 'cylinder' +maximum_lift_m = 0.0099999997764825821 +misfire_probability = 0.0 +name = 'Cylinder 1' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ 0.0, 0.4100109338760376, 0.40000021457672119 ], [ 0.0, 0.41001105308532715, -0.5000004768371582 ] ] +position_m = [ -0.1793999969959259, 0.11950695514678955, -0.063542984426021576 ] +pressure_pa = 101325.0 +rod_m = 0.1289999932050705 +rotation_deg = [ -28.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.076200000941753387 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 75.0 +valve_spring_n_m = 38000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.10159999877214432 +burn_duration_deg = 65.0 +burn_start_deg = 5.0 +compression = 7.0 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 140.0 +exhaust_profile = [ + [ 0.0, 0.0 ], + [ 140.0, 0.0 ], + [ 145.0, 0.00021369299793150276 ], + [ 150.0, 0.00080217001959681511 ], + [ 155.0, 0.0016865290235728025 ], + [ 160.0, 0.0027878689579665661 ], + [ 165.0, 0.0040272870101034641 ], + [ 170.0, 0.0053258808329701424 ], + [ 175.0, 0.006604751106351614 ], + [ 180.0, 0.0077849919907748699 ], + [ 185.0, 0.008787703700363636 ], + [ 190.0, 0.009533984586596489 ], + [ 195.0, 0.0099449325352907181 ], + [ 197.5, 0.0099999997764825821 ], + [ 312.5, 0.0099999997764825821 ], + [ 317.5, 0.0097863068804144859 ], + [ 322.5, 0.0091978302225470543 ], + [ 327.5, 0.0083134714514017105 ], + [ 332.5, 0.007212130818516016 ], + [ 337.5, 0.0059727132320404053 ], + [ 342.5, 0.0046741189435124397 ], + [ 347.5, 0.0033952489029616117 ], + [ 352.5, 0.0022150080185383558 ], + [ 357.5, 0.0012122959597036242 ], + [ 362.5, 0.00046601498615927994 ], + [ 367.5, 5.5067001085262746e-05 ], + [ 370.0, 0.0 ], + [ 720.0, 0.0 ] +] +exhaust_valve_count = 1 +exhaust_valve_m = 0.037000000476837158 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 28 +injection_enabled = true +intake_close_deg = 570.0 +intake_open_deg = 350.0 +intake_profile = [ + [ 0.0, 0.0 ], + [ 350.0, 0.0 ], + [ 355.0, 0.00023290800163522363 ], + [ 360.0, 0.00087152497144415975 ], + [ 365.0, 0.0018256950424984097 ], + [ 370.0, 0.003005258971825242 ], + [ 375.0, 0.0043200599029660225 ], + [ 380.0, 0.0056799398735165596 ], + [ 385.0, 0.0069947410374879837 ], + [ 390.0, 0.0081743048503994942 ], + [ 395.0, 0.0091284746304154396 ], + [ 400.0, 0.0097670918330550194 ], + [ 405.0, 0.0099999997764825821 ], + [ 515.0, 0.0099999997764825821 ], + [ 520.0, 0.0097670918330550194 ], + [ 525.0, 0.0091284746304154396 ], + [ 530.0, 0.0081743048503994942 ], + [ 535.0, 0.0069947410374879837 ], + [ 540.0, 0.0056799398735165596 ], + [ 545.0, 0.0043200599029660225 ], + [ 550.0, 0.003005258971825242 ], + [ 555.0, 0.0018256950424984097 ], + [ 560.0, 0.00087152497144415975 ], + [ 565.0, 0.00023290800163522363 ], + [ 570.0, 0.0 ], + [ 720.0, 0.0 ] +] +intake_valve_count = 1 +intake_valve_m = 0.045000001788139343 +junction_method = 0 +kind = 'cylinder' +maximum_lift_m = 0.0099999997764825821 +misfire_probability = 0.0 +name = 'Cylinder 2' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 450.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ 0.0, 0.4100109338760376, 0.40000021457672119 ], [ 0.0, 0.41001105308532715, -0.5000004768371582 ] ] +position_m = [ -0.059799998998641968, 0.11950695514678955, -0.063542984426021576 ] +pressure_pa = 101325.0 +rod_m = 0.1289999932050705 +rotation_deg = [ -28.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.076200000941753387 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 75.0 +valve_spring_n_m = 38000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.10159999877214432 +burn_duration_deg = 65.0 +burn_start_deg = 5.0 +compression = 7.0 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 140.0 +exhaust_profile = [ + [ 0.0, 0.0 ], + [ 140.0, 0.0 ], + [ 145.0, 0.00021369299793150276 ], + [ 150.0, 0.00080217001959681511 ], + [ 155.0, 0.0016865290235728025 ], + [ 160.0, 0.0027878689579665661 ], + [ 165.0, 0.0040272870101034641 ], + [ 170.0, 0.0053258808329701424 ], + [ 175.0, 0.006604751106351614 ], + [ 180.0, 0.0077849919907748699 ], + [ 185.0, 0.008787703700363636 ], + [ 190.0, 0.009533984586596489 ], + [ 195.0, 0.0099449325352907181 ], + [ 197.5, 0.0099999997764825821 ], + [ 312.5, 0.0099999997764825821 ], + [ 317.5, 0.0097863068804144859 ], + [ 322.5, 0.0091978302225470543 ], + [ 327.5, 0.0083134714514017105 ], + [ 332.5, 0.007212130818516016 ], + [ 337.5, 0.0059727132320404053 ], + [ 342.5, 0.0046741189435124397 ], + [ 347.5, 0.0033952489029616117 ], + [ 352.5, 0.0022150080185383558 ], + [ 357.5, 0.0012122959597036242 ], + [ 362.5, 0.00046601498615927994 ], + [ 367.5, 5.5067001085262746e-05 ], + [ 370.0, 0.0 ], + [ 720.0, 0.0 ] +] +exhaust_valve_count = 1 +exhaust_valve_m = 0.037000000476837158 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 29 +injection_enabled = true +intake_close_deg = 570.0 +intake_open_deg = 350.0 +intake_profile = [ + [ 0.0, 0.0 ], + [ 350.0, 0.0 ], + [ 355.0, 0.00023290800163522363 ], + [ 360.0, 0.00087152497144415975 ], + [ 365.0, 0.0018256950424984097 ], + [ 370.0, 0.003005258971825242 ], + [ 375.0, 0.0043200599029660225 ], + [ 380.0, 0.0056799398735165596 ], + [ 385.0, 0.0069947410374879837 ], + [ 390.0, 0.0081743048503994942 ], + [ 395.0, 0.0091284746304154396 ], + [ 400.0, 0.0097670918330550194 ], + [ 405.0, 0.0099999997764825821 ], + [ 515.0, 0.0099999997764825821 ], + [ 520.0, 0.0097670918330550194 ], + [ 525.0, 0.0091284746304154396 ], + [ 530.0, 0.0081743048503994942 ], + [ 535.0, 0.0069947410374879837 ], + [ 540.0, 0.0056799398735165596 ], + [ 545.0, 0.0043200599029660225 ], + [ 550.0, 0.003005258971825242 ], + [ 555.0, 0.0018256950424984097 ], + [ 560.0, 0.00087152497144415975 ], + [ 565.0, 0.00023290800163522363 ], + [ 570.0, 0.0 ], + [ 720.0, 0.0 ] +] +intake_valve_count = 1 +intake_valve_m = 0.045000001788139343 +junction_method = 0 +kind = 'cylinder' +maximum_lift_m = 0.0099999997764825821 +misfire_probability = 0.0 +name = 'Cylinder 3' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 270.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ 0.0, 0.4100109338760376, 0.40000021457672119 ], [ 0.0, 0.41001105308532715, -0.5000004768371582 ] ] +position_m = [ 0.059799998998641968, 0.11950695514678955, -0.063542984426021576 ] +pressure_pa = 101325.0 +rod_m = 0.1289999932050705 +rotation_deg = [ -28.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.076200000941753387 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 75.0 +valve_spring_n_m = 38000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.10159999877214432 +burn_duration_deg = 65.0 +burn_start_deg = 5.0 +compression = 7.0 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 140.0 +exhaust_profile = [ + [ 0.0, 0.0 ], + [ 140.0, 0.0 ], + [ 145.0, 0.00021369299793150276 ], + [ 150.0, 0.00080217001959681511 ], + [ 155.0, 0.0016865290235728025 ], + [ 160.0, 0.0027878689579665661 ], + [ 165.0, 0.0040272870101034641 ], + [ 170.0, 0.0053258808329701424 ], + [ 175.0, 0.006604751106351614 ], + [ 180.0, 0.0077849919907748699 ], + [ 185.0, 0.008787703700363636 ], + [ 190.0, 0.009533984586596489 ], + [ 195.0, 0.0099449325352907181 ], + [ 197.5, 0.0099999997764825821 ], + [ 312.5, 0.0099999997764825821 ], + [ 317.5, 0.0097863068804144859 ], + [ 322.5, 0.0091978302225470543 ], + [ 327.5, 0.0083134714514017105 ], + [ 332.5, 0.007212130818516016 ], + [ 337.5, 0.0059727132320404053 ], + [ 342.5, 0.0046741189435124397 ], + [ 347.5, 0.0033952489029616117 ], + [ 352.5, 0.0022150080185383558 ], + [ 357.5, 0.0012122959597036242 ], + [ 362.5, 0.00046601498615927994 ], + [ 367.5, 5.5067001085262746e-05 ], + [ 370.0, 0.0 ], + [ 720.0, 0.0 ] +] +exhaust_valve_count = 1 +exhaust_valve_m = 0.037000000476837158 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 30 +injection_enabled = true +intake_close_deg = 570.0 +intake_open_deg = 350.0 +intake_profile = [ + [ 0.0, 0.0 ], + [ 350.0, 0.0 ], + [ 355.0, 0.00023290800163522363 ], + [ 360.0, 0.00087152497144415975 ], + [ 365.0, 0.0018256950424984097 ], + [ 370.0, 0.003005258971825242 ], + [ 375.0, 0.0043200599029660225 ], + [ 380.0, 0.0056799398735165596 ], + [ 385.0, 0.0069947410374879837 ], + [ 390.0, 0.0081743048503994942 ], + [ 395.0, 0.0091284746304154396 ], + [ 400.0, 0.0097670918330550194 ], + [ 405.0, 0.0099999997764825821 ], + [ 515.0, 0.0099999997764825821 ], + [ 520.0, 0.0097670918330550194 ], + [ 525.0, 0.0091284746304154396 ], + [ 530.0, 0.0081743048503994942 ], + [ 535.0, 0.0069947410374879837 ], + [ 540.0, 0.0056799398735165596 ], + [ 545.0, 0.0043200599029660225 ], + [ 550.0, 0.003005258971825242 ], + [ 555.0, 0.0018256950424984097 ], + [ 560.0, 0.00087152497144415975 ], + [ 565.0, 0.00023290800163522363 ], + [ 570.0, 0.0 ], + [ 720.0, 0.0 ] +] +intake_valve_count = 1 +intake_valve_m = 0.045000001788139343 +junction_method = 0 +kind = 'cylinder' +maximum_lift_m = 0.0099999997764825821 +misfire_probability = 0.0 +name = 'Cylinder 4' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 540.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ 0.0, 0.4100109338760376, 0.40000021457672119 ], [ 0.0, 0.41001105308532715, -0.5000004768371582 ] ] +position_m = [ 0.1793999969959259, 0.11950695514678955, -0.063542984426021576 ] +pressure_pa = 101325.0 +rod_m = 0.1289999932050705 +rotation_deg = [ -28.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.076200000941753387 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 75.0 +valve_spring_n_m = 38000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.10159999877214432 +burn_duration_deg = 65.0 +burn_start_deg = 5.0 +compression = 7.0 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 140.0 +exhaust_profile = [ + [ 0.0, 0.0 ], + [ 140.0, 0.0 ], + [ 145.0, 0.00021369299793150276 ], + [ 150.0, 0.00080217001959681511 ], + [ 155.0, 0.0016865290235728025 ], + [ 160.0, 0.0027878689579665661 ], + [ 165.0, 0.0040272870101034641 ], + [ 170.0, 0.0053258808329701424 ], + [ 175.0, 0.006604751106351614 ], + [ 180.0, 0.0077849919907748699 ], + [ 185.0, 0.008787703700363636 ], + [ 190.0, 0.009533984586596489 ], + [ 195.0, 0.0099449325352907181 ], + [ 197.5, 0.0099999997764825821 ], + [ 312.5, 0.0099999997764825821 ], + [ 317.5, 0.0097863068804144859 ], + [ 322.5, 0.0091978302225470543 ], + [ 327.5, 0.0083134714514017105 ], + [ 332.5, 0.007212130818516016 ], + [ 337.5, 0.0059727132320404053 ], + [ 342.5, 0.0046741189435124397 ], + [ 347.5, 0.0033952489029616117 ], + [ 352.5, 0.0022150080185383558 ], + [ 357.5, 0.0012122959597036242 ], + [ 362.5, 0.00046601498615927994 ], + [ 367.5, 5.5067001085262746e-05 ], + [ 370.0, 0.0 ], + [ 720.0, 0.0 ] +] +exhaust_valve_count = 1 +exhaust_valve_m = 0.037000000476837158 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 31 +injection_enabled = true +intake_close_deg = 570.0 +intake_open_deg = 350.0 +intake_profile = [ + [ 0.0, 0.0 ], + [ 350.0, 0.0 ], + [ 355.0, 0.00023290800163522363 ], + [ 360.0, 0.00087152497144415975 ], + [ 365.0, 0.0018256950424984097 ], + [ 370.0, 0.003005258971825242 ], + [ 375.0, 0.0043200599029660225 ], + [ 380.0, 0.0056799398735165596 ], + [ 385.0, 0.0069947410374879837 ], + [ 390.0, 0.0081743048503994942 ], + [ 395.0, 0.0091284746304154396 ], + [ 400.0, 0.0097670918330550194 ], + [ 405.0, 0.0099999997764825821 ], + [ 515.0, 0.0099999997764825821 ], + [ 520.0, 0.0097670918330550194 ], + [ 525.0, 0.0091284746304154396 ], + [ 530.0, 0.0081743048503994942 ], + [ 535.0, 0.0069947410374879837 ], + [ 540.0, 0.0056799398735165596 ], + [ 545.0, 0.0043200599029660225 ], + [ 550.0, 0.003005258971825242 ], + [ 555.0, 0.0018256950424984097 ], + [ 560.0, 0.00087152497144415975 ], + [ 565.0, 0.00023290800163522363 ], + [ 570.0, 0.0 ], + [ 720.0, 0.0 ] +] +intake_valve_count = 1 +intake_valve_m = 0.045000001788139343 +junction_method = 0 +kind = 'cylinder' +maximum_lift_m = 0.0099999997764825821 +misfire_probability = 0.0 +name = 'Cylinder 5' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 630.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ 0.0, 0.4100109338760376, -0.40000021457672119 ], [ 0.0, 0.41001105308532715, 0.5000004768371582 ] ] +position_m = [ -0.1793999969959259, 0.11950695514678955, 0.063542984426021576 ] +pressure_pa = 101325.0 +rod_m = 0.1289999932050705 +rotation_deg = [ 28.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.076200000941753387 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 75.0 +valve_spring_n_m = 38000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.10159999877214432 +burn_duration_deg = 65.0 +burn_start_deg = 5.0 +compression = 7.0 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 140.0 +exhaust_profile = [ + [ 0.0, 0.0 ], + [ 140.0, 0.0 ], + [ 145.0, 0.00021369299793150276 ], + [ 150.0, 0.00080217001959681511 ], + [ 155.0, 0.0016865290235728025 ], + [ 160.0, 0.0027878689579665661 ], + [ 165.0, 0.0040272870101034641 ], + [ 170.0, 0.0053258808329701424 ], + [ 175.0, 0.006604751106351614 ], + [ 180.0, 0.0077849919907748699 ], + [ 185.0, 0.008787703700363636 ], + [ 190.0, 0.009533984586596489 ], + [ 195.0, 0.0099449325352907181 ], + [ 197.5, 0.0099999997764825821 ], + [ 312.5, 0.0099999997764825821 ], + [ 317.5, 0.0097863068804144859 ], + [ 322.5, 0.0091978302225470543 ], + [ 327.5, 0.0083134714514017105 ], + [ 332.5, 0.007212130818516016 ], + [ 337.5, 0.0059727132320404053 ], + [ 342.5, 0.0046741189435124397 ], + [ 347.5, 0.0033952489029616117 ], + [ 352.5, 0.0022150080185383558 ], + [ 357.5, 0.0012122959597036242 ], + [ 362.5, 0.00046601498615927994 ], + [ 367.5, 5.5067001085262746e-05 ], + [ 370.0, 0.0 ], + [ 720.0, 0.0 ] +] +exhaust_valve_count = 1 +exhaust_valve_m = 0.037000000476837158 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 32 +injection_enabled = true +intake_close_deg = 570.0 +intake_open_deg = 350.0 +intake_profile = [ + [ 0.0, 0.0 ], + [ 350.0, 0.0 ], + [ 355.0, 0.00023290800163522363 ], + [ 360.0, 0.00087152497144415975 ], + [ 365.0, 0.0018256950424984097 ], + [ 370.0, 0.003005258971825242 ], + [ 375.0, 0.0043200599029660225 ], + [ 380.0, 0.0056799398735165596 ], + [ 385.0, 0.0069947410374879837 ], + [ 390.0, 0.0081743048503994942 ], + [ 395.0, 0.0091284746304154396 ], + [ 400.0, 0.0097670918330550194 ], + [ 405.0, 0.0099999997764825821 ], + [ 515.0, 0.0099999997764825821 ], + [ 520.0, 0.0097670918330550194 ], + [ 525.0, 0.0091284746304154396 ], + [ 530.0, 0.0081743048503994942 ], + [ 535.0, 0.0069947410374879837 ], + [ 540.0, 0.0056799398735165596 ], + [ 545.0, 0.0043200599029660225 ], + [ 550.0, 0.003005258971825242 ], + [ 555.0, 0.0018256950424984097 ], + [ 560.0, 0.00087152497144415975 ], + [ 565.0, 0.00023290800163522363 ], + [ 570.0, 0.0 ], + [ 720.0, 0.0 ] +] +intake_valve_count = 1 +intake_valve_m = 0.045000001788139343 +junction_method = 0 +kind = 'cylinder' +maximum_lift_m = 0.0099999997764825821 +misfire_probability = 0.0 +name = 'Cylinder 6' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 360.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ 0.0, 0.4100109338760376, -0.40000021457672119 ], [ 0.0, 0.41001105308532715, 0.5000004768371582 ] ] +position_m = [ -0.059799998998641968, 0.11950695514678955, 0.063542984426021576 ] +pressure_pa = 101325.0 +rod_m = 0.1289999932050705 +rotation_deg = [ 28.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.076200000941753387 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 75.0 +valve_spring_n_m = 38000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.10159999877214432 +burn_duration_deg = 65.0 +burn_start_deg = 5.0 +compression = 7.0 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 140.0 +exhaust_profile = [ + [ 0.0, 0.0 ], + [ 140.0, 0.0 ], + [ 145.0, 0.00021369299793150276 ], + [ 150.0, 0.00080217001959681511 ], + [ 155.0, 0.0016865290235728025 ], + [ 160.0, 0.0027878689579665661 ], + [ 165.0, 0.0040272870101034641 ], + [ 170.0, 0.0053258808329701424 ], + [ 175.0, 0.006604751106351614 ], + [ 180.0, 0.0077849919907748699 ], + [ 185.0, 0.008787703700363636 ], + [ 190.0, 0.009533984586596489 ], + [ 195.0, 0.0099449325352907181 ], + [ 197.5, 0.0099999997764825821 ], + [ 312.5, 0.0099999997764825821 ], + [ 317.5, 0.0097863068804144859 ], + [ 322.5, 0.0091978302225470543 ], + [ 327.5, 0.0083134714514017105 ], + [ 332.5, 0.007212130818516016 ], + [ 337.5, 0.0059727132320404053 ], + [ 342.5, 0.0046741189435124397 ], + [ 347.5, 0.0033952489029616117 ], + [ 352.5, 0.0022150080185383558 ], + [ 357.5, 0.0012122959597036242 ], + [ 362.5, 0.00046601498615927994 ], + [ 367.5, 5.5067001085262746e-05 ], + [ 370.0, 0.0 ], + [ 720.0, 0.0 ] +] +exhaust_valve_count = 1 +exhaust_valve_m = 0.037000000476837158 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 33 +injection_enabled = true +intake_close_deg = 570.0 +intake_open_deg = 350.0 +intake_profile = [ + [ 0.0, 0.0 ], + [ 350.0, 0.0 ], + [ 355.0, 0.00023290800163522363 ], + [ 360.0, 0.00087152497144415975 ], + [ 365.0, 0.0018256950424984097 ], + [ 370.0, 0.003005258971825242 ], + [ 375.0, 0.0043200599029660225 ], + [ 380.0, 0.0056799398735165596 ], + [ 385.0, 0.0069947410374879837 ], + [ 390.0, 0.0081743048503994942 ], + [ 395.0, 0.0091284746304154396 ], + [ 400.0, 0.0097670918330550194 ], + [ 405.0, 0.0099999997764825821 ], + [ 515.0, 0.0099999997764825821 ], + [ 520.0, 0.0097670918330550194 ], + [ 525.0, 0.0091284746304154396 ], + [ 530.0, 0.0081743048503994942 ], + [ 535.0, 0.0069947410374879837 ], + [ 540.0, 0.0056799398735165596 ], + [ 545.0, 0.0043200599029660225 ], + [ 550.0, 0.003005258971825242 ], + [ 555.0, 0.0018256950424984097 ], + [ 560.0, 0.00087152497144415975 ], + [ 565.0, 0.00023290800163522363 ], + [ 570.0, 0.0 ], + [ 720.0, 0.0 ] +] +intake_valve_count = 1 +intake_valve_m = 0.045000001788139343 +junction_method = 0 +kind = 'cylinder' +maximum_lift_m = 0.0099999997764825821 +misfire_probability = 0.0 +name = 'Cylinder 7' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 180.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ 0.0, 0.4100109338760376, -0.40000021457672119 ], [ 0.0, 0.41001105308532715, 0.5000004768371582 ] ] +position_m = [ 0.059799998998641968, 0.11950695514678955, 0.063542984426021576 ] +pressure_pa = 101325.0 +rod_m = 0.1289999932050705 +rotation_deg = [ 28.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.076200000941753387 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 75.0 +valve_spring_n_m = 38000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.10159999877214432 +burn_duration_deg = 65.0 +burn_start_deg = 5.0 +compression = 7.0 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 140.0 +exhaust_profile = [ + [ 0.0, 0.0 ], + [ 140.0, 0.0 ], + [ 145.0, 0.00021369299793150276 ], + [ 150.0, 0.00080217001959681511 ], + [ 155.0, 0.0016865290235728025 ], + [ 160.0, 0.0027878689579665661 ], + [ 165.0, 0.0040272870101034641 ], + [ 170.0, 0.0053258808329701424 ], + [ 175.0, 0.006604751106351614 ], + [ 180.0, 0.0077849919907748699 ], + [ 185.0, 0.008787703700363636 ], + [ 190.0, 0.009533984586596489 ], + [ 195.0, 0.0099449325352907181 ], + [ 197.5, 0.0099999997764825821 ], + [ 312.5, 0.0099999997764825821 ], + [ 317.5, 0.0097863068804144859 ], + [ 322.5, 0.0091978302225470543 ], + [ 327.5, 0.0083134714514017105 ], + [ 332.5, 0.007212130818516016 ], + [ 337.5, 0.0059727132320404053 ], + [ 342.5, 0.0046741189435124397 ], + [ 347.5, 0.0033952489029616117 ], + [ 352.5, 0.0022150080185383558 ], + [ 357.5, 0.0012122959597036242 ], + [ 362.5, 0.00046601498615927994 ], + [ 367.5, 5.5067001085262746e-05 ], + [ 370.0, 0.0 ], + [ 720.0, 0.0 ] +] +exhaust_valve_count = 1 +exhaust_valve_m = 0.037000000476837158 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 34 +injection_enabled = true +intake_close_deg = 570.0 +intake_open_deg = 350.0 +intake_profile = [ + [ 0.0, 0.0 ], + [ 350.0, 0.0 ], + [ 355.0, 0.00023290800163522363 ], + [ 360.0, 0.00087152497144415975 ], + [ 365.0, 0.0018256950424984097 ], + [ 370.0, 0.003005258971825242 ], + [ 375.0, 0.0043200599029660225 ], + [ 380.0, 0.0056799398735165596 ], + [ 385.0, 0.0069947410374879837 ], + [ 390.0, 0.0081743048503994942 ], + [ 395.0, 0.0091284746304154396 ], + [ 400.0, 0.0097670918330550194 ], + [ 405.0, 0.0099999997764825821 ], + [ 515.0, 0.0099999997764825821 ], + [ 520.0, 0.0097670918330550194 ], + [ 525.0, 0.0091284746304154396 ], + [ 530.0, 0.0081743048503994942 ], + [ 535.0, 0.0069947410374879837 ], + [ 540.0, 0.0056799398735165596 ], + [ 545.0, 0.0043200599029660225 ], + [ 550.0, 0.003005258971825242 ], + [ 555.0, 0.0018256950424984097 ], + [ 560.0, 0.00087152497144415975 ], + [ 565.0, 0.00023290800163522363 ], + [ 570.0, 0.0 ], + [ 720.0, 0.0 ] +] +intake_valve_count = 1 +intake_valve_m = 0.045000001788139343 +junction_method = 0 +kind = 'cylinder' +maximum_lift_m = 0.0099999997764825821 +misfire_probability = 0.0 +name = 'Cylinder 8' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 89.999977111816406 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ 0.0, 0.4100109338760376, -0.40000021457672119 ], [ 0.0, 0.41001105308532715, 0.5000004768371582 ] ] +position_m = [ 0.1793999969959259, 0.11950695514678955, 0.063542984426021576 ] +pressure_pa = 101325.0 +rod_m = 0.1289999932050705 +rotation_deg = [ 28.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.076200000941753387 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 75.0 +valve_spring_n_m = 38000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 75.0 + seat_restitution = 0.004999999888241291 + spring_rate = 38000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 35 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'intake_plenum' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ + [ -0.375, -0.4999992847442627, -0.23423221707344055 ], + [ -0.125, -0.4999992847442627, -0.23423221707344055 ], + [ 0.125, -0.4999992847442627, -0.23423221707344055 ], + [ 0.375, -0.4999992847442627, -0.23423221707344055 ], + [ -0.375, -0.4999992847442627, 0.23423221707344055 ], + [ -0.125, -0.4999992847442627, 0.23423221707344055 ], + [ 0.125, -0.4999992847442627, 0.23423221707344055 ], + [ 0.375, -0.4999992847442627, 0.23423221707344055 ], + [ -0.5, 0.0, 0.0 ] +] +position_m = [ 0.0, 0.31264111399650574, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.47839999198913574, 0.043742898851633072, 0.19114449620246887 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.064999997615814209 +discharge = 0.69999998807907104 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 36 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'throttle' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Electronic throttle' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.3091999888420105, 0.31264111399650574, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, -0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.070000000298023224, 0.070000000298023224 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.070000000298023224 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 37 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Inlet metering stub' +opening = 1.0 +outlet_diameter_m = 0.070000000298023224 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.28919997811317444, 0.31264111399650574, 0.0 ], [ -0.23919999599456787, 0.31264111399650574, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.0, 0.0, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 38 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'ecu' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Injection / ignition ECU' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 0.0, 0.56264114379882812, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.10000000149011612, 0.02500000037252903, 0.075000002980232239 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.ecu] + fixed_idle = false + idle_base_opening = 0.079999998211860657 + idle_ki = 3.9999998989515007e-05 + idle_kp = 9.9999997473787516e-05 + idle_max_opening = 0.25 + idle_minimum_advance = -5.0 + idle_rpm = 750.0 + idle_spark_gain = 0.019999999552965164 + ignition_map = [ [ 0.0, 10.0 ], [ 750.0, 12.0 ], [ 2500.0, 28.0 ], [ 4000.0, 32.0 ], [ 6000.0, 30.0 ] ] + overrun_cut_rpm = 2000.0 + overrun_fuel_cut_enabled = true + overrun_ignition_cut_enabled = false + overrun_resume_rpm = 1100.0 + power_afr = 12.800000190734863 + rev_limit_cut_time = 0.0 + rev_limit_enabled = true + rev_limit_fuel_cut_enabled = true + rev_limit_ignition_cut_enabled = true + rev_limit_rpm = 7000.0 + rev_resume_rpm = 6750.0 + stoichiometric_afr = 14.699999809265137 + throttle_closing_rate = 0.0 + throttle_closing_time_constant = 0.0 + throttle_rate = 1000.0 + throttle_time_constant = 0.0010000000474974513 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 39 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Intake atmosphere' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ -0.41920000314712524, 0.31264111399650574, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 40 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'junction_0' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ -0.42925277352333069, 0.50000137090682983, 0.21610835194587708 ], [ -0.062024462968111038, 0.50000137090682983, 0.21610835194587708 ], [ 0.50000149011611938, -0.059396862983703613, 0.0 ] ] +position_m = [ -0.039599999785423279, -0.035000000149011612, -0.19589219987392426 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.036840301007032394, 0.036840301007032394, 0.036840301007032394 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 700.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 41 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'junction_1' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ -0.42925268411636353, 0.50000137090682983, 0.21610835194587708 ], [ -0.062024161219596863, 0.50000137090682983, 0.21610835194587708 ], [ 0.50000149011611938, -0.16117401421070099, 0.0 ] ] +position_m = [ 0.19959999620914459, -0.035000000149011612, -0.19589219987392426 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.036840301007032394, 0.036840301007032394, 0.036840301007032394 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 700.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 42 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'junction_2' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ -0.50000125169754028, 0.40792009234428406, -0.17630955576896667 ], [ 0.12158969044685364, 0.50000137090682983, -0.21610835194587708 ], [ 0.50000149011611938, -0.070534117519855499, 0.0 ] ] +position_m = [ 0.020199999213218689, -0.035000000149011612, 0.19589219987392426 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.036840301007032394, 0.036840301007032394, 0.036840301007032394 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 700.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 43 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'junction_3' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ -0.50000125169754028, 0.40792009234428406, -0.17630955576896667 ], [ 0.12158989161252975, 0.50000137090682983, -0.21610835194587708 ], [ 0.50000149011611938, -0.11283836513757706, 0.0 ] ] +position_m = [ 0.13979999721050262, -0.035000000149011612, 0.19589219987392426 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.036840301007032394, 0.036840301007032394, 0.036840301007032394 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 700.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 44 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'junction_4' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ -0.50000107288360596, 0.059396862983703613, 0.0 ], [ -0.50000107288360596, 0.16117390990257263, 0.0 ], [ 0.49999862909317017, 0.0, -0.19320952892303467 ] ] +position_m = [ 0.33919999003410339, -0.079999998211860657, -0.19589219987392426 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.036840301007032394, 0.036840301007032394, 0.036840301007032394 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 700.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 45 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'junction_5' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ -0.50000107288360596, 0.070534214377403259, 0.0 ], [ -0.50000107288360596, 0.11283836513757706, 0.0 ], [ 0.49999862909317017, 0.0, 0.19320952892303467 ] ] +position_m = [ 0.33919999003410339, -0.079999998211860657, 0.19589219987392426 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.036840301007032394, 0.036840301007032394, 0.036840301007032394 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 700.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 46 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'junction_6' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ -0.50000184774398804, 0.0, 0.19320710003376007 ], [ 0.49999862909317017, 0.0, 0.0 ], [ 0.0, 0.0, 0.50000184774398804 ] ] +position_m = [ 0.88919991254806519, -0.079999998211860657, -0.40842020511627197 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.036840301007032394, 0.036840301007032394, 0.036840301007032394 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 700.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 47 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'junction_7' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [ [ -0.50000184774398804, 0.0, -0.19320710003376007 ], [ 0.49999862909317017, 0.0, 0.0 ], [ 0.0, 0.0, -0.50000184774398804 ] ] +position_m = [ 0.88919991254806519, -0.079999998211860657, 0.40842020511627197 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.036840301007032394, 0.036840301007032394, 0.036840301007032394 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 700.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 48 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Exhaust atmosphere' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 2.1576199531555176, -0.079999998211860657, -0.40842020511627197 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + +[[parts]] +afr = 14.699999809265137 +ambient_temperature = 300.0 +bore_m = 0.075000002980232239 +burn_duration_deg = 60.0 +burn_start_deg = 5.0 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +energy_variation = 0.05000000074505806 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_profile = [] +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +fuel_lhv = 44000000.0 +heat_transfer = 100.0 +id = 49 +injection_enabled = true +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_profile = [] +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +misfire_probability = 0.0 +name = 'Exhaust atmosphere' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +port_coordinates = [] +position_m = [ 2.1576199531555176, -0.079999998211860657, 0.40842020511627197 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 +wiebe_a = 5.0 +wiebe_m = 2.0 + + [parts.exhaust_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 + + [parts.intake_dynamics] + cam_restitution = 0.0099999997764825821 + damping = 24.0 + mass_kg = 0.035000000149011612 + maximum_lift = 0.014999999664723873 + seat_force = 100.0 + seat_restitution = 0.004999999888241291 + spring_rate = 30000.0 + stop_restitution = 0.0099999997764825821 diff --git a/machines/vacuum-pump.machine.toml b/machines/vacuum-pump.machine.toml new file mode 100644 index 0000000..01cca23 --- /dev/null +++ b/machines/vacuum-pump.machine.toml @@ -0,0 +1,254 @@ +cells_per_meter = 40.0 +drive_rpm = 900.0 +driven = true +format = 1 +load_torque = 0.0 +name = 'Vacuum pump bench' +scene = 2 +shaft_inertia = 0.15000000596046448 +solver = 'rusanov' + +[[connections]] +a_part = 4 +a_port = 0 +b_part = 1 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 4 +a_port = 1 +b_part = 2 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 5 +a_port = 0 +b_part = 2 +b_port = 1 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[connections]] +a_part = 5 +a_port = 1 +b_part = 3 +b_port = 0 +discharge = 1.0 +inertance_length_m = 0.0099999997764825821 +loss = 0.0 +method = 0 + +[[parts]] +afr = 14.699999809265137 +bore_m = 0.075000002980232239 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +heat_transfer = 100.0 +id = 1 +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'chamber' +maximum_lift_m = 0.0080000003799796104 +name = 'Chamber / tank 1' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +position_m = [ -0.40000000596046448, 0.2800000011920929, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.20000000298023224, 0.18000000715255737, 0.18000000715255737 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 + +[[parts]] +afr = 14.699999809265137 +bore_m = 0.075000002980232239 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +heat_transfer = 100.0 +id = 2 +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'piston_pump' +maximum_lift_m = 0.0080000003799796104 +name = 'Piston pump 2' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +position_m = [ 0.0, 0.20000000298023224, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 + +[[parts]] +afr = 14.699999809265137 +bore_m = 0.075000002980232239 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.039999999105930328 +discharge = 0.80000001192092896 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +heat_transfer = 100.0 +id = 3 +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'reservoir' +maximum_lift_m = 0.0080000003799796104 +name = 'Pressure / atmosphere 3' +opening = 1.0 +outlet_diameter_m = 0.039999999105930328 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.15000000596046448, 0.0, 0.0 ], [ 0.15000000596046448, 0.0, 0.0 ] ] +position_m = [ 0.40000000596046448, 0.31999999284744263, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.039999999105930328, 0.039999999105930328, 0.039999999105930328 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 + +[[parts]] +afr = 14.699999809265137 +bore_m = 0.075000002980232239 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.032000001519918442 +discharge = 0.80000001192092896 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +heat_transfer = 100.0 +id = 4 +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +name = 'Pipe / reducer 4' +opening = 1.0 +outlet_diameter_m = 0.032000001519918442 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.14137500524520874, 0.02138093113899231, 0.0 ], [ 0.14137500524520874, -0.021380960941314697, 0.0 ] ] +position_m = [ -0.15862500667572021, 0.25861907005310059, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 + +[[parts]] +afr = 14.699999809265137 +bore_m = 0.075000002980232239 +compression = 11.5 +cracking_pressure_pa = 100.0 +diameter_m = 0.032000001519918442 +discharge = 0.80000001192092896 +exhaust_close_deg = 370.0 +exhaust_open_deg = 130.0 +exhaust_valve_count = 1 +exhaust_valve_m = 0.028000000864267349 +heat_transfer = 100.0 +id = 5 +intake_close_deg = 580.0 +intake_open_deg = 350.0 +intake_valve_count = 1 +intake_valve_m = 0.029999999329447746 +junction_method = 0 +kind = 'pipe' +maximum_lift_m = 0.0080000003799796104 +name = 'Pipe / reducer 5' +opening = 1.0 +outlet_diameter_m = 0.032000001519918442 +pedal_control = true +phase_deg = 0.0 +points_m = [ [ -0.1913750022649765, -0.041380941867828369, 0.0 ], [ 0.1913750022649765, 0.041380941867828369, 0.0 ] ] +position_m = [ 0.20862500369548798, 0.27861905097961426, 0.0 ] +pressure_pa = 101325.0 +rod_m = 0.1120000034570694 +rotation_deg = [ 0.0, 0.0, 0.0 ] +size_m = [ 0.11999999731779099, 0.11999999731779099, 0.11999999731779099 ] +spark_advance_deg = 20.0 +stroke_m = 0.068000003695487976 +temperature_k = 300.0 +turning_loss = 0.0 +valve_damping = 24.0 +valve_mass_kg = 0.035000000149011612 +valve_preload_n = 100.0 +valve_spring_n_m = 30000.0 +wall_area_m2 = 0.019999999552965164 diff --git a/src/builder_geometry.cpp b/src/builder_geometry.cpp new file mode 100644 index 0000000..6bed9c9 --- /dev/null +++ b/src/builder_geometry.cpp @@ -0,0 +1,61 @@ +#include "FluidSim/Desktop/MachineBuilder.hpp" +#include "FluidSim/Constants.hpp" +#include +#include +namespace fluidsim::port::desktop { +using namespace machine_math; +BuilderRay builder_ray(const OrbitCamera& c,sf::FloatRect v,sf::Vector2f screen){ + const float x=(2*(screen.x-v.position.x)/v.size.x-1)*.41421356f*v.size.x/v.size.y; + const float y=(1-2*(screen.y-v.position.y)/v.size.y)*.41421356f; + const float yaw=c.yaw*pi/180,pitch=c.pitch*pi/180; + const Vec3 right{std::cos(yaw),0,std::sin(yaw)},up{std::sin(pitch)*std::sin(yaw),std::cos(pitch),-std::sin(pitch)*std::cos(yaw)},back{-std::cos(pitch)*std::sin(yaw),std::sin(pitch),std::cos(pitch)*std::cos(yaw)}; + return {c.listener().position,unit(add(add(multiply(right,x),multiply(up,y)),multiply(back,-1)))}; +} +std::optional builder_project(const OrbitCamera& c,sf::FloatRect v,Vec3 world){ + const auto d=subtract(world,c.target);const float yaw=c.yaw*pi/180,pitch=c.pitch*pi/180; + const float x=std::cos(yaw)*d.x+std::sin(yaw)*d.z,z=-std::sin(yaw)*d.x+std::cos(yaw)*d.z; + const float y=std::cos(pitch)*d.y-std::sin(pitch)*z,depth=c.distance-(std::sin(pitch)*d.y+std::cos(pitch)*z); + if(depth<.03f)return std::nullopt; + const float scale=v.size.y/(2*.41421356f*depth); + return sf::Vector2f{v.position.x+v.size.x*.5f+x*scale,v.position.y+v.size.y*.5f-y*scale}; +} +std::optional builder_pipe_location(const OrbitCamera& c,sf::FloatRect viewport,const MachinePart& p,sf::Vector2f screen){ + if(p.kind!=MachinePartKind::Pipe)return std::nullopt; + std::optional result; + const auto depth=[&](Vec3 world){const auto d=subtract(world,c.target);const float yaw=c.yaw*pi/180,pitch=c.pitch*pi/180;return c.distance-(std::sin(pitch)*d.y+std::cos(pitch)*(-std::sin(yaw)*d.x+std::cos(yaw)*d.z));}; + for(std::size_t i=1;iscreen_distance)result=BuilderPipeLocation{i-1,add(p.points[i-1],multiply(subtract(p.points[i],p.points[i-1]),t)),distance}; + } + return result; +} +std::optional builder_pick_part(const MachinePart& p,BuilderRay ray,float tolerance){ + ray.origin=machine_local_point(p,ray.origin);ray.direction=inverse_rotate(ray.direction,p.rotation); + if(p.kind==MachinePartKind::Pipe){ + float best=std::numeric_limits::max();const auto diameters=machine_pipe_diameters(p); + for(std::size_t i=1;i1e-10f?(eq-de*dq)/den:0;u=std::clamp(u,0.f,1.f);float t=std::max(0.f,dot(subtract(add(a,multiply(edge,u)),ray.origin),ray.direction)); + // Re-clamp the finite segment after solving for the half ray. + u=std::clamp(dot(subtract(add(ray.origin,multiply(ray.direction,t)),a),edge)/ee,0.f,1.f); + const float radius=std::lerp(diameters[i-1],diameters[i],u)*.5f+tolerance; + if(length(subtract(add(ray.origin,multiply(ray.direction,t)),add(a,multiply(edge,u))))<=radius)best=std::min(best,std::max(0.f,t-radius)); + }return best==std::numeric_limits::max()?std::nullopt:std::optional(best); + } + Vec3 size=machine_part_size(p);if(p.kind==MachinePartKind::Reservoir||p.kind==MachinePartKind::Cap)size={.032f,.032f,.032f}; + if(p.kind==MachinePartKind::Junction&&p.junction_method)size={.035f,.035f,.035f}; + const float origin[]{ray.origin.x,ray.origin.y,ray.origin.z},dir[]{ray.direction.x,ray.direction.y,ray.direction.z},half[]{size.x*.5f+tolerance,size.y*.5f+tolerance,size.z*.5f+tolerance};float enter=0,exit=1e9f; + for(int i=0;i<3;++i){if(std::abs(dir[i])<1e-8f){if(std::abs(origin[i])>half[i])return std::nullopt;}else{float a=(-half[i]-origin[i])/dir[i],b=(half[i]-origin[i])/dir[i];if(a>b)std::swap(a,b);enter=std::max(enter,a);exit=std::min(exit,b);if(exit #include @@ -34,6 +36,8 @@ constexpr float throttle_lerp_rate = 10.0f; int main(int argc, char** argv) try { using namespace fluidsim::port; bool scenario_from_args=false; + bool builder_from_args=false,builder_play=false; + std::filesystem::path machine_file; std::string junction_model; std::optional junction_loss; auto examples=scenario_examples(); @@ -42,6 +46,8 @@ int main(int argc, char** argv) try { OutdoorScene selected_scene=OutdoorScene::Wall; for(int i=1;i scenario_resolution; while (window.isOpen()) { if (!scenario_from_args) { - window.setTitle("FluidSim - Examples"); - window.setMinimumSize(sf::Vector2u{720,606}); - window.setSize({720,606}); + window.setTitle("FluidSim - Main menu"); + window.setMinimumSize(sf::Vector2u{1280,720}); + desktop::maximize_window(window); const auto choice = desktop::choose_scenario(window, font_ptr,examples); if (!choice) return 0; - selected=examples[*choice]; - window.setSize({window_width, window_height}); - window.setView(sf::View(sf::FloatRect({0, 0}, {float(window_width), float(window_height)}))); + if(!choice->example){desktop::run_machine_builder(window,font_ptr,choice->machine,choice->play);continue;} + selected=examples[*choice->example]; } scenario_from_args=false; window.setTitle("FluidSim - "+selected.title); window.setMinimumSize(sf::Vector2u{1040,800}); + desktop::maximize_window(window); if(!junction_model.empty()) selected.junctions.model=junction_model; if(junction_loss) selected.junctions.turning_loss=*junction_loss; if(selected.kind==ScenarioKind::TwoStroke && (!junction_model.empty() || junction_loss)) diff --git a/src/ecu.cpp b/src/ecu.cpp index 7d47df1..bb4bc23 100644 --- a/src/ecu.cpp +++ b/src/ecu.cpp @@ -1,15 +1,65 @@ #include "FluidSim/Ecu.hpp" +#include "FluidSim/Constants.hpp" #include #include +#include +#include namespace fluidsim::port { +const char* sensor_ecu_strategy_key(SensorEcuConfig::Strategy strategy){ + switch(strategy){case SensorEcuConfig::Strategy::Ignition:return "ignition";case SensorEcuConfig::Strategy::AlphaN:return "alpha_n";case SensorEcuConfig::Strategy::SpeedDensity:return "speed_density";}throw std::invalid_argument("Unknown sensor ECU strategy"); +} +const char* sensor_ecu_strategy_name(SensorEcuConfig::Strategy strategy){ + switch(strategy){case SensorEcuConfig::Strategy::Ignition:return "Crank ignition";case SensorEcuConfig::Strategy::AlphaN:return "Alpha-N EFI";case SensorEcuConfig::Strategy::SpeedDensity:return "Speed-density EFI";}throw std::invalid_argument("Unknown sensor ECU strategy"); +} +SensorEcuConfig::Strategy sensor_ecu_strategy_from_key(std::string_view key){ + for(int i=0;i ecu_crank_sample(const SignalValue& speed,const SignalValue& angle,double time){ + if(!std::isfinite(time)||speed.validity!=SignalValidity::Valid||angle.validity!=SignalValidity::Valid|| + speed.timestamp!=time||angle.timestamp!=time||!speed.timing||!angle.timing||*speed.timing!=*angle.timing|| + !std::holds_alternative(speed.value)||!std::holds_alternative(angle.value))return {}; + const auto& pickup=*speed.timing; + if(!pickup.sensor||!pickup.shaft||pickup.sampled_at!=time||!std::isfinite(pickup.unwrapped_angle)|| + !std::isfinite(pickup.angular_speed)||pickup.angular_speed<0||std::get(speed.value)!=pickup.angular_speed)return {}; + // Sensors use the solver's float pi convention; tolerate its rounding while + // rejecting changed angle values that no longer describe the pickup. + constexpr double period=2*double(pi); + double wrapped=std::fmod(pickup.unwrapped_angle,period);if(wrapped<0)wrapped+=period; + const double measured=std::get(angle.value); + if(!std::isfinite(measured)||measured<0||measured>=period||std::abs(measured-wrapped)>1e-9)return {}; + return pickup; +} + +CrankIgnitionClock::Result CrankIgnitionClock::sample(const SignalValue& speed,const SignalValue& angle,double time,double advance,double tdc){ + const auto current=ecu_crank_sample(speed,angle,time); + if(!current||!std::isfinite(advance)||advance< -90||advance>90||!std::isfinite(tdc)||tdc<0||tdc>=360){reset();return {};} + constexpr double period=2*double(pi); + const double offset=(advance-tdc)*period/360; + const double current_cycle=std::floor((current->unwrapped_angle+offset)/period); + if(!previous_){previous_=current;last_event_cycle_=current_cycle;return {true,0};} + const auto before=*previous_; + if(current->sensor!=before.sensor||current->shaft!=before.shaft||time<=before.sampled_at||current->unwrapped_anglestd::numeric_limits::max()){reset();return {};} + previous_=current; + if(crossed>0)last_event_cycle_=current_cycle; + return {true,static_cast(crossed)}; +} + void Ecu::step(float dt, float pedal, float rpm) { fault_ = !std::isfinite(dt) || dt <= 0.0f || !std::isfinite(pedal) || !std::isfinite(rpm) || rpm < 0.0f; if (fault_) { target_ = 0.0f; idle_ = false; return; } pedal = std::clamp(pedal, 0.0f, 1.0f); - if (!rev_limit_enabled) limiter_ = false; - else if (rpm >= rev_limit_rpm) limiter_ = true; - else if (rpm <= rev_resume_rpm) limiter_ = false; + if (!rev_limit_enabled) {limiter_ = false;limiter_time_=0;} + else if (!limiter_ && rpm >= rev_limit_rpm) {limiter_=true;limiter_time_=std::max(0.f,rev_limit_cut_time);} + else if (limiter_) {limiter_time_=std::max(0.f,limiter_time_-dt);if(rpm<=rev_resume_rpm&&limiter_time_<=0)limiter_=false;} if (pedal > 0.005f || rpm < overrun_resume_rpm) overrun_ = false; else if (rpm > overrun_cut_rpm) overrun_ = true; @@ -58,4 +108,85 @@ float Ecu::fuel_for_air(float air) const { if (fuel_cut() || !std::isfinite(air) || air <= 0.0f) return 0.0f; return air / std::max(afr_, 1.0f); } + +namespace { +void require_calibration(bool ok,const char* why){if(!ok)throw std::invalid_argument(why);} +void validate_axis(const std::vector& axis,const char* why){ + require_calibration(axis.size()>=2&&axis.size()<=256,why); + for(std::size_t i=0;i=0&&(!i||axis[i]>axis[i-1]),why); +} +struct TableInterval {std::size_t lower;double fraction;}; +TableInterval interval(const std::vector& axis,double value){ + const auto high=std::upper_bound(axis.begin(),axis.end(),value); + const auto lower=high==axis.begin()?0:high==axis.end()?axis.size()-2:std::size_t(high-axis.begin()-1); + return {lower,std::clamp((value-axis[lower])/(axis[lower+1]-axis[lower]),0.,1.)}; +} +} +SensorEcu::SensorEcu(SensorEcuConfig config):config_(std::move(config)){ + sensor_ecu_strategy_key(config_.strategy); + require_calibration(!config_.ignition_map.empty()&&config_.ignition_map.size()<=256,"Ignition map needs 1..256 points"); + for(std::size_t i=0;i=0&&(!i||p.rpm>config_.ignition_map[i-1].rpm)&&std::isfinite(p.degrees_btdc)&&p.degrees_btdc>=-90&&p.degrees_btdc<=90,"Invalid ignition map calibration");} + require_calibration(std::isfinite(config_.rev_limit_rpm)&&std::isfinite(config_.rev_resume_rpm)&&config_.rev_limit_rpm>0&&config_.rev_resume_rpm>=0&&config_.rev_resume_rpm=0,"Fuel table requires finite nonnegative kg/cycle"); + } + if(config_.strategy==SensorEcuConfig::Strategy::SpeedDensity){ + require_calibration(std::isfinite(config_.displacement)&&config_.displacement>0&&std::isfinite(config_.air_gas_constant)&&config_.air_gas_constant>0&&std::isfinite(config_.target_afr)&&config_.target_afr>0,"Speed-density displacement, air gas constant and target AFR must be positive and finite"); + validate_axis(config_.ve_rpm,"VE RPM axis must be finite, nonnegative and increasing");validate_axis(config_.ve_pressure,"VE absolute pressure axis must be finite, nonnegative and increasing"); + require_calibration(config_.volumetric_efficiency.size()==config_.ve_rpm.size()*config_.ve_pressure.size(),"VE table dimensions do not match axes"); + for(const auto ve:config_.volumetric_efficiency)require_calibration(std::isfinite(ve)&&ve>=0&&ve<=3,"VE cells must be finite fractions in 0..3"); + } +} +void SensorEcu::apply_calibration(SensorEcuConfig config){ + SensorEcu checked(std::move(config));if(checked.config_.strategy!=config_.strategy)throw std::invalid_argument("ECU strategy requires Build");config_=std::move(checked.config_); +} +const SensorEcuState& SensorEcu::sample(const SignalValue& speed,const SignalValue& angle,const SignalValue& tps,double time){ + return sample(SensorEcuMeasurements{speed,angle,tps,{},{}},time); +} +const SensorEcuState& SensorEcu::sample(const SensorEcuMeasurements& measured,double time){ + const auto& speed=measured.speed;const auto& angle=measured.angle;const auto& tps=measured.tps; + state_={};const auto crank=ecu_crank_sample(speed,angle,time); + if(!crank){clock_.reset();return state_;} + double opening=0; + if(config_.strategy==SensorEcuConfig::Strategy::AlphaN){ + if(tps.validity!=SignalValidity::Valid||tps.timestamp!=time||!std::holds_alternative(tps.value)){clock_.reset();return state_;} + opening=std::get(tps.value);if(!std::isfinite(opening)||opening<0||opening>1){clock_.reset();return state_;} + } + double pressure=0,temperature=0; + if(config_.strategy==SensorEcuConfig::Strategy::SpeedDensity){ + const auto current=[&](const SignalValue& value){return value.validity==SignalValidity::Valid&&value.timestamp==time&&std::holds_alternative(value.value)&&std::isfinite(std::get(value.value))&&std::get(value.value)>0;}; + if(!current(measured.map)||!current(measured.iat)){clock_.reset();return state_;} + pressure=std::get(measured.map.value);temperature=std::get(measured.iat.value); + } + const double rpm=crank->angular_speed*60/(2*double(pi)); + if(!std::isfinite(rpm)){clock_.reset();return state_;} + double advance=config_.ignition_map.front().degrees_btdc; + for(std::size_t i=1;i=config_.rev_limit_rpm)limiter_=true; + else if(rpm<=config_.rev_resume_rpm)limiter_=false; + state_.sensor_fault=false;state_.rev_limited=limiter_;state_.spark_advance=advance; + if(limiter_)return state_; + state_.spark_events=timing.sparks; + if(config_.strategy==SensorEcuConfig::Strategy::AlphaN){ + const auto row=interval(config_.fuel_rpm,rpm),column=interval(config_.fuel_tps,opening);const auto width=config_.fuel_tps.size(); + const auto a=row.lower*width+column.lower,b=a+width; + state_.fuel_per_cycle=std::lerp(std::lerp(config_.fuel_per_cycle[a],config_.fuel_per_cycle[a+1],column.fraction),std::lerp(config_.fuel_per_cycle[b],config_.fuel_per_cycle[b+1],column.fraction),row.fraction); + } + if(config_.strategy==SensorEcuConfig::Strategy::SpeedDensity){ + const auto row=interval(config_.ve_rpm,rpm),column=interval(config_.ve_pressure,pressure);const auto width=config_.ve_pressure.size();const auto a=row.lower*width+column.lower,b=a+width; + state_.volumetric_efficiency=std::lerp(std::lerp(config_.volumetric_efficiency[a],config_.volumetric_efficiency[a+1],column.fraction),std::lerp(config_.volumetric_efficiency[b],config_.volumetric_efficiency[b+1],column.fraction),row.fraction); + // Divide first so finite high P/T pairs do not overflow R*T and silently + // become a valid zero-fuel estimate. + const double density=pressure/config_.air_gas_constant/temperature; + state_.estimated_air_per_cycle=density*config_.displacement*state_.volumetric_efficiency; + state_.fuel_per_cycle=state_.estimated_air_per_cycle/config_.target_afr; + } + if(config_.strategy!=SensorEcuConfig::Strategy::Ignition){state_.fuel_mass_flow=state_.fuel_per_cycle*rpm/120;if(!std::isfinite(state_.fuel_mass_flow)||!std::isfinite(state_.fuel_per_cycle)){state_={};clock_.reset();}} + return state_; +} } // namespace fluidsim::port diff --git a/src/engine.cpp b/src/engine.cpp index e788d34..92184bf 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -3,12 +3,14 @@ #include #include +#include namespace fluidsim::port { void Crankshaft::add_torque(float torque) { external_torque_ += torque; } +float Crankshaft::take_torque(){const float torque=external_torque_;external_torque_=0;return torque;} void Crankshaft::step(float dt) { if (!std::isfinite(omega)) omega = 0.0f; @@ -24,6 +26,11 @@ void Crankshaft::step(float dt) { // Brake shaft output, before external test load/drivetrain reaction. Using // acceleration torque reports zero dyno power at a loaded steady speed. const float instant_power = net_torque * omega; + record_output(net_torque,instant_power); + external_torque_ = 0.0f; + drivetrain_torque = 0.0f; +} +void Crankshaft::record_output(float net_torque,float instant_power){ if (power_count_ == power_buffer_.size()) power_sum_ -= power_buffer_[power_index_]; else ++power_count_; power_buffer_[power_index_] = instant_power; @@ -37,8 +44,6 @@ void Crankshaft::step(float dt) { torque_sum_ += net_torque; torque_index_ = (torque_index_ + 1) % torque_buffer_.size(); average_torque = torque_sum_ / static_cast(torque_count_); - external_torque_ = 0.0f; - drivetrain_torque = 0.0f; } float EngineCylinder::swept_volume() const { @@ -80,6 +85,7 @@ void EngineCylinder::initialize(const Crankshaft& /*crank*/) { air_mass = 1.225f * volume; exhaust_mass = 0.0f; fuel_mass = 0.0f; + fuel_equivalent_mass=0; energy = 101325.0f * volume / (gamma - 1.0f); pressure = 101325.0f; intake.owner_energy = exhaust.owner_energy = &energy; @@ -146,8 +152,11 @@ float FourStrokeCylinder::advance_valve(ValveState& state,const CamProfile& cam, } void FourStrokeCylinder::pre_step(Crankshaft& crank, float dt, float /*throttle*/) { + if(ignition_command&&!transported_fuel)throw std::invalid_argument("External ignition requires transported fuel delivery"); const float rpm = crank.omega * 60.0f / (2.0f * pi); - if (ecu) { + if (ignition_command) { + rev_limit_active=false; + } else if (ecu) { spark_advance = ecu->spark_advance(); rev_limit_active = ecu->rev_limited(); } else { @@ -175,16 +184,23 @@ void FourStrokeCylinder::pre_step(Crankshaft& crank, float dt, float /*throttle* const float current_degrees = wrap_degrees(theta * 180.0f / pi); const bool intake_closed_this_step=intake_cam_ && sample_cam(*intake_cam_,previous_degrees).lift_metres>0 && sample_cam(*intake_cam_,current_degrees).lift_metres<=0; if (intake_closed_this_step) { - if (!transported_fuel) - fuel_mass = ecu ? ecu->fuel_for_air(air_mass) : air_mass / std::max(afr, 1e-6f); + external_charge_ready_=true; + if (!transported_fuel){const float injected=ecu?ecu->fuel_for_air(air_mass):air_mass/std::max(afr,1e-6f);fuel_equivalent_mass=std::max(0.f,fuel_equivalent_mass+injected-fuel_mass);fuel_mass=injected;} const float charge_afr = air_mass / std::max(fuel_mass, 1e-12f); fuel_injected = fuel_mass > 0.0f && (!transported_fuel || (charge_afr >= 7 && charge_afr <= 24)); } const float spark_angle = wrap_degrees(720.0f - spark_advance); - const bool crossed_spark = crossed_angle(previous_degrees,current_degrees,spark_angle); - if (crossed_spark && fuel_injected && !combustion_active && !(ecu ? ecu->ignition_cut() : rev_limit_active) && random_unit() >= misfire_probability) { + const bool crossed_spark = ignition_command?ignition_command->fire:crossed_angle(previous_degrees,current_degrees,spark_angle); + const bool ignition_cut=ignition_command?ignition_command->cut:ecu?ecu->ignition_cut():rev_limit_active; + if(ignition_command)ignition_command->fire=false; + if(ignition_command&&combustion_active){float travel=current_degrees-previous_degrees;if(travel<0)travel+=720;external_burn_angle_+=travel;} + // Wasted sparks on the exhaust revolution must not overwrite the most + // recent trapped-charge measurement with already burned exhaust gas. + if(crossed_spark&&(!ignition_command||external_charge_ready_||fuel_injected)){charge_sampled=true;charge_afr=fuel_mass>1e-12f?air_mass/fuel_mass:std::numeric_limits::quiet_NaN();external_charge_ready_=false;} + if (crossed_spark && fuel_injected && !combustion_active && !ignition_cut && random_unit() >= misfire_probability) { combustion_active = true; + external_burn_angle_=0; burn_fraction = 0.0f; active_fuel_mass_ = transported_fuel ? std::min(fuel_mass, air_mass / 14.7f) : fuel_mass; energy_factor_ = 1.0f + energy_variation * (2.0f * random_unit() - 1.0f); @@ -193,10 +209,11 @@ void FourStrokeCylinder::pre_step(Crankshaft& crank, float dt, float /*throttle* if (combustion_active) { float angle_since_spark = current_degrees - spark_angle; if (angle_since_spark < 0.0f) angle_since_spark += 720.0f; + if(ignition_command)angle_since_spark=external_burn_angle_; const float phi = (angle_since_spark - burn_start) / std::max(burn_duration, 1e-6f); const float burned = phi <= 0.0f ? 0.0f : 1.0f - std::exp(-wiebe_a * std::pow(phi, wiebe_m + 1.0f)); float fraction = std::max(burn_fraction, std::clamp(burned, 0.0f, 1.0f)); - if (fraction >= 1.0f || angle_since_spark > burn_duration + burn_start + spark_advance) { + if (fraction >= 1.0f || angle_since_spark > burn_duration + burn_start + (ignition_command?0:spark_advance)) { fraction = 1.0f; combustion_active = false; if (!transported_fuel) { @@ -221,6 +238,7 @@ void FourStrokeCylinder::pre_step(Crankshaft& crank, float dt, float /*throttle* } if (!ecu && rev_limit_active) { + fuel_equivalent_mass=std::max(0.f,fuel_equivalent_mass-fuel_mass); fuel_mass = 0.0f; fuel_injected = false; combustion_active = false; @@ -232,6 +250,7 @@ void FourStrokeCylinder::pre_step(Crankshaft& crank, float dt, float /*throttle* void EngineCylinder::update_state(float dt) { float delta_air = 0.0f, delta_exhaust = 0.0f, delta_fuel = 0.0f, delta_energy = 0.0f; + float delta_equivalent=0;const float equivalent=std::clamp(std::max(fuel_mass,fuel_equivalent_mass)/std::max(total_mass(),1e-12f),0.f,1.f); for (Port* port : {&intake, &exhaust}) { const float mass_flow = port->mass_flow; const float fraction = mass_flow >= 0.0f ? port->air_fraction : air_fraction(); @@ -241,11 +260,14 @@ void EngineCylinder::update_state(float dt) { delta_fuel += mass_flow * fuel_fraction * dt; delta_exhaust += mass_flow * std::max(1.0f - fraction - fuel_fraction, 0.0f) * dt; delta_energy += mass_flow * port->enthalpy * dt; + delta_equivalent+=mass_flow*(mass_flow>=0?std::max(port->fuel_equivalent_fraction,port->fuel_fraction):equivalent)*dt; } air_mass += delta_air; exhaust_mass += delta_exhaust; fuel_mass += delta_fuel; energy += delta_energy; + fuel_equivalent_mass=std::clamp(fuel_equivalent_mass+delta_equivalent,0.f,std::max(total_mass(),0.f)); + if(fuel_equivalent_mass::min())fuel_equivalent_mass=0; const float current_volume = std::max(volume, 1e-12f); float current_pressure = (gamma - 1.0f) * energy / current_volume; @@ -263,6 +285,7 @@ void EngineCylinder::update_state(float dt) { air_mass = 1e-9f; exhaust_mass = 0.0f; fuel_mass = 0.0f; + fuel_equivalent_mass=0; energy = 101325.0f * current_volume / (gamma - 1.0f); } else if (energy < 0.0f) { energy = 101325.0f * current_volume / (gamma - 1.0f); @@ -314,6 +337,7 @@ void EngineCylinder::sync_ports() { port->temperature = temp; port->air_fraction = fraction; port->fuel_fraction = fuel_fraction; + port->fuel_equivalent_fraction=std::clamp(std::max(fuel_mass,fuel_equivalent_mass)/std::max(total_mass(),1e-12f),0.f,1.f); port->enthalpy = enthalpy; } } diff --git a/src/engine_config.cpp b/src/engine_config.cpp index 5f318a0..86a8242 100644 --- a/src/engine_config.cpp +++ b/src/engine_config.cpp @@ -193,6 +193,8 @@ void read_ecu(Reader r,EcuConfig& c) { r.field("power_afr",c.power_afr); r.field("rev_limit_enabled",c.rev_limit_enabled); r.field("rev_limit_fuel_cut_enabled",c.rev_limit_fuel_cut_enabled); + r.field("rev_limit_ignition_cut_enabled",c.rev_limit_ignition_cut_enabled); + r.field("rev_limit_cut_time_s",c.rev_limit_cut_time); r.field("rev_limit_rpm",c.rev_limit_rpm); r.field("rev_resume_rpm",c.rev_resume_rpm); r.field("stoichiometric_afr",c.stoichiometric_afr); @@ -377,6 +379,7 @@ void validate_ecu(const EcuConfig& c,const std::string& path) { require(c.throttle_time_constant>0 && c.throttle_rate>0 && c.idle_spark_gain>=0 && c.throttle_closing_time_constant>=0 && c.throttle_closing_rate>=0,path,"ecu: invalid actuator calibration"); require(c.stoichiometric_afr>0 && c.power_afr>0,path,"ecu: AFR targets must be positive"); + require(c.rev_limit_cut_time>=0&&c.rev_limit_cut_time<=10,path,"ecu: rev_limit_cut_time_s must be in 0..10"); } void validate_vehicle(const VehicleConfig& c,const std::string& path) { require(c.mass>0 && c.wheel_radius>0 && c.rotational_inertia>=0 && diff --git a/src/fuel_injector.cpp b/src/fuel_injector.cpp new file mode 100644 index 0000000..d1552e9 --- /dev/null +++ b/src/fuel_injector.cpp @@ -0,0 +1,45 @@ +#include "FluidSim/FuelInjector.hpp" +#include "FluidSim/Fluid.hpp" +#include +#include +#include +#include + +namespace fluidsim::port { +FuelInjector::FuelInjector(FuelInjectorConfig config):config_(config){ + if((config.supply!=FuelInjectorConfig::Supply::IdealRail&&config.supply!=FuelInjectorConfig::Supply::FiniteRail)|| + !std::isfinite(config.maximum_mass_flow)||config.maximum_mass_flow<=0|| + !std::isfinite(config.initial_fuel_mass)||config.initial_fuel_mass<0|| + !std::isfinite(config.fuel_temperature)||config.fuel_temperature<=0) + throw std::invalid_argument("Invalid injector rail, flow capacity or fuel temperature"); + state_.remaining_mass=config.supply==FuelInjectorConfig::Supply::FiniteRail?config.initial_fuel_mass:0; + state_.empty=config.supply==FuelInjectorConfig::Supply::FiniteRail&&state_.remaining_mass==0; +} +const FuelInjectorState& FuelInjector::deliver(Volume0D& host,const SignalValue& command,double time,double dt){ + if(!std::isfinite(time)||!std::isfinite(dt)||dt<=0)throw std::invalid_argument("Injector requires finite time and positive period"); + state_.mass_flow=0;state_.saturated=false;state_.command_fault=false; + if(command.validity!=SignalValidity::Valid||command.timestamp!=time||!std::holds_alternative(command.value)){ + state_.command_fault=true;return state_; + } + const auto flow=std::get(command.value); + if(!std::isfinite(flow)||flow<0){state_.command_fault=true;return state_;} + state_.saturated=flow>config_.maximum_mass_flow; + auto requested=std::min(flow,config_.maximum_mass_flow)*dt; + if(config_.supply==FuelInjectorConfig::Supply::FiniteRail)requested=std::min(requested,state_.remaining_mass); + if(!std::isfinite(requested)||requested>std::numeric_limits::max()){state_.command_fault=true;return state_;} + float mass=static_cast(requested); + // Never remove more fuel from a finite supply than it actually contains. + if(double(mass)>requested)mass=std::nextafter(mass,0.f); + const double energy=double(mass)*host.gas_constant/(host.gamma-1)*config_.fuel_temperature; + if(!std::isfinite(energy)||double(host.internal_energy())+energy>std::numeric_limits::max()||double(host.mass())+mass>std::numeric_limits::max()){ + state_.command_fault=true;return state_; + } + host.add_fuel_mass(mass,config_.fuel_temperature); + state_.mass_flow=double(mass)/dt;state_.delivered_mass+=mass; + if(config_.supply==FuelInjectorConfig::Supply::FiniteRail){ + state_.remaining_mass=std::max(0.,state_.remaining_mass-mass); + state_.empty=state_.remaining_mass::denorm_min(); + } + return state_; +} +} diff --git a/src/logic_assembly.cpp b/src/logic_assembly.cpp new file mode 100644 index 0000000..18d6fd0 --- /dev/null +++ b/src/logic_assembly.cpp @@ -0,0 +1,25 @@ +#include "logic_assembly.hpp" +#include + +namespace fluidsim::port { +void extract_machine_logic(const MachineDocument& source,const std::set& selected,MachineDocument& assembly){ + std::set chosen;std::unordered_map> neighbours; + for(const auto& node:source.logic.nodes)if(((node.kind==LogicNodeKind::Command||node.kind==LogicNodeKind::Ecu)&&selected.contains(node.part))||(node.kind==LogicNodeKind::Sensor&&assembly.find_sensor(node.sensor)))chosen.insert(node.id); + for(const auto& wire:source.logic.links){neighbours[wire.from.node].push_back(wire.to.node);neighbours[wire.to.node].push_back(wire.from.node);} + std::vector pending(chosen.begin(),chosen.end());for(std::size_t i=0;iname;node.part=0;} + if(node.kind==LogicNodeKind::Sensor){if(const auto sensor=source.find_sensor(node.sensor)){ + if(!assembly.find_sensor(sensor->id)){auto copy=*sensor;for(auto* tap:{©.target,©.reference})if(!selected.contains(tap->part)){if(const auto host=source.find(tap->part))tap->label=host->name;tap->part=0;}assembly.next_sensor_id=std::max(assembly.next_sensor_id,copy.id+1);assembly.sensors.push_back(std::move(copy));} + }else node.sensor=0; + } + assembly.logic.next_id=std::max(assembly.logic.next_id,node.id+1);assembly.logic.nodes.push_back(std::move(node)); + } + for(const auto& wire:source.logic.links)if(chosen.contains(wire.from.node)&&chosen.contains(wire.to.node))assembly.logic.links.push_back(wire); +} +void insert_machine_logic(MachineDocument& destination,const MachineDocument& assembly,const std::unordered_map& parts,const std::unordered_map& sensors){ + std::unordered_map nodes;float shift=0;if(!destination.logic.nodes.empty()){float right=-1e6f,left=1e6f;for(const auto& n:destination.logic.nodes)right=std::max(right,n.position.x);for(const auto& n:assembly.logic.nodes)left=std::min(left,n.position.x);if(!assembly.logic.nodes.empty())shift=right-left+320;} + for(auto node:assembly.logic.nodes){const auto original=node.id;node.id=destination.logic.next_id++;node.position.x+=shift;if((node.kind==LogicNodeKind::Command||node.kind==LogicNodeKind::Ecu))node.part=parts.contains(node.part)?parts.at(node.part):0;if(node.kind==LogicNodeKind::Sensor)node.sensor=sensors.contains(node.sensor)?sensors.at(node.sensor):0;nodes.emplace(original,node.id);destination.logic.nodes.push_back(std::move(node));} + for(auto wire:assembly.logic.links){wire.from.node=nodes.at(wire.from.node);wire.to.node=nodes.at(wire.to.node);destination.logic.links.push_back(std::move(wire));} +} +} diff --git a/src/logic_assembly.hpp b/src/logic_assembly.hpp new file mode 100644 index 0000000..0f66c48 --- /dev/null +++ b/src/logic_assembly.hpp @@ -0,0 +1,8 @@ +#pragma once +#include "FluidSim/Machine.hpp" +#include +#include +namespace fluidsim::port { +void extract_machine_logic(const MachineDocument&,const std::set&,MachineDocument&); +void insert_machine_logic(MachineDocument&,const MachineDocument&,const std::unordered_map& parts,const std::unordered_map& sensors); +} diff --git a/src/logic_editor.cpp b/src/logic_editor.cpp new file mode 100644 index 0000000..c83591f --- /dev/null +++ b/src/logic_editor.cpp @@ -0,0 +1,309 @@ +#include "FluidSim/Desktop/LogicEditor.hpp" +#include "FluidSim/Desktop/PortAppearance.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fluidsim::port::desktop { +namespace { +const sf::Color background{18,21,26},panel{27,31,38},border{54,62,75},text_color{229,233,240},muted{158,168,183},accent{116,170,235},amber{234,187,106}; +constexpr float node_width=240,header_height=34,pins_top=60,pin_height=40; +void box(sf::RenderTarget& target,sf::FloatRect r,sf::Color fill,sf::Color outline=sf::Color::Transparent){sf::RectangleShape shape(r.size);shape.setPosition(r.position);shape.setFillColor(fill);shape.setOutlineColor(outline);shape.setOutlineThickness(1);target.draw(shape);} +void line(sf::RenderTarget& target,sf::Vector2f a,sf::Vector2f b,sf::Color color){const sf::Vertex vertices[]{{a,color},{b,color}};target.draw(vertices,2,sf::PrimitiveType::Lines);} +void circle(sf::RenderTarget& target,sf::Vector2f p,float radius,sf::Color fill,sf::Color outline){sf::CircleShape shape(radius);shape.setOrigin({radius,radius});shape.setPosition(p);shape.setFillColor(fill);shape.setOutlineColor(outline);shape.setOutlineThickness(1.5f);target.draw(shape);} +std::string fitted(const sf::Font* font,std::string s,float width,unsigned size){if(!font)return {};sf::Text text(*font,s,size);if(text.getLocalBounds().size.x<=width)return s;while(!s.empty()){s.pop_back();text.setString(s+"...");if(text.getLocalBounds().size.x<=width)return s+"...";}return {};} +void label(sf::RenderTarget& target,const sf::Font* font,const std::string& s,float x,float y,float width,unsigned size=12,sf::Color color=text_color){if(!font||width<=0)return;sf::Text text(*font,fitted(font,s,width,size),size);text.setPosition({x,y});text.setFillColor(color);target.draw(text);} +void wrapped(sf::RenderTarget& target,const sf::Font* font,std::string text,float x,float y,float width,unsigned size,int rows,sf::Color color){if(!font)return;for(int row=0;rowwidth){--count;measured.setString(text.substr(0,count));}if(!count)return;if(count0)count=space;}label(target,font,text.substr(0,count)+(row==rows-1&&countvalidity!=SignalValidity::Valid)return value->validity==SignalValidity::Fault?"FAULT":"MISSING";if(std::holds_alternative(value->value))return std::get(value->value)?"true":"false";return number(std::get(value->value))+" "+signal_quantity_unit(port.quantity);} +int category(LogicNodeKind k){if(k<=LogicNodeKind::NumberInput)return 1;if(k<=LogicNodeKind::Hysteresis)return 2;if(k<=LogicNodeKind::BooleanToNumber)return 3;if(logic_memory_node(k))return 5;return 4;} +bool manual_boolean(LogicNodeKind k){return k==LogicNodeKind::Switch||k==LogicNodeKind::Button;} +bool manual_number(LogicNodeKind k){return k==LogicNodeKind::Slider||k==LogicNodeKind::NumberInput;} +bool has_control(LogicNodeKind k){return manual_boolean(k)||manual_number(k);} +bool configurable_quantity(LogicNodeKind k){return k==LogicNodeKind::NumberConstant||manual_number(k)||(k>=LogicNodeKind::Greater&&k<=LogicNodeKind::BooleanToNumber)||k==LogicNodeKind::Readout||k==LogicNodeKind::NumberDelay;} +std::uint32_t physical_host(const MachineDocument& doc,const LogicNode& n){if(n.kind==LogicNodeKind::Command||n.kind==LogicNodeKind::Ecu)return n.part;if(n.kind==LogicNodeKind::Sensor)if(const auto sensor=doc.find_sensor(n.sensor))return sensor->target.part;return 0;} +const LogicNodeState* live_node(const LogicEditorHooks& hooks,std::uint32_t id){if(hooks.live)for(const auto& state:hooks.live->logic)if(state.id==id)return &state;return nullptr;} +const SignalValue* live_value(const LogicEditorHooks& hooks,const LogicNode& n,const std::string& key){const auto state=live_node(hooks,n.id);if(!state)return nullptr;const auto ports=logic_node_ports(n);for(std::size_t i=0;ivalues.size();++i)if(ports[i].key==key)return &state->values[i];return nullptr;} +float body_height(const LogicNode& n){unsigned inputs=0,outputs=0;for(const auto& p:logic_node_ports(n)){if(p.direction==PortDirection::Input)++inputs;else ++outputs;}return pins_top+pin_height*std::max(inputs,outputs)+12+(has_control(n.kind)||n.kind==LogicNodeKind::Readout?40:0);} +struct Field { + std::string key,label; + double* value{};bool* boolean{};unsigned* integer{}; + double step{1},minimum{-1e15},maximum{1e15};bool restart{}; + double get()const{return value?*value:boolean?double(*boolean):double(*integer);} + void set(double v)const{if(value)*value=v;else if(boolean)*boolean=v>=.5;else *integer=unsigned(std::round(v));} +}; +std::vector fields(LogicNode& n){ + std::vector result; + const auto num=[&](const char* key,const std::string& name,double& v,double step=1,bool restart=false,double minimum=-1e15,double maximum=1e15){result.push_back({key,name,&v,nullptr,nullptr,step,minimum,maximum,restart});}; + const auto bit=[&](const char* key,const char* name,bool& v,bool restart=false){result.push_back({key,name,nullptr,&v,nullptr,1,0,1,restart});}; + const auto unit=std::string(signal_quantity_unit(n.quantity)); + if(n.kind==LogicNodeKind::BooleanConstant)bit("boolean","Value",n.boolean); + if(n.kind==LogicNodeKind::Switch||n.kind==LogicNodeKind::BooleanDelay)bit("boolean","Initial state",n.boolean,true); + if(n.kind==LogicNodeKind::NumberConstant||manual_number(n.kind)||n.kind==LogicNodeKind::NumberDelay)num("value",n.kind==LogicNodeKind::NumberConstant?"Value ["+unit+"]":"Initial value ["+unit+"]",n.value,1,n.kind!=LogicNodeKind::NumberConstant); + if(manual_number(n.kind)){num("minimum","Minimum ["+unit+"]",n.minimum,1,true);num("maximum","Maximum ["+unit+"]",n.maximum,1,true);} + if(n.kind==LogicNodeKind::And||n.kind==LogicNodeKind::Or||n.kind==LogicNodeKind::Xor||n.kind==LogicNodeKind::Nand||n.kind==LogicNodeKind::Nor)result.push_back({"input_count","Number of inputs",nullptr,nullptr,&n.input_count,1,2,16,true}); + if(n.kind==LogicNodeKind::Window||n.kind==LogicNodeKind::Hysteresis||n.kind==LogicNodeKind::Clamp){num("low","Low ["+unit+"]",n.low);num("high","High ["+unit+"]",n.high);} + if(n.kind==LogicNodeKind::Hysteresis){bit("below","Activate below low",n.below);bit("initial","Initial output",n.initial,true);} + if(n.kind==LogicNodeKind::Equal)num("tolerance","Tolerance ["+unit+"]",n.tolerance,.001,false,0); + if(n.kind==LogicNodeKind::GainOffset){num("gain","Gain [scalar]",n.gain,.1);num("offset","Offset ["+unit+"]",n.offset);} + if(n.kind==LogicNodeKind::MapRange){num("input_low","Input low ["+unit+"]",n.input_low);num("input_high","Input high ["+unit+"]",n.input_high);const auto out=std::string(signal_quantity_unit(n.output_quantity));num("output_low","Output low ["+out+"]",n.output_low);num("output_high","Output high ["+out+"]",n.output_high);bit("clamp","Clamp mapped range",n.clamp);} + if(n.kind==LogicNodeKind::BooleanToNumber){num("false_value","False value ["+unit+"]",n.false_value);num("true_value","True value ["+unit+"]",n.true_value);} + return result; +} +} +struct LogicEditor::Impl { + LogicPosition center{};float zoom{1};bool needs_fit{true}; + std::uint32_t selected{};std::set selection; + std::optional placing;std::optional pending;bool rewiring{},port_drag{}; + sf::Vector2f mouse{},press{},previous{},drag_offset{};bool panning{},dragging{},boxing{},slider_drag{};std::uint32_t held_button{},slider_node{};double slider_preview{}; + int library_tab{},library_category{},library_scroll{},field_scroll{}; + std::string search,edit_key,buffer;bool search_focus{},search_replace{},replace_text{true}; + std::optional clipboard;std::uint64_t epoch{},clipboard_epoch{}; + std::map>> traces; + std::map> manual_values; + struct Action {sf::FloatRect rect;std::string title;std::function run;bool enabled{true},active{};}; + sf::Vector2f project(LogicPosition p,sf::FloatRect canvas)const{return canvas.position+canvas.size*.5f+sf::Vector2f{p.x-center.x,p.y-center.y}*zoom;} + LogicPosition unproject(sf::Vector2f p,sf::FloatRect canvas)const{const auto q=(p-canvas.position-canvas.size*.5f)/zoom;return {center.x+q.x,center.y+q.y};} + sf::FloatRect rect(const LogicNode& n,sf::FloatRect canvas)const{auto at=project(n.position,canvas);if(dragging&&selection.contains(n.id))at+=drag_offset;return {at,{node_width*zoom,body_height(n)*zoom}};} + sf::Vector2f pin(const LogicNode& n,const std::string& key,sf::FloatRect canvas)const{unsigned in=0,out=0;const auto r=rect(n,canvas);for(const auto& p:logic_node_ports(n)){const bool input=p.direction==PortDirection::Input;const auto row=input?in++:out++;if(p.key==key)return r.position+sf::Vector2f{input?0:node_width,(pins_top+row*pin_height+7)}*zoom;}throw std::invalid_argument("Unknown logic editor pin");} + std::optional pick_pin(const LogicGraph& graph,sf::Vector2f point,sf::FloatRect canvas)const{for(auto it=graph.nodes.rbegin();it!=graph.nodes.rend();++it)for(const auto& p:logic_node_ports(*it))if((point-pin(*it,p.key,canvas)).length()<=std::max(7.f,7*zoom))return LogicEndpoint{it->id,p.key};return {};} + const LogicNode* pick(const LogicGraph& graph,sf::Vector2f point,sf::FloatRect canvas)const{for(auto it=graph.nodes.rbegin();it!=graph.nodes.rend();++it)if(rect(*it,canvas).contains(point))return &*it;return nullptr;} + void select(std::uint32_t id,const LogicEditorHooks& h,bool extend=false){if(!extend)selection.clear();if(id){if(extend&&selection.contains(id))selection.erase(id);else selection.insert(id);}selected=selection.contains(id)?id:selection.empty()?0:*selection.rbegin();field_scroll=0;edit_key.clear();if(h.select_physical)h.select_physical(selected?physical_host(h.document,*h.document.logic.find(selected)):0);} + void fit(const LogicGraph& graph,sf::FloatRect canvas,bool only){float x=1e9f,y=1e9f,r=-1e9f,b=-1e9f;for(const auto& n:graph.nodes)if(!only||selection.contains(n.id)){x=std::min(x,n.position.x);y=std::min(y,n.position.y);r=std::max(r,n.position.x+node_width);b=std::max(b,n.position.y+body_height(n));}if(x>r){center={};zoom=1;}else{center={(x+r)*.5f,(y+b)*.5f};zoom=std::clamp(std::min((canvas.size.x-64)/std::max(1.f,r-x),(canvas.size.y-64)/std::max(1.f,b-y)),.25f,1.4f);}needs_fit=false;} + void deactivate(const LogicEditorHooks& h){if(h.release_buttons)h.release_buttons();if(!h.live){manual_values.clear();traces.clear();}held_button=0;slider_drag=panning=dragging=boxing=port_drag=false;pending.reset();placing.reset();edit_key.clear();search_focus=false;} + bool control(const LogicEditorHooks& h,std::uint32_t id,bool value){try{h.boolean_control(id,value);manual_values[id]=value;return true;}catch(const std::exception& e){h.status(e.what());return false;}} + bool control(const LogicEditorHooks& h,std::uint32_t id,double value){try{h.number_control(id,value);manual_values[id]=value;return true;}catch(const std::exception& e){h.status(e.what());return false;}} + bool connect(const LogicEditorHooks& h,const LogicEndpoint& target){if(!pending)return false;const auto source=*pending;const bool rewire=rewiring;const bool result=h.edit([&]{if(rewire)h.document.logic.disconnect(source);h.document.logic.connect(source,target);});if(result){pending.reset();port_drag=false;h.status("Signal connected. Red Boolean / green Number; Ctrl+Z undoes.");}return result;} + PortCompatibility preview(const LogicGraph& graph,const LogicEndpoint& target)const{try{auto copy=graph;if(rewiring&&pending)copy.disconnect(*pending);if(pending)return logic_connection_preview(copy,*pending,target);return {false,"Choose a pin first"};}catch(const std::exception& e){return {false,e.what()};}} + void configure(LogicNode& node,const MachinePortDescriptor& descriptor)const{if(descriptor.domain==PortDomain::Number){if(configurable_quantity(node.kind)){node.quantity=descriptor.quantity;node.pressure_reference=descriptor.pressure_reference;node.output_quantity=descriptor.quantity;node.output_pressure_reference=descriptor.pressure_reference;}if(node.kind==LogicNodeKind::Command){if(descriptor.quantity==SignalQuantity::AngularSpeed)node.command=LogicCommand::MotorSpeed;else if(descriptor.quantity==SignalQuantity::Torque)node.command=LogicCommand::MotorTorque;else if(descriptor.quantity==SignalQuantity::Fraction)node.command=LogicCommand::ThrottleOpening;}if(node.kind==LogicNodeKind::Sensor){if(descriptor.quantity==SignalQuantity::AngularSpeed||descriptor.quantity==SignalQuantity::Angle)node.sensor_kind=MachineSensorKind::Crank;else if(descriptor.quantity==SignalQuantity::Temperature)node.sensor_kind=MachineSensorKind::GasTemperature;else if(descriptor.quantity==SignalQuantity::MassFlow)node.sensor_kind=MachineSensorKind::MassFlow;else if(descriptor.quantity==SignalQuantity::Fraction)node.sensor_kind=MachineSensorKind::ThrottlePosition;else if(descriptor.quantity==SignalQuantity::Pressure&&descriptor.pressure_reference==PressureReference::Differential)node.sensor_kind=MachineSensorKind::DifferentialPressure;}}} + std::vector items(const LogicEditorHooks& h)const{std::vector result;std::string query=search;for(auto& c:query)c=char(std::tolower(static_cast(c)));const int count=library_tab?int(h.document.logic.nodes.size()):logic_node_kind_count;for(int i=0;i(c)));if(!query.empty()&&title.find(query)==std::string::npos)continue; + if(!library_tab&&pending){const auto source=logic_port(h.document.logic,*pending);LogicNode candidate;candidate.kind=kind;configure(candidate,source);bool compatible=false;for(const auto& pin:logic_node_ports(candidate))if(port_compatibility(source,pin).allowed)compatible=true;if(!compatible)continue;} + result.push_back(i); + }return result;} + void create(const LogicEditorHooks& h,sf::FloatRect canvas){if(!placing||h.live)return;const auto kind=*placing;const auto at=unproject(mouse,canvas);std::uint32_t id=0;const auto source=pending;const bool rewire=rewiring;if(h.edit([&]{auto& graph=h.document.logic;id=graph.add(kind,at).id;if(source)configure(*graph.find(id),logic_port(graph,*source));if(source){const auto descriptor=logic_port(graph,*source);for(const auto& pin:logic_node_ports(*graph.find(id)))if(port_compatibility(descriptor,pin).allowed){if(rewire)graph.disconnect(*source);graph.connect(*source,{id,pin.key});break;}}})){select(id,h);pending.reset();placing.reset();}} + void copy(const LogicEditorHooks& h){if(selection.empty())return;LogicGraph fragment;for(auto n:h.document.logic.nodes)if(selection.contains(n.id)){if(n.kind==LogicNodeKind::Sensor){if(const auto s=h.document.find_sensor(n.sensor))n.binding_label=s->name;}else if(n.kind==LogicNodeKind::Command||n.kind==LogicNodeKind::Ecu){if(const auto p=h.document.find(n.part))n.binding_label=p->name;}fragment.nodes.push_back(n);fragment.next_id=std::max(fragment.next_id,n.id+1);}for(const auto& l:h.document.logic.links)if(selection.contains(l.from.node)&&selection.contains(l.to.node))fragment.links.push_back(l);clipboard=std::move(fragment);clipboard_epoch=epoch;h.status("Copied logic selection with internal wires. Commands paste with unresolved ownership.");} + void paste(const LogicEditorHooks& h){ + if(!clipboard||h.live)return; + std::vector ids;const auto fragment=*clipboard;const bool same=clipboard_epoch==epoch; + if(h.edit([&]{ + std::map map; + for(auto n:fragment.nodes){const auto old=n.id;n.id=h.document.logic.next_id++;n.position.x+=32;n.position.y+=32; + if(n.kind==LogicNodeKind::Command||n.kind==LogicNodeKind::Ecu){if(same)if(const auto p=h.document.find(n.part))n.binding_label=p->name;n.part=0;} + if(n.kind==LogicNodeKind::Sensor&&!same)n.sensor=0; + map[old]=n.id;ids.push_back(n.id);h.document.logic.nodes.push_back(std::move(n)); + } + for(auto l:fragment.links){l.from.node=map.at(l.from.node);l.to.node=map.at(l.to.node);h.document.logic.links.push_back(std::move(l));} + })){selection=std::set(ids.begin(),ids.end());selected=ids.empty()?0:ids.back();} + } + void remove(const LogicEditorHooks& h){if(h.live||selection.empty())return;const auto ids=selection;if(h.edit([&]{for(const auto id:ids)h.document.logic.remove(id);})){selection.clear();selected=0;pending.reset();}} + std::vector actions(const LogicEditorHooks&,const WorkshopLayout&,sf::FloatRect,bool); + bool commit(const LogicEditorHooks&); + bool event(const sf::Event&,const LogicEditorHooks&,const WorkshopLayout&,sf::FloatRect,bool); + void draw(sf::RenderWindow&,const sf::Font*,const LogicEditorHooks&,const WorkshopLayout&,sf::FloatRect,bool); +}; +std::vector LogicEditor::Impl::actions(const LogicEditorHooks& h,const WorkshopLayout& ui,sf::FloatRect canvas,bool panels){ + std::vector result;if(!panels)return result; + const auto button=[&](sf::FloatRect rect,std::string title,std::function action,bool enabled=true,bool active=false){result.push_back({rect,std::move(title),std::move(action),enabled,active});}; + if(ui.left.size.x>40){const float half=(ui.left.size.x-32)/2; + button({{20,108},{half,28}},"Nodes",[this]{library_tab=0;library_scroll=0;},true,!library_tab);button({{28+half,108},{half,28}},"Objects",[this]{library_tab=1;library_scroll=0;},true,library_tab); + if(!library_tab){const char* names[]{"All","Ctl","Bool","Num","I/O","Mem"};const float width=(ui.left.size.x-44)/6;for(int i=0;i<6;++i)button({{20+i*(width+4),192},{width,28}},names[i],[this,i]{library_category=i;library_scroll=0;},true,library_category==i);} + const auto entries=items(h);const int rows=ui.library_rows();library_scroll=std::clamp(library_scroll,0,std::max(0,int(entries.size())-rows)); + for(int row=0;rowname,[this,n]{edit_key="name";buffer=n->name;replace_text=true;}); + if(n->kind==LogicNodeKind::Sensor){const auto sensor=h.document.find_sensor(n->sensor);button({{x,194},{width,28}},sensor?"Sensor: "+sensor->name:"Choose sensor attachment",[this,&h]{const auto id=selected;h.edit([&]{auto& n=*h.document.logic.find(id);std::vector ids{0};for(const auto& s:h.document.sensors)ids.push_back(s.id);auto at=std::find(ids.begin(),ids.end(),n.sensor);n.sensor=at==ids.end()||++at==ids.end()?0:*at;if(const auto sensor=h.document.find_sensor(n.sensor)){n.sensor_kind=sensor->kind;n.binding_label=sensor->name;}});},!h.live); + }else if(n->kind==LogicNodeKind::Command||n->kind==LogicNodeKind::Ecu){ + if(n->kind==LogicNodeKind::Ecu)button({{x,194},{width,28}},std::string("Preset: ")+sensor_ecu_strategy_name(n->ecu_strategy),[this,&h]{h.edit([&]{auto& n=*h.document.logic.find(selected);n.ecu_strategy=next_sensor_ecu_strategy(n.ecu_strategy);std::erase_if(h.document.logic.links,[&](const auto& wire){return wire.to.node==n.id&&(wire.to.port=="tps"||wire.to.port=="map"||wire.to.port=="iat");});});},!h.live); + if(n->kind==LogicNodeKind::Command)button({{x,194},{width,28}},std::string("Command: ")+logic_command_name(n->command),[this,&h]{h.edit([&]{auto& n=*h.document.logic.find(selected);n.command=LogicCommand((int(n.command)+1)%logic_command_count);});},!h.live); + const auto target=h.document.find(n->part);button({{x,228},{width,28}},target?"Target: "+target->name:"Choose physical target",[this,&h]{h.edit([&]{auto& n=*h.document.logic.find(selected);std::vector ids{0};for(const auto& p:h.document.parts){auto candidate=n;candidate.part=p.id;if(logic_binding_problem(h.document,candidate).empty())ids.push_back(p.id);}auto at=std::find(ids.begin(),ids.end(),n.part);n.part=at==ids.end()||++at==ids.end()?0:*at;if(const auto target=h.document.find(n.part))n.binding_label=target->name;});},!h.live); + }else if(configurable_quantity(n->kind)){ + const auto quantity=[this,&h](bool output){h.edit([&]{auto& n=*h.document.logic.find(selected);auto& q=output?n.output_quantity:n.quantity;auto& ref=output?n.output_pressure_reference:n.pressure_reference;q=SignalQuantity(int(q)%int(SignalQuantity::Inertia)+1);ref=q==SignalQuantity::Pressure?PressureReference::Absolute:PressureReference::None;});}; + button({{x,194},{width,28}},std::string("Quantity: ")+signal_quantity_key(n->quantity)+" ["+signal_quantity_unit(n->quantity)+"]",[quantity]{quantity(false);},!h.live); + const auto pressure=[this,&h](bool output){h.edit([&]{auto& n=*h.document.logic.find(selected);auto& ref=output?n.output_pressure_reference:n.pressure_reference;ref=PressureReference(int(ref)%3+1);});}; + const char* references[]{"none","absolute","gauge","differential"}; + if(n->quantity==SignalQuantity::Pressure)button({{x,228},{width,28}},std::string("Pressure: ")+references[int(n->pressure_reference)],[pressure]{pressure(false);},!h.live); + if(n->kind==LogicNodeKind::MapRange){const float half=(width-8)/2;button({{x,264},{half,28}},std::string("Out: ")+signal_quantity_key(n->output_quantity),[quantity]{quantity(true);},!h.live);if(n->output_quantity==SignalQuantity::Pressure)button({{x+half+8,264},{half,28}},references[int(n->output_pressure_reference)],[pressure]{pressure(true);},!h.live);} + } + const auto f=fields(*h.document.logic.find(selected));const int rows=std::max(1,int((ui.height-190-310)/52));field_scroll=std::clamp(field_scroll,0,std::max(0,int(f.size())-rows)); + for(int row=0;rowrestart)throw std::invalid_argument("This initial/schema property requires Build");if(valueminimum||value>at->maximum)throw std::invalid_argument("Value is outside the property range");at->set(value);}}); + if(applied)edit_key.clear(); + return applied; + }catch(const std::exception& e){h.status(e.what());return false;} +} +bool LogicEditor::Impl::event(const sf::Event& event,const LogicEditorHooks& h,const WorkshopLayout& ui,sf::FloatRect canvas,bool panels){ + auto& graph=h.document.logic;std::erase_if(selection,[&](auto id){return !graph.find(id);});if(!selection.contains(selected))selected=selection.empty()?0:*selection.rbegin(); + if(pending)try{logic_port(graph,*pending);}catch(...){pending.reset();port_drag=false;} + if(event.is()){deactivate(h);return false;} + if(!edit_key.empty()){ + if(const auto key=event.getIf()){if(key->code==sf::Keyboard::Key::Escape){edit_key.clear();return true;}if(key->code==sf::Keyboard::Key::Enter||key->code==sf::Keyboard::Key::Tab){commit(h);return true;}if(key->code==sf::Keyboard::Key::Backspace){if(replace_text)buffer.clear();else if(!buffer.empty())buffer.pop_back();replace_text=false;}return true;} + if(const auto text=event.getIf()){const auto c=text->unicode;const bool valid=edit_key=="name"?c>=32&&c<127:(c>='0'&&c<='9')||c=='-'||c=='+'||c=='.'||c=='e'||c=='E';if(valid){if(replace_text)buffer.clear();replace_text=false;if(buffer.size()<(edit_key=="name"?120:32))buffer+=char(c);}return true;} + if(event.is()&&!commit(h))return true; + } + if(search_focus){if(const auto key=event.getIf()){if(key->code==sf::Keyboard::Key::Escape||key->code==sf::Keyboard::Key::Enter)search_focus=false;else if(key->code==sf::Keyboard::Key::Backspace){if(search_replace)search.clear();else if(!search.empty())search.pop_back();search_replace=false;library_scroll=0;}return true;}if(const auto text=event.getIf()){if(text->unicode>=32&&text->unicode<127){if(search_replace)search.clear();search_replace=false;if(search.size()<80)search+=char(text->unicode);library_scroll=0;}return true;}} + if(const auto key=event.getIf()){ + using K=sf::Keyboard::Key; + if(key->control){if(key->code==K::F&&panels){search_focus=true;search_replace=true;return true;}if(key->code==K::A){selection.clear();for(const auto& node:graph.nodes)selection.insert(node.id);selected=selection.empty()?0:*selection.rbegin();return true;}if(key->code==K::C){copy(h);return true;}if(key->code==K::V){paste(h);return true;}if(key->code==K::D){copy(h);paste(h);return true;}return false;} + if(key->code==K::Home){fit(graph,canvas,false);return true;}if(key->code==K::F){fit(graph,canvas,true);return true;} + if(key->code==K::Escape){if(held_button){if(h.release_buttons)h.release_buttons();held_button=0;}if(pending){pending.reset();port_drag=false;}else if(placing)placing.reset();else if(dragging||boxing||slider_drag){dragging=boxing=slider_drag=false;drag_offset={};}else select(0,h);return true;} + if(key->code==K::Delete||key->code==K::Backspace){if(pending&&!h.live){const auto pin=*pending;h.edit([&]{graph.disconnect(pin);});pending.reset();}else remove(h);return true;} + if(key->code==K::Q||key->code==K::G||key->code==K::R||key->code==K::C||key->code==K::P||key->code==K::X)return true; + return false; + } + if(const auto moved=event.getIf()){ + mouse=sf::Vector2f(moved->position);const auto delta=mouse-previous;previous=mouse; + if(panning){center.x-=delta.x/zoom;center.y-=delta.y/zoom;return true;} + if(dragging){drag_offset=mouse-press;return true;} + if(slider_drag){if(const auto n=graph.find(slider_node)){const auto r=rect(*n,canvas);const auto ratio=std::clamp((mouse.x-r.position.x-12*zoom)/((node_width-24)*zoom),0.f,1.f);slider_preview=std::lerp(n->minimum,n->maximum,double(ratio));if(h.live)control(h,n->id,slider_preview);}return true;} + return boxing||port_drag||canvas.contains(mouse)||(panels&&(ui.left.contains(mouse)||ui.right.contains(mouse))); + } + if(const auto released=event.getIf()){ + mouse=sf::Vector2f(released->position);if(released->button==sf::Mouse::Button::Middle&&panning){panning=false;return true;} + if(released->button!=sf::Mouse::Button::Left)return false; + if(held_button){const auto id=held_button;held_button=0;if(h.live)control(h,id,false);return true;} + if(slider_drag){slider_drag=false;if(!h.live){const auto id=slider_node;const auto v=slider_preview;h.edit([&]{if(auto n=graph.find(id))n->value=v;});}return true;} + if(dragging){dragging=false;const auto delta=drag_offset/zoom;drag_offset={};if(delta.length()>1){const auto ids=selection;h.edit([&]{for(const auto id:ids)if(auto n=graph.find(id)){n->position.x+=delta.x;n->position.y+=delta.y;}});}return true;} + if(boxing){boxing=false;const sf::FloatRect region{{std::min(press.x,mouse.x),std::min(press.y,mouse.y)},{std::abs(mouse.x-press.x),std::abs(mouse.y-press.y)}};for(const auto& n:graph.nodes)if(region.findIntersection(rect(n,canvas)))selection.insert(n.id);selected=selection.empty()?0:*selection.rbegin();return true;} + if(port_drag){port_drag=false;if((mouse-press).length()>5&&canvas.contains(mouse))if(const auto target=pick_pin(graph,mouse,canvas);target&&pending&&*target!=*pending)connect(h,*target);return true;} + return canvas.contains(mouse); + } + if(const auto wheel=event.getIf()){ + const auto point=sf::Vector2f(wheel->position); + if(canvas.contains(point)){const auto before=unproject(point,canvas);zoom=std::clamp(zoom*std::exp(wheel->delta*.12f),.25f,2.f);const auto after=unproject(point,canvas);center.x+=before.x-after.x;center.y+=before.y-after.y;return true;} + if(panels&&ui.left.contains(point)){library_scroll=std::max(0,library_scroll+(wheel->delta<0?1:-1));return true;} + if(panels&&ui.right.contains(point)){field_scroll=std::max(0,field_scroll+(wheel->delta<0?1:-1));return true;}return false; + } + if(const auto pressed=event.getIf()){ + mouse=previous=sf::Vector2f(pressed->position); + if(panels&&ui.left.size.x>40&&ui.search.contains(mouse)&&pressed->button==sf::Mouse::Button::Left){search_focus=true;search_replace=false;return true;}search_focus=false; + if(panels&&pressed->button==sf::Mouse::Button::Left){auto list=actions(h,ui,canvas,panels);for(auto i=list.rbegin();i!=list.rend();++i)if(i->rect.contains(mouse)){if(i->enabled)i->run();else h.status(h.live?"This schema or initial-state property requires Build.":"Choose a compatible physical binding first.");return true;}} + if(!canvas.contains(mouse))return panels&&(ui.left.contains(mouse)||ui.right.contains(mouse)); + if(pressed->button==sf::Mouse::Button::Middle){panning=true;return true;} + if(pressed->button==sf::Mouse::Button::Right){if(const auto pin=pick_pin(graph,mouse,canvas);pin&&!h.live){const auto endpoint=*pin;h.edit([&]{graph.disconnect(endpoint);});pending.reset();}else if(const auto n=pick(graph,mouse,canvas))select(n->id,h);return true;} + if(pressed->button!=sf::Mouse::Button::Left)return true; + if(placing){create(h,canvas);return true;} + if(const auto hit=pick_pin(graph,mouse,canvas);hit){if(h.live){select(hit->node,h);return true;}if(pending&&*pending!=*hit){connect(h,*hit);return true;}pending=*hit;rewiring=logic_port(graph,*hit).direction==PortDirection::Input;port_drag=true;press=mouse;return true;} + if(const auto n=pick(graph,mouse,canvas)){ + const auto id=n->id;const auto r=rect(*n,canvas);const float control_y=r.position.y+r.size.y-42*zoom; + if(has_control(n->kind)&&mouse.y>=control_y){select(id,h);if(manual_boolean(n->kind)){if(n->kind==LogicNodeKind::Button){if(h.live){if(control(h,id,true))held_button=id;}else h.status("Momentary buttons are active during Test; their initial state is released.");}else{bool current=n->boolean;if(const auto live=live_value(h,*n,"out");live&&live->validity==SignalValidity::Valid)current=std::get(live->value);if(h.live&&manual_values.contains(id))current=std::get(manual_values.at(id));if(h.live)control(h,id,!current);else h.edit([&]{graph.find(id)->boolean=!current;});}} + else if(n->kind==LogicNodeKind::Slider){slider_drag=true;slider_node=id;slider_preview=n->value;sf::Event moved=sf::Event::MouseMoved{pressed->position};this->event(moved,h,ui,canvas,panels);} + else {edit_key=h.live?"manual":"value";if(const auto live=live_value(h,*n,"out");h.live&&live&&live->validity==SignalValidity::Valid)buffer=number(std::get(live->value),12);else buffer=number(n->value,12);replace_text=true;} + return true; + } + if(!selection.contains(id)||h.shift)select(id,h,h.shift); + else {selected=id;if(h.select_physical)h.select_physical(physical_host(h.document,*n));} + if(mouse.y cycle; + if(pending)if(const auto target=pick_pin(h.document.logic,mouse,canvas);target&&*target!=*pending){const auto result=preview(h.document.logic,*target);if(!result.allowed&&result.message.starts_with("Combinational cycle:")){auto graph=h.document.logic;if(rewiring)graph.disconnect(*pending);auto from=*pending,to=*target;if(logic_port(graph,from).direction==PortDirection::Input)std::swap(from,to);graph.links.push_back({from,to});std::vector path;try{logic_evaluation_order(graph,&path);}catch(const std::exception&){}cycle.insert(path.begin(),path.end());}} + for(const auto& link:h.document.logic.links){const auto from=h.document.logic.find(link.from.node),to=h.document.logic.find(link.to.node);if(!from||!to)continue;const auto descriptor=logic_port(h.document.logic,link.from);auto color=port_color(descriptor.domain);const auto value=live_value(h,*from,link.from.port);const bool invalid=value&&value->validity!=SignalValidity::Valid;if(value&&value->validity==SignalValidity::Valid&&descriptor.domain==PortDomain::Boolean&&!std::get(value->value))color=sf::Color(color.r/2,color.g/2,color.b/2);wire(window,pin(*from,link.from.port,canvas),pin(*to,link.to.port,canvas),color,invalid);} + for(const auto& node:h.document.logic.nodes){const auto r=rect(node,canvas);if(!r.findIntersection(canvas))continue;const bool active=selection.contains(node.id);const auto state=live_node(h,node.id);const bool fault=state&&!state->problem.empty(); + box(window,r,sf::Color{29,34,42},cycle.contains(node.id)?amber:active?accent:fault?amber:border);box(window,{r.position,{r.size.x,header_height*zoom}},cycle.contains(node.id)?sf::Color{75,56,35}:active?sf::Color{39,57,78}:sf::Color{37,43,52}); + const auto title_size=unsigned(std::clamp(12*zoom,8.f,14.f));label(window,font,node.name,r.position.x+10*zoom,r.position.y+8*zoom,r.size.x-20*zoom,title_size,active?accent:text_color); + if(zoom>=.65f){std::string description=logic_node_name(node.kind);if(node.kind==LogicNodeKind::Sensor){const auto sensor=h.document.find_sensor(node.sensor);description=sensor?"S"+std::to_string(sensor->id)+" / "+sensor->name:"Unresolved sensor / "+node.binding_label;}else if(node.kind==LogicNodeKind::Command||node.kind==LogicNodeKind::Ecu){const auto part=h.document.find(node.part);description=part?std::string(node.kind==LogicNodeKind::Ecu?"ECU inputs":logic_command_name(node.command))+" / "+part->name:"Unresolved target / "+node.binding_label;} + label(window,font,description,r.position.x+10*zoom,r.position.y+40*zoom,r.size.x-20*zoom,unsigned(std::max(9.f,10*zoom)),muted); + } + const auto ports=logic_node_ports(node,&h.document);for(std::size_t index=0;indexvalues.size()?&state->values[index]:nullptr; + if(value&&value->validity==SignalValidity::Valid&&port.domain==PortDomain::Boolean&&!std::get(value->value))color=sf::Color(color.r/2,color.g/2,color.b/2); + const auto type_color=port_color(port.domain);circle(window,at,std::max(3.f,5.f*zoom),filled_port(port)?color:background,type_color); + if(pending&&pending->node==node.id&&pending->port==port.key)circle(window,at,std::max(6.f,8*zoom),sf::Color::Transparent,accent); + if(zoom>=.65f){const float x=input?r.position.x+12*zoom:r.position.x+126*zoom;const float width=102*zoom;label(window,font,port.name,x,at.y-6*zoom,width,unsigned(std::max(9.f,11*zoom)),type_color);if(h.live)label(window,font,reading(value,port),x,at.y+9*zoom,width,unsigned(std::max(8.f,10*zoom)),value&&value->validity!=SignalValidity::Valid?amber:muted);} + if(canvas.contains(mouse)&&(mouse-at).length()<10){hover=port.name+" / "+port_domain_key(port.domain)+(input?(port.required?" required input":" optional input"):" output");hover_detail=port.domain==PortDomain::Number?std::string(signal_quantity_key(port.quantity))+" ["+signal_quantity_unit(port.quantity)+"]":"Boolean";if(port.quantity==SignalQuantity::Pressure){const char* reference[]{"none","absolute","gauge","differential"};hover_detail+=" / "+std::string(reference[int(port.pressure_reference)]);}if(port.minimum||port.maximum)hover_detail+=" / "+(port.minimum?number(*port.minimum):"open")+".."+(port.maximum?number(*port.maximum):"open");if(std::holds_alternative(port.default_value))hover_detail+=std::get(port.default_value)?" / default true":" / default false";else if(std::holds_alternative(port.default_value))hover_detail+=" / default "+number(std::get(port.default_value));if(h.live)hover_detail+=" / "+reading(value,port);if(pending&&*pending!=LogicEndpoint{node.id,port.key})hover_detail=preview(h.document.logic,{node.id,port.key}).message;} + } + if(has_control(node.kind)){ + const sf::FloatRect control{{r.position.x+12*zoom,r.position.y+r.size.y-40*zoom},{r.size.x-24*zoom,28*zoom}};box(window,control,background,border);std::string title; + const auto value=live_value(h,node,"out"); + if(h.live&&manual_values.contains(node.id)&&value&&value->validity==SignalValidity::Valid&&manual_values.at(node.id)==value->value)manual_values.erase(node.id); + const bool queued=h.live&&manual_values.contains(node.id); + if(manual_boolean(node.kind)){bool active=node.kind==LogicNodeKind::Switch?node.boolean:false;if(value&&value->validity==SignalValidity::Valid)active=std::get(value->value);if(queued)active=std::get(manual_values.at(node.id));title=node.kind==LogicNodeKind::Button?(held_button==node.id?"HELD":"Hold button"):std::string(queued?"Queued ":"")+(active?"ON / click to toggle":"OFF / click to toggle");label(window,font,title,control.position.x+6*zoom,control.position.y+6*zoom,control.size.x-12*zoom,unsigned(std::max(8.f,11*zoom)),port_color(PortDomain::Boolean));} + else {double current=node.value;if(value&&value->validity==SignalValidity::Valid)current=std::get(value->value);if(queued)current=std::get(manual_values.at(node.id));if(slider_drag&&slider_node==node.id)current=slider_preview; + if(node.kind==LogicNodeKind::Slider&&node.maximum>node.minimum){const float fraction=float(std::clamp((current-node.minimum)/(node.maximum-node.minimum),0.,1.));box(window,{control.position,{control.size.x*fraction,control.size.y}},sf::Color{32,72,53});} + title=(edit_key=="manual"||(edit_key=="value"&&node.kind==LogicNodeKind::NumberInput))&&selected==node.id?buffer+"|":number(current,6)+" "+signal_quantity_unit(node.quantity)+(queued?" (queued)":"");label(window,font,title,control.position.x+6*zoom,control.position.y+6*zoom,control.size.x-12*zoom,unsigned(std::max(8.f,11*zoom)),port_color(PortDomain::Number)); + } + } + if(node.kind==LogicNodeKind::Readout&&h.live){const auto value=live_value(h,node,"in");auto& trace=traces[node.id];if(value&&(trace.empty()||trace.back().first!=value->timestamp)){trace.push_back({value->timestamp,value->validity==SignalValidity::Valid?std::get(value->value):std::numeric_limits::quiet_NaN()});if(trace.size()>128)trace.pop_front();} + double lo=std::numeric_limits::infinity(),hi=-lo;for(const auto& point:trace)if(std::isfinite(point.second)){lo=std::min(lo,point.second);hi=std::max(hi,point.second);}const auto area=sf::FloatRect({r.position.x+12*zoom,r.position.y+r.size.y-36*zoom},{r.size.x-24*zoom,24*zoom});box(window,area,background); + if(trace.size()>1&&std::isfinite(lo)){const double range=std::max(1e-12,hi-lo);for(std::size_t i=1;inode);if(n){const auto port=logic_port(h.document.logic,*pending);const auto origin=pin(*n,pending->port,canvas);wire(window,port.direction==PortDirection::Output?origin:mouse,port.direction==PortDirection::Output?mouse:origin,port_color(port.domain));}} + if(placing){LogicNode ghost;ghost.kind=*placing;const auto at=unproject(mouse,canvas);ghost.position=at;if(pending)configure(ghost,logic_port(h.document.logic,*pending));const auto r=rect(ghost,canvas);box(window,r,sf::Color{27,31,38,190},accent);label(window,font,logic_node_name(ghost.kind),r.position.x+8,r.position.y+8,r.size.x-16,11,accent);} + if(boxing)box(window,{{std::min(press.x,mouse.x),std::min(press.y,mouse.y)},{std::abs(press.x-mouse.x),std::abs(press.y-mouse.y)}},sf::Color{116,170,235,28},accent); + label(window,font,std::string("LOGIC / ")+(h.live?number(h.live->time,4)+" s / ":"")+std::to_string(h.document.logic.nodes.size())+" nodes / "+number(zoom*100,3)+"%",canvas.position.x+12,canvas.position.y+10,canvas.size.x-24,11,accent); + if(h.document.logic.nodes.empty()){label(window,font,"Build a signal graph",canvas.position.x+26,canvas.position.y+64,canvas.size.x-52,18);label(window,font,"Pick a node on the left. Drag pins or click a pin then its target.",canvas.position.x+26,canvas.position.y+100,canvas.size.x-52,11,muted);} + if(canvas.contains(mouse)&&!hover.empty()){const auto width=std::min(460.f,canvas.size.x-24),x=std::clamp(mouse.x+12,canvas.position.x+8,canvas.position.x+canvas.size.x-width-8),y=std::clamp(mouse.y+24,canvas.position.y+36,canvas.position.y+canvas.size.y-82);box(window,{{x,y},{width,68}},background,accent);label(window,font,hover,x+8,y+8,width-16,10,text_color);label(window,font,hover_detail,x+8,y+26,width-16,10,muted);label(window,font,pending?"Escape cancels / connected inputs can be rewired":"Input = ring / output = filled circle",x+8,y+44,width-16,10,muted);} + window.setView(original); + if(!panels)return; + window.setView(sf::View(sf::FloatRect({0,0},{float(size.x),float(size.y)}))); + if(ui.left.size.x>40){box(window,ui.left,panel);box(window,ui.search,background,search_focus?accent:border);label(window,font,search.empty()&&!search_focus?"Search logic nodes...":search+(search_focus?"|":""),ui.search.position.x+8,ui.search.position.y+7,ui.search.size.x-16,12,search_focus?text_color:muted);const auto entries=items(h);if(library_tab)label(window,font,std::to_string(entries.size())+" nodes / F focuses selection",20,198,ui.left.size.x-24,10,muted);if(entries.empty())label(window,font,"No compatible matching nodes",20,236,ui.left.size.x-24,11,muted); + label(window,font,pending?"Library filtered by pending pin":"RED Boolean / GREEN Number",20,ui.height-220,ui.left.size.x-24,11,muted);label(window,font,"Inputs: rings / outputs: filled",20,ui.height-196,ui.left.size.x-24,10,muted);label(window,font,"Wheel: zoom / middle drag: pan",20,ui.height-172,ui.left.size.x-24,10,muted); + } + if(ui.right.size.x>40){box(window,ui.right,panel);const float x=ui.inspector_x+12,width=ui.inspector_width-24;label(window,font,"LOGIC INSPECTOR",x,109,width,13,muted);const auto n=h.document.logic.find(selected); + if(n){label(window,font,logic_node_name(n->kind),x,175,width,11,muted);const auto f=fields(*h.document.logic.find(selected));const int rows=std::max(1,int((ui.height-190-310)/52));for(int row=0;rowid);state){if(!state->problem.empty())problem=state->problem;if(!state->values.empty())label(window,font,"Sample "+number(state->values[0].timestamp,8)+" s",x,ui.height-196,width,10,muted);}label(window,font,problem.empty()?(h.live?"Live tuning / initial fields require Build":"Click values to edit. Enter applies."):problem,x,ui.height-178,width,10,problem.empty()?muted:amber); + if(int(f.size())>rows)label(window,font,"Scroll properties: "+std::to_string(field_scroll+1)+" / "+std::to_string(f.size()),x,ui.height-158,width,10,accent); + }else{label(window,font,"Select a node to inspect",x,148,width,13);label(window,font,"Shift selects several / drag headers",x,184,width,11,muted);label(window,font,"Delete removes / Ctrl+Z undoes",x,210,width,11,muted);label(window,font,"Right-click a pin to disconnect it",x,236,width,11,muted);} + } + std::string action_help;for(const auto& action:actions(h,ui,canvas,panels)){const bool over=action.rect.contains(mouse),small=action.rect.size.x<44;const float inset=small?3.f:8.f;const unsigned text_size=small?9:11;box(window,action.rect,action.active?sf::Color{38,57,79}:over&&action.enabled?sf::Color{40,46,57}:panel,action.active?accent:border);label(window,font,action.title,action.rect.position.x+inset,action.rect.position.y+6,action.rect.size.x-inset*2,text_size,action.enabled?text_color:sf::Color{100,110,125});if(over&&(fitted(font,action.title,action.rect.size.x-inset*2,text_size)!=action.title||!action.enabled))action_help=action.title+(action.enabled?"":action.title=="Focus in 3D"?" / This node has no physical host.":" / Return to Build for this edit.");} + if(!action_help.empty()){const auto area=ui.right.contains(mouse)?ui.right:ui.left;const float x=area.position.x+12,width=area.size.x-24,y=std::clamp(mouse.y+32,area.position.y+10,area.position.y+area.size.y-80);box(window,{{x,y},{width,70}},background,accent);wrapped(window,font,action_help,x+8,y+8,width-16,10,3,text_color);} + window.setView(original); +} +LogicEditor::LogicEditor():impl_(std::make_unique()){} +LogicEditor::~LogicEditor()=default; +void LogicEditor::reset_document(){auto next=std::make_unique();next->epoch=impl_->epoch+1;next->clipboard_epoch=impl_->clipboard_epoch;next->clipboard=std::move(impl_->clipboard);impl_=std::move(next);} +void LogicEditor::deactivate(const LogicEditorHooks& hooks){impl_->deactivate(hooks);} +bool LogicEditor::editing_text()const{return !impl_->edit_key.empty()||impl_->search_focus;} +bool LogicEditor::active_gesture()const{return impl_->dragging||impl_->boxing||impl_->panning||impl_->slider_drag||impl_->port_drag||impl_->held_button;} +std::uint32_t LogicEditor::selected_node()const{return impl_->selected;} +void LogicEditor::select_node(std::uint32_t id,const MachineDocument& doc){impl_->selection.clear();if(doc.logic.find(id))impl_->selection.insert(id);impl_->selected=impl_->selection.empty()?0:id;impl_->field_scroll=0;impl_->edit_key.clear();} +void LogicEditor::select_part_reference(std::uint32_t part,const MachineDocument& doc){if(!part)return;for(const auto& n:doc.logic.nodes)if(physical_host(doc,n)==part){select_node(n.id,doc);return;}} +void LogicEditor::fit(const LogicGraph& graph,sf::FloatRect canvas,bool only){impl_->fit(graph,canvas,only);} +void LogicEditor::reveal(std::uint32_t id,const MachineDocument& doc,sf::FloatRect canvas){select_node(id,doc);impl_->fit(doc.logic,canvas,true);} +sf::Vector2f LogicEditor::project(LogicPosition point,sf::FloatRect canvas)const{return impl_->project(point,canvas);} +LogicPosition LogicEditor::unproject(sf::Vector2f point,sf::FloatRect canvas)const{return impl_->unproject(point,canvas);} +sf::FloatRect LogicEditor::node_rect(const LogicNode& node,sf::FloatRect canvas)const{return impl_->rect(node,canvas);} +sf::Vector2f LogicEditor::pin_position(const LogicNode& node,const std::string& port,sf::FloatRect canvas)const{return impl_->pin(node,port,canvas);} +bool LogicEditor::event(const sf::Event& event,const LogicEditorHooks& hooks,const WorkshopLayout& ui,sf::FloatRect canvas,bool panels){if(impl_->needs_fit)impl_->fit(hooks.document.logic,canvas,false);return impl_->event(event,hooks,ui,canvas,panels);} +void LogicEditor::draw(sf::RenderWindow& window,const sf::Font* font,const LogicEditorHooks& hooks,const WorkshopLayout& ui,sf::FloatRect canvas,bool panels){impl_->draw(window,font,hooks,ui,canvas,panels);} +} diff --git a/src/logic_example.cpp b/src/logic_example.cpp new file mode 100644 index 0000000..3a306ef --- /dev/null +++ b/src/logic_example.cpp @@ -0,0 +1,52 @@ +#include "FluidSim/Machine.hpp" +#include + +namespace fluidsim::port { +MachineDocument machine_logic_example(){ + auto doc=machine_pump_example();doc.name="Automatic air supply / logic bench"; + const auto tank=doc.parts[0].id,pump=doc.parts[1].id,air=doc.parts[2].id; + doc.find(tank)->position={.4f,.28f,0};doc.find(tank)->size={.12f,.12f,.12f};doc.find(tank)->name="Compressed air tank"; + doc.find(air)->position={-.4f,.32f,0};doc.find(air)->name="Intake atmosphere"; + for(auto& link:doc.links)for(auto* endpoint:{&link.a,&link.b}){if(endpoint->part==tank){endpoint->part=air;endpoint->port=0;}else if(endpoint->part==air){endpoint->part=tank;endpoint->port=0;}} + doc.sync_connections();const auto shaft=machine_shaft_for(doc,pump);doc.find(shaft)->shaft.inertia=.01f;doc.find(shaft)->shaft.initial_speed=0;doc.find(shaft)->shaft.starter_torque=0; + std::uint32_t motor=0;for(const auto& part:doc.parts)if(part.kind==MachinePartKind::DriveMotor)motor=part.id; + const auto pressure=doc.add_sensor(MachineSensorKind::Pressure,tank).id;doc.find_sensor(pressure)->name="Tank absolute pressure"; + const auto crank=doc.add_sensor(MachineSensorKind::Crank,shaft).id; + auto& graph=doc.logic; + const auto tap=graph.add(LogicNodeKind::Sensor,{0,0}).id;graph.find(tap)->sensor=pressure;graph.find(tap)->name="Tank pressure"; + const auto threshold=graph.add(LogicNodeKind::Hysteresis,{300,0}).id;auto& h=*graph.find(threshold);h.name="Compressor demand";h.quantity=SignalQuantity::Pressure;h.pressure_reference=PressureReference::Absolute;h.low=120000;h.high=150000;h.below=true;h.initial=true; + const auto enable=graph.add(LogicNodeKind::Switch,{300,240}).id;graph.find(enable)->name="Master enable";graph.find(enable)->boolean=true; + const auto gate=graph.add(LogicNodeKind::And,{600,0}).id;graph.find(gate)->name="Enabled demand"; + const auto command=graph.add(LogicNodeKind::Command,{900,0}).id;graph.find(command)->name="Compressor motor enable";graph.find(command)->part=motor;graph.find(command)->binding_label=doc.find(motor)->name; + const auto speed=graph.add(LogicNodeKind::Slider,{600,260}).id;auto& slider=*graph.find(speed);slider.name="Compressor speed (rad/s)";slider.quantity=SignalQuantity::AngularSpeed;slider.minimum=0;slider.maximum=300;slider.value=900*2*double(pi)/60; + const auto target=graph.add(LogicNodeKind::Command,{900,260}).id;graph.find(target)->name="Motor speed target";graph.find(target)->command=LogicCommand::MotorSpeed;graph.find(target)->part=motor; + const auto display=graph.add(LogicNodeKind::Readout,{300,-180}).id;graph.find(display)->name="Tank pressure readout";graph.find(display)->quantity=SignalQuantity::Pressure;graph.find(display)->pressure_reference=PressureReference::Absolute; + const auto lamp=graph.add(LogicNodeKind::Lamp,{900,-180}).id;graph.find(lamp)->name="Compressor running demand"; + const auto pickup=graph.add(LogicNodeKind::Sensor,{0,380}).id;graph.find(pickup)->sensor=crank;graph.find(pickup)->sensor_kind=MachineSensorKind::Crank;graph.find(pickup)->name="Compressor crank pickup"; + const auto rpm=graph.add(LogicNodeKind::Readout,{300,420}).id;graph.find(rpm)->quantity=SignalQuantity::AngularSpeed;graph.find(rpm)->name="Measured angular speed"; + graph.connect({tap,"value"},{threshold,"in"});graph.connect({threshold,"out"},{gate,"in1"});graph.connect({enable,"out"},{gate,"in2"});graph.connect({gate,"out"},{command,"in"});graph.connect({speed,"out"},{target,"in"});graph.connect({tap,"value"},{display,"in"});graph.connect({gate,"out"},{lamp,"in"});graph.connect({pickup,"speed"},{rpm,"in"}); + doc.probes={{1,tank,"Tank absolute pressure",MachineQuantity::Pressure},{2,shaft,"Compressor shaft RPM",MachineQuantity::Rpm},{3,motor,"Drive power",MachineQuantity::Power}}; + validate_machine(doc);return doc; +} +MachineDocument machine_sensor_engine_example(bool injection){ + auto doc=machine_engine_example();doc.name=injection?"Sensor Alpha-N EFI bench":"Sensor crank ignition / carburetor bench"; + std::uint32_t ecu=0,cylinder=0,throttle=0,intake=0; + for(auto& part:doc.parts){if(part.kind==MachinePartKind::Ecu){ecu=part.id;part.sensor_ecu.enabled=true;part.sensor_ecu.strategy=injection?SensorEcuConfig::Strategy::AlphaN:SensorEcuConfig::Strategy::Ignition;part.injection_enabled=false;part.ecu.rev_limit_enabled=false;part.name=injection?"Alpha-N ECU":"Crank ignition ECU";} + if(part.kind==MachinePartKind::Cylinder)cylinder=part.id; + if(part.kind==MachinePartKind::Throttle){throttle=part.id;if(!injection){part.kind=MachinePartKind::Carburetor;part.name="Physical carburetor";}part.pedal_control=false;part.opening=.5f;} + if(part.kind==MachinePartKind::Chamber&&part.position.x<0)intake=part.id; + if(part.kind==MachinePartKind::DriveMotor)part.drive.target_speed=3000*2*pi/60; + } + if(injection)doc.find(ecu)->sensor_ecu.fuel_per_cycle={.000002f,.00004f,.000002f,.00004f}; + const auto shaft=machine_primary_shaft(doc);bool driven=false;for(const auto& part:doc.parts)if(part.kind==MachinePartKind::DriveMotor)driven=true; + if(!driven){const auto motor=doc.add(MachinePartKind::DriveMotor,machine_math::add(doc.find(shaft)->position,{-.3f,0,0})).id;doc.find(motor)->name="3000 RPM test drive";doc.find(motor)->drive.target_speed=3000*2*pi/60;doc.connect({shaft,0},{motor,0});} + std::erase_if(doc.links,[&](const auto& link){return link.kind==MachineConnectionKind::Control&&link.b.part!=cylinder;}); + const auto sensor=doc.add_sensor(MachineSensorKind::Crank,machine_primary_shaft(doc)).id; + auto& graph=doc.logic;const auto pickup=graph.add(LogicNodeKind::Sensor,{0,0}).id;graph.find(pickup)->sensor=sensor;graph.find(pickup)->sensor_kind=MachineSensorKind::Crank;graph.find(pickup)->name="Crank pickup"; + const auto connector=graph.add(LogicNodeKind::Ecu,{340,0}).id;graph.find(connector)->part=ecu;graph.find(connector)->ecu_strategy=doc.find(ecu)->sensor_ecu.strategy;graph.find(connector)->name="Engine ECU inputs";graph.find(connector)->binding_label=doc.find(ecu)->name; + graph.connect({pickup,"speed"},{connector,"speed"});graph.connect({pickup,"angle"},{connector,"angle"}); + if(injection){const auto tps=doc.add_sensor(MachineSensorKind::ThrottlePosition,throttle).id;const auto node=graph.add(LogicNodeKind::Sensor,{0,230}).id;graph.find(node)->sensor=tps;graph.find(node)->sensor_kind=MachineSensorKind::ThrottlePosition;graph.find(node)->name="Actual throttle position";graph.connect({node,"value"},{connector,"tps"});doc.find(intake)->injector=MachineInjectorConfig{};doc.find(intake)->injector->ecu=ecu;doc.find(intake)->injector->ecu_label=doc.find(ecu)->name;} + doc.probes={{1,cylinder,"Cylinder pressure",MachineQuantity::Pressure},{2,machine_primary_shaft(doc),"Crank RPM",MachineQuantity::Rpm},{3,cylinder,"Charge AFR",MachineQuantity::ChargeAfr}}; + validate_machine(doc);return doc; +} +} diff --git a/src/logic_runtime.cpp b/src/logic_runtime.cpp new file mode 100644 index 0000000..084f605 --- /dev/null +++ b/src/logic_runtime.cpp @@ -0,0 +1,143 @@ +#include "FluidSim/Machine.hpp" +#include +#include +#include +#include +#include +#include + +namespace fluidsim::port { +struct MachineLogicRuntime::Impl { + struct Source {int node{-1},port{-1};}; + struct Node { + std::vector ports; + std::vector sources; + std::vector values; + std::vector inputs,outputs; + SignalValue manual,stored,next; + bool hysteresis{}; + bool sensor_bound{}; + std::uint32_t ecu_shaft{}; + const char* problem{}; + std::deque events; + }; + LogicGraph graph; + SensorReader reader; + std::vector nodes; + std::unordered_map by_id; + std::vector order; + std::vector commands; + std::vector ecu_inputs; + explicit Impl(const MachineDocument& document,SensorReader source):graph(document.logic),reader(std::move(source)){ + validate_machine_logic(document);const auto missing=logic_required_inputs(graph);if(!missing.empty())throw std::invalid_argument(missing.front()); + nodes.reserve(graph.nodes.size()); + for(std::size_t i=0;ikind==MachinePartKind::Cylinder)node.ecu_shaft=machine_shaft_for(document,target->id); + for(unsigned p=0;p=0)value=nodes[source.node].values[source.port]; + else if(std::holds_alternative(node.ports[p].default_value))value=SignalValue::boolean(std::get(node.ports[p].default_value),time); + else if(std::holds_alternative(node.ports[p].default_value))value=SignalValue::number(std::get(node.ports[p].default_value),time); + else value=invalid(node.ports[p],SignalValidity::Missing,time); + if(value.validity==SignalValidity::Valid&&(!signal_value_valid_for(value,node.ports[p])||value.timestamp!=time)){value=invalid(node.ports[p],SignalValidity::Fault,time);node.problem="Input type, range or sample time does not match";} + if(value.validity!=SignalValidity::Valid&&!node.problem)node.problem=value.validity==SignalValidity::Missing?"Input signal is missing":"Input signal is invalid"; + node.values[p]=value; + }} + void emit(Node& node,unsigned output,SignalValue value,double time){const auto p=node.outputs[output];value.timestamp=time;if(value.validity==SignalValidity::Valid&&!signal_value_valid_for(value,node.ports[p])){value=invalid(node.ports[p],SignalValidity::Fault,time);node.problem="Output type or declared range does not match";}if(value.validity!=SignalValidity::Valid&&!node.problem)node.problem=value.validity==SignalValidity::Missing?"Signal is missing":"Non-finite or invalid signal";node.values[p]=value;} + void evaluate(double time,double dt){ + if(!std::isfinite(time)||!std::isfinite(dt)||dt<=0)throw std::invalid_argument("Logic requires a finite sample time and positive period"); + for(auto& node:nodes)node.problem=nullptr; + // Memory outputs are visible before any combinational evaluation. Every + // memory input is then sampled before any stored value is committed. + for(std::size_t i=0;i(node.values[node.inputs[i]].value);}; + const auto bit=[&](unsigned i){return std::get(node.values[node.inputs[i]].value);}; + const auto numeric=[&](double value){emit(node,0,SignalValue::number(value,time),time);}; + const auto boolean=[&](bool value){emit(node,0,SignalValue::boolean(value,time),time);}; + if(n.kind==LogicNodeKind::Sensor){MachineSensorState sample;if(reader&&node.sensor_bound)sample=reader(n.sensor);for(unsigned i=0;i(select.value)?2:1]];node.problem=selected.validity==SignalValidity::Valid?nullptr:selected.validity==SignalValidity::Missing?"Selected input is missing":"Selected input is invalid";emit(node,0,selected,time);continue;} + SignalValidity validity=SignalValidity::Valid;for(const auto p:node.inputs){if(node.values[p].validity==SignalValidity::Fault){validity=SignalValidity::Fault;break;}if(node.values[p].validity==SignalValidity::Missing)validity=SignalValidity::Missing;} + if(validity!=SignalValidity::Valid){for(unsigned o=0;onumber(1));break; + case LogicNodeKind::Less:boolean(number(0)=n.low&&number(0)<=n.high);break; + case LogicNodeKind::Hysteresis: + if(n.below){if(number(0)<=n.low)node.hysteresis=true;else if(number(0)>=n.high)node.hysteresis=false;} + else {if(number(0)>=n.high)node.hysteresis=true;else if(number(0)<=n.low)node.hysteresis=false;} + boolean(node.hysteresis);break; + case LogicNodeKind::Add:numeric(number(0)+number(1));break; + case LogicNodeKind::Subtract:numeric(number(0)-number(1));break; + case LogicNodeKind::Multiply:numeric(number(0)*number(1));break; + case LogicNodeKind::Divide: + if(number(1)==0){node.problem="Division by zero";emit(node,0,invalid(node.ports[node.outputs[0]],SignalValidity::Fault,time),time);}else numeric(number(0)/number(1));break; + case LogicNodeKind::Minimum:numeric(std::min(number(0),number(1)));break; + case LogicNodeKind::Maximum:numeric(std::max(number(0),number(1)));break; + case LogicNodeKind::Absolute:numeric(std::abs(number(0)));break; + case LogicNodeKind::Clamp:numeric(std::clamp(number(0),n.low,n.high));break; + case LogicNodeKind::GainOffset:numeric(number(0)*n.gain+n.offset);break; + case LogicNodeKind::MapRange:{auto f=(number(0)-n.input_low)/(n.input_high-n.input_low);if(n.clamp)f=std::clamp(f,0.,1.);numeric(std::lerp(n.output_low,n.output_high,f));break;} + case LogicNodeKind::BooleanToNumber:numeric(bit(0)?n.true_value:n.false_value);break; + case LogicNodeKind::Command:case LogicNodeKind::Ecu:case LogicNodeKind::Lamp:case LogicNodeKind::Readout:break; + case LogicNodeKind::Sensor:case LogicNodeKind::Select:case LogicNodeKind::BooleanDelay:case LogicNodeKind::NumberDelay:break; + } + } + for(std::size_t i=0;ishaft!=node.ecu_shaft)node.problem="Crank pickup belongs to a different engine shaft"; + } + } + void manual(std::uint32_t id,SignalValue value){const auto found=by_id.find(id);if(found==by_id.end())throw std::invalid_argument("Unknown logic control");auto& node=nodes[found->second];const auto& definition=graph.nodes[found->second];const bool bit=definition.kind==LogicNodeKind::Switch||definition.kind==LogicNodeKind::Button; + if(!bit&&definition.kind!=LogicNodeKind::Slider&&definition.kind!=LogicNodeKind::NumberInput)throw std::invalid_argument("This node is not a manual control"); + if(!signal_value_valid_for(value,node.ports[node.outputs[0]]))throw std::invalid_argument("Manual control type or range does not match its output"); + const auto& previous=node.events.empty()?node.manual:node.events.back();if(previous.value==value.value)return; + // Numeric controls set the next boundary's value. Coalesce pointer + // motion while paused; momentary button transitions retain their queue. + if(definition.kind==LogicNodeKind::Slider||definition.kind==LogicNodeKind::NumberInput)node.events.clear(); + if(node.events.size()>=4096)throw std::invalid_argument("Too many pending logic control events"); + node.events.push_back(value); + } +}; +MachineLogicRuntime::MachineLogicRuntime(const MachineDocument& document,SensorReader reader):impl_(std::make_unique(document,std::move(reader))){} +MachineLogicRuntime::~MachineLogicRuntime()=default; +void MachineLogicRuntime::evaluate(double time,double dt){impl_->evaluate(time,dt);} +SignalValue MachineLogicRuntime::value(const LogicEndpoint& endpoint)const{const auto found=impl_->by_id.find(endpoint.node);if(found==impl_->by_id.end())throw std::out_of_range("Unknown logic node");const auto& node=impl_->nodes[found->second];for(std::size_t i=0;i MachineLogicRuntime::snapshot()const{std::vector result;result.reserve(impl_->nodes.size());for(std::size_t i=0;inodes.size();++i)result.push_back({impl_->graph.nodes[i].id,impl_->nodes[i].values,impl_->nodes[i].problem?impl_->nodes[i].problem:""});return result;} +const std::vector& MachineLogicRuntime::commands()const{return impl_->commands;} +const std::vector& MachineLogicRuntime::ecu_inputs()const{return impl_->ecu_inputs;} +void MachineLogicRuntime::set_manual(std::uint32_t node,bool value){impl_->manual(node,SignalValue::boolean(value));} +void MachineLogicRuntime::set_manual(std::uint32_t node,double value){impl_->manual(node,SignalValue::number(value));} +void MachineLogicRuntime::release_buttons(){for(std::size_t i=0;inodes.size();++i)if(impl_->graph.nodes[i].kind==LogicNodeKind::Button){auto& node=impl_->nodes[i];node.events.clear();node.events.push_back(SignalValue::boolean(false));}} +void MachineLogicRuntime::apply_tuning(const MachineDocument& document){validate_machine_logic(document);if(!logic_topology_equal(impl_->graph,document.logic))throw std::invalid_argument("Logic topology, quantities, bindings, control ranges and initial memory require Build");for(std::size_t i=0;inodes[i].ports)throw std::invalid_argument("A changed port contract requires Build");}impl_->graph=document.logic;} +} diff --git a/src/logic_serialization.cpp b/src/logic_serialization.cpp new file mode 100644 index 0000000..57c4ca1 --- /dev/null +++ b/src/logic_serialization.cpp @@ -0,0 +1,47 @@ +#include "logic_serialization.hpp" +#include +#include + +namespace fluidsim::port { +namespace { +void require(bool value,const std::string& message){if(!value)throw std::invalid_argument(message);} +std::string text(const toml::table& table,const char* key,std::string fallback){if(!table.contains(key))return fallback;const auto value=table[key].value();require(bool(value),std::string("Logic ")+key+" must be text");return *value;} +std::uint32_t id(const toml::table& table,const char* key,std::uint32_t fallback,bool zero=true){if(!table.contains(key))return fallback;const auto value=table[key].value();require(value&&*value>=(zero?0:1)&&*value<1000000000,std::string("Invalid logic ")+key);return std::uint32_t(*value);} +const char* reference_key(PressureReference reference){switch(reference){case PressureReference::None:return "none";case PressureReference::Absolute:return "absolute";case PressureReference::Gauge:return "gauge";case PressureReference::Differential:return "differential";}throw std::invalid_argument("Unknown pressure reference");} +templateT enumeration(const toml::table& table,const char* field,T fallback,int count,Key key){const auto name=text(table,field,key(fallback));for(int i=0;i>; +Numbers numbers(LogicNode& n){return {{"value",&n.value},{"minimum",&n.minimum},{"maximum",&n.maximum},{"low",&n.low},{"high",&n.high},{"tolerance",&n.tolerance},{"gain",&n.gain},{"offset",&n.offset},{"false_value",&n.false_value},{"true_value",&n.true_value},{"input_low",&n.input_low},{"input_high",&n.input_high},{"output_low",&n.output_low},{"output_high",&n.output_high}};} +using Booleans=std::vector>; +Booleans booleans(LogicNode& n){return {{"boolean",&n.boolean},{"initial",&n.initial},{"below",&n.below},{"clamp",&n.clamp}};} +} +LogicGraph read_logic_graph(const toml::table& machine){ + LogicGraph graph;if(!machine.contains("logic"))return graph;const auto logic=machine["logic"].as_table();require(logic,"Logic must be a table"); + if(logic->contains("nodes")){const auto nodes=(*logic)["nodes"].as_array();require(nodes&&nodes->size()<=2048,"Invalid logic node array"); + for(const auto& item:*nodes){const auto table=item.as_table();require(table,"Logic node must be a table");LogicNode n;n.id=id(*table,"id",0,false);require(n.id>0,"Logic node needs an ID"); + n.kind=enumeration(*table,"kind",n.kind,logic_node_kind_count,logic_node_key);n.name=text(*table,"name",logic_node_name(n.kind)); + n.quantity=enumeration(*table,"quantity",n.quantity,int(SignalQuantity::Inertia)+1,signal_quantity_key);n.output_quantity=enumeration(*table,"output_quantity",n.output_quantity,int(SignalQuantity::Inertia)+1,signal_quantity_key); + n.pressure_reference=enumeration(*table,"pressure_reference",n.pressure_reference,4,reference_key);n.output_pressure_reference=enumeration(*table,"output_pressure_reference",n.output_pressure_reference,4,reference_key); + n.command=enumeration(*table,"command",n.command,logic_command_count,logic_command_key);n.sensor_kind=enumeration(*table,"sensor_kind",n.sensor_kind,machine_sensor_kind_count,machine_sensor_key); + n.part=id(*table,"part",0);n.sensor=id(*table,"sensor",0);n.binding_label=text(*table,"binding_label",{});n.input_count=id(*table,"input_count",2); + n.ecu_strategy=sensor_ecu_strategy_from_key(text(*table,"ecu_strategy","ignition")); + for(auto [key,target]:numbers(n))if(table->contains(key)){const auto value=(*table)[key].value();require(bool(value),std::string("Logic ")+key+" must be numeric");*target=*value;} + for(auto [key,target]:booleans(n))if(table->contains(key)){const auto value=(*table)[key].value();require(bool(value),std::string("Logic ")+key+" must be Boolean");*target=*value;} + if(table->contains("position")){const auto position=(*table)["position"].as_array();require(position&&position->size()==2,"Logic position needs two coordinates");const auto x=(*position)[0].value(),y=(*position)[1].value();require(x&&y,"Logic position must be numeric");n.position={float(*x),float(*y)};} + graph.next_id=std::max(graph.next_id,n.id+1);graph.nodes.push_back(std::move(n)); + } + } + if(logic->contains("wires")){const auto wires=(*logic)["wires"].as_array();require(wires&&wires->size()<=8192,"Invalid logic wire array");for(const auto& item:*wires){const auto t=item.as_table();require(t,"Logic wire must be a table");graph.links.push_back({{id(*t,"from_node",0,false),text(*t,"from_port",{})},{id(*t,"to_node",0,false),text(*t,"to_port",{})}});}} + const auto next=id(*logic,"next_id",graph.next_id,false);require(next>=graph.next_id,"Invalid next logic ID");graph.next_id=next;validate_logic_graph(graph);return graph; +} +toml::table write_logic_graph(const LogicGraph& graph){ + validate_logic_graph(graph);toml::table result{{"next_id",int64_t(graph.next_id)}};toml::array nodes,wires; + for(auto n:graph.nodes){toml::table t{{"id",int64_t(n.id)},{"kind",logic_node_key(n.kind)},{"name",n.name},{"position",toml::array{double(n.position.x),double(n.position.y)}},{"quantity",signal_quantity_key(n.quantity)},{"pressure_reference",reference_key(n.pressure_reference)},{"output_quantity",signal_quantity_key(n.output_quantity)},{"output_pressure_reference",reference_key(n.output_pressure_reference)},{"sensor",int64_t(n.sensor)},{"sensor_kind",machine_sensor_key(n.sensor_kind)},{"part",int64_t(n.part)},{"command",logic_command_key(n.command)},{"binding_label",n.binding_label},{"input_count",int64_t(n.input_count)}}; + for(auto [key,value]:numbers(n))t.insert(key,*value); + t.insert("ecu_strategy",sensor_ecu_strategy_key(n.ecu_strategy)); + for(auto [key,value]:booleans(n))t.insert(key,*value); + nodes.push_back(std::move(t)); + } + for(const auto& wire:graph.links)wires.push_back(toml::table{{"from_node",int64_t(wire.from.node)},{"from_port",wire.from.port},{"to_node",int64_t(wire.to.node)},{"to_port",wire.to.port}}); + result.insert("nodes",std::move(nodes));result.insert("wires",std::move(wires));return result; +} +} diff --git a/src/logic_serialization.hpp b/src/logic_serialization.hpp new file mode 100644 index 0000000..67ee9e7 --- /dev/null +++ b/src/logic_serialization.hpp @@ -0,0 +1,7 @@ +#pragma once +#include "FluidSim/MachineLogic.hpp" +#include +namespace fluidsim::port { +LogicGraph read_logic_graph(const toml::table& machine); +toml::table write_logic_graph(const LogicGraph&); +} diff --git a/src/machine.cpp b/src/machine.cpp new file mode 100644 index 0000000..bb9e491 --- /dev/null +++ b/src/machine.cpp @@ -0,0 +1,621 @@ +#include "FluidSim/Machine.hpp" +#include "logic_serialization.hpp" +#include "logic_assembly.hpp" +#include "FluidSim/Constants.hpp" +#include "FluidSim/Fluid.hpp" +#include "FluidSim/PipeProfile.hpp" +#include "FluidSim/Solver.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fluidsim::port { +namespace machine_math { +Vec3 add(Vec3 a,Vec3 b){return {a.x+b.x,a.y+b.y,a.z+b.z};} +Vec3 subtract(Vec3 a,Vec3 b){return {a.x-b.x,a.y-b.y,a.z-b.z};} +Vec3 multiply(Vec3 a,float s){return {a.x*s,a.y*s,a.z*s};} +float dot(Vec3 a,Vec3 b){return a.x*b.x+a.y*b.y+a.z*b.z;} +Vec3 cross(Vec3 a,Vec3 b){return {a.y*b.z-a.z*b.y,a.z*b.x-a.x*b.z,a.x*b.y-a.y*b.x};} +float length(Vec3 a){return std::sqrt(dot(a,a));} +Vec3 unit(Vec3 a){const float l=length(a);return l>1e-9f?multiply(a,1/l):Vec3{1,0,0};} +Vec3 rotate(Vec3 v,Vec3 r){ + r=multiply(r,pi/180);float c=std::cos(r.x),s=std::sin(r.x);v={v.x,c*v.y-s*v.z,s*v.y+c*v.z}; + c=std::cos(r.y);s=std::sin(r.y);v={c*v.x+s*v.z,v.y,-s*v.x+c*v.z}; + c=std::cos(r.z);s=std::sin(r.z);return {c*v.x-s*v.y,s*v.x+c*v.y,v.z}; +} +Vec3 inverse_rotate(Vec3 v,Vec3 r){ + r=multiply(r,-pi/180);float c=std::cos(r.z),s=std::sin(r.z);v={c*v.x-s*v.y,s*v.x+c*v.y,v.z}; + c=std::cos(r.y);s=std::sin(r.y);v={c*v.x+s*v.z,v.y,-s*v.x+c*v.z}; + c=std::cos(r.x);s=std::sin(r.x);return {v.x,c*v.y-s*v.z,s*v.y+c*v.z}; +} +Vec3 rotate_euler_world(Vec3 e,unsigned axis,float degrees){ + const Vec3 a=axis==0?Vec3{1,0,0}:axis==1?Vec3{0,1,0}:Vec3{0,0,1};const float c=std::cos(degrees*pi/180),s=std::sin(degrees*pi/180); + const auto turn=[&](Vec3 v){return add(add(multiply(v,c),multiply(cross(a,v),s)),multiply(a,dot(a,v)*(1-c)));}; + const auto x=turn(rotate({1,0,0},e)),y=turn(rotate({0,1,0},e)),z=turn(rotate({0,0,1},e));const float pitch=std::asin(std::clamp(-x.z,-1.f,1.f)); + if(std::abs(std::cos(pitch))<1e-5f)return {0,pitch*180/pi,std::atan2(-y.x,y.y)*180/pi}; + return {std::atan2(y.z,z.z)*180/pi,pitch*180/pi,std::atan2(x.y,x.x)*180/pi}; +} +} +using namespace machine_math; +namespace { +constexpr const char* keys[]{"pipe","chamber","junction","throttle","check_valve","cylinder","piston_pump","reservoir","cap","carburetor","ecu","shaft","flywheel","drive_motor","brake"}; +constexpr const char* names[]{"Pipe / reducer","Chamber / tank","Junction","Throttle valve","Check valve","Engine cylinder","Piston pump","Pressure / atmosphere","End cap","Carburetor","ECU / fuel injection","Shaft / crankshaft","Flywheel","Drive motor","Brake / load"}; +static_assert(std::size(keys)==machine_part_kind_count&&std::size(names)==machine_part_kind_count,"Every machine part needs a stable key and name"); +void require(bool condition,const std::string& message){if(!condition)throw std::invalid_argument(message);} +bool finite(Vec3 v){return std::isfinite(v.x)&&std::isfinite(v.y)&&std::isfinite(v.z);} +bool range(float v,float lo,float hi){return std::isfinite(v)&&v>=lo&&v<=hi;} +bool cylinder_kind(MachinePartKind k){return k==MachinePartKind::Cylinder||k==MachinePartKind::PistonPump;} +bool occupied(const MachineDocument& d,MachinePort e){return std::any_of(d.links.begin(),d.links.end(),[&](auto& l){return l.a==e||l.b==e;});} +toml::array xyz(Vec3 v){return toml::array{double(v.x),double(v.y),double(v.z)};} +Vec3 read_xyz(const toml::node_view& n,Vec3 fallback={}){ + if(!n)return fallback; + auto a=n.as_array();require(a&&a->size()==3,"Expected a vector of three numbers"); + Vec3 v;float* values[]{&v.x,&v.y,&v.z}; + for(int i=0;i<3;++i){const auto value=(*a)[i].value();require(bool(value),"Vector needs numeric coordinates");*values[i]=float(*value);} + return v; +} +float read_number(const toml::table& t,const char* key,float fallback){ + const auto node=t[key];if(!node)return fallback;auto n=node.value();require(bool(n),std::string(key)+" must be numeric");return float(*n); +} +unsigned read_id(const toml::node_view& node){auto n=node.value();require(n&&*n>0&&*n<1000000000,"Invalid part ID");return unsigned(*n);} +MachinePartKind read_kind(const std::string& k){for(int i=0;i>; +Numbers ecu_values(EcuConfig& e){return {{"idle_rpm",&e.idle_rpm},{"idle_base_opening",&e.idle_base_opening},{"idle_max_opening",&e.idle_max_opening},{"idle_kp",&e.idle_kp},{"idle_ki",&e.idle_ki},{"throttle_time_constant",&e.throttle_time_constant},{"throttle_rate",&e.throttle_rate},{"throttle_closing_time_constant",&e.throttle_closing_time_constant},{"throttle_closing_rate",&e.throttle_closing_rate},{"rev_limit_rpm",&e.rev_limit_rpm},{"rev_resume_rpm",&e.rev_resume_rpm},{"rev_limit_cut_time",&e.rev_limit_cut_time},{"overrun_cut_rpm",&e.overrun_cut_rpm},{"overrun_resume_rpm",&e.overrun_resume_rpm},{"idle_spark_gain",&e.idle_spark_gain},{"idle_minimum_advance",&e.idle_minimum_advance},{"stoichiometric_afr",&e.stoichiometric_afr},{"power_afr",&e.power_afr}};} +Numbers carb_values(CarburetorConfig& c){return {{"fuel_density",&c.fuel_density},{"pilot_jet_diameter",&c.pilot_jet_diameter},{"main_jet_diameter",&c.main_jet_diameter},{"needle_jet_diameter",&c.needle_jet_diameter},{"needle_low_scale",&c.needle_low_scale},{"needle_mid_scale",&c.needle_mid_scale},{"needle_off_idle_scale",&c.needle_off_idle_scale},{"needle_transition_scale",&c.needle_transition_scale},{"needle_tip_scale",&c.needle_tip_scale},{"needle_quarter_scale",&c.needle_quarter_scale},{"needle_upper_scale",&c.needle_upper_scale},{"pilot_transition_scale",&c.pilot_transition_scale},{"pilot_tip_scale",&c.pilot_tip_scale},{"pilot_discharge_coefficient",&c.pilot_discharge_coefficient},{"main_discharge_coefficient",&c.main_discharge_coefficient},{"bowl_head_pressure",&c.bowl_head_pressure},{"film_fraction",&c.film_fraction},{"film_time_constant",&c.film_time_constant},{"bowl_capacity",&c.bowl_capacity},{"bowl_refill_rate",&c.bowl_refill_rate},{"overrun_rpm",&c.overrun_rpm},{"overrun_exit_rpm",&c.overrun_exit_rpm}};} +Numbers valve_values(FourStrokeCylinder::ValveDynamics& v){return {{"mass_kg",&v.mass_kg},{"spring_rate",&v.spring_rate_n_per_m},{"damping",&v.damping_n_s_per_m},{"seat_force",&v.seat_force_n},{"maximum_lift",&v.maximum_lift_m},{"seat_restitution",&v.seat_restitution},{"cam_restitution",&v.cam_restitution},{"stop_restitution",&v.stop_restitution}};} +void read_numbers(const toml::table& t,const Numbers& numbers){for(auto [key,value]:numbers)*value=read_number(t,key,*value);} +toml::table write_numbers(const Numbers& numbers){toml::table t;for(auto [key,value]:numbers)t.insert(key,double(*value));return t;} +FourStrokeCylinder::CamProfile read_cam(const toml::array& points){FourStrokeCylinder::CamProfile profile;require(points.size()<=2048,"Too many cam points");for(auto& point:points){auto pair=point.as_array();require(pair&&pair->size()==2,"Cam point needs angle and lift");auto angle=(*pair)[0].value(),lift=(*pair)[1].value();require(angle&&lift,"Cam point must be numeric");profile.points.push_back({float(*angle),float(*lift)});}return profile;} +toml::array write_cam(const FourStrokeCylinder::CamProfile& c){toml::array points;for(auto p:c.points)points.push_back(toml::array{double(p.angle_degrees),double(p.lift_metres)});return points;} +} +const char* machine_part_name(MachinePartKind k){require(int(k)>=0&&int(k)=0&&int(k)part==id){if(const auto host=find(id))tap->label=host->name;tap->part=0;} + for(auto& node:logic.nodes)if((node.kind==LogicNodeKind::Command||node.kind==LogicNodeKind::Ecu)&&node.part==id){if(const auto host=find(id))node.binding_label=host->name;node.part=0;} + for(auto& part:parts)if(part.injector&&part.injector->ecu==id){if(const auto host=find(id))part.injector->ecu_label=host->name;part.injector->ecu=0;} + if(primary_shaft==id)primary_shaft=0; + std::erase_if(links,[&](const auto& l){return l.a.part==id||l.b.part==id;});std::erase_if(probes,[&](const auto& p){return p.part==id;});std::erase_if(parts,[&](auto& p){return p.id==id;}); +} +const char* machine_quantity_name(MachineQuantity q){static const char* names[]{"Pressure","Temperature","Mass flow","Bulk AFR","Lambda","Charge AFR (pre-ignition)","Shaft RPM","Shaft torque","Shaft power"};return names[int(q)];} +const char* machine_reading_state_name(MachineReadingState state){switch(state){case MachineReadingState::Valid:return "Valid";case MachineReadingState::NoFuel:return "No fuel detected";case MachineReadingState::LeanLimit:return "Very lean / trace fuel (>1000 AFR)";case MachineReadingState::FuelCut:return "Injection fuel cut";case MachineReadingState::AwaitingCharge:return "Awaiting pre-ignition charge";}return "Unavailable";} +const char* machine_quantity_unit(MachineQuantity q){static const char* units[]{"kPa abs","K","g/s","AFR","lambda","AFR","RPM","Nm","kW"};return units[int(q)];} +float machine_quantity_scale(MachineQuantity q){return q==MachineQuantity::Pressure||q==MachineQuantity::Power?.001f:q==MachineQuantity::MassFlow?1000.f:1.f;} +bool machine_quantity_supported(const MachinePart& part,MachineQuantity q){ + const auto kind=part.kind; + if(q>=MachineQuantity::Rpm)return machine_rotational_part(kind)||kind==MachinePartKind::Cylinder||kind==MachinePartKind::PistonPump; + if(machine_rotational_part(kind))return false; + if(q==MachineQuantity::ChargeAfr)return kind==MachinePartKind::Cylinder; + if(kind==MachinePartKind::Ecu||kind==MachinePartKind::Cap)return false; + if(kind==MachinePartKind::Junction)return part.junction_method==0; + if(q==MachineQuantity::Pressure||q==MachineQuantity::Temperature)return true; + if(q==MachineQuantity::MassFlow)return kind==MachinePartKind::Pipe||kind==MachinePartKind::Cylinder||kind==MachinePartKind::PistonPump||kind==MachinePartKind::Chamber||kind==MachinePartKind::Throttle||kind==MachinePartKind::CheckValve||kind==MachinePartKind::Carburetor; + return kind==MachinePartKind::Pipe||kind==MachinePartKind::Cylinder||kind==MachinePartKind::Carburetor||kind==MachinePartKind::Chamber; +} +void MachineDocument::disconnect(MachinePort p){std::erase_if(links,[&](auto& l){return l.a==p||l.b==p;});} +Vec3 machine_world_point(const MachinePart& p,Vec3 local){return add(p.position,rotate(local,p.rotation));} +Vec3 machine_local_point(const MachinePart& p,Vec3 world){return inverse_rotate(subtract(world,p.position),p.rotation);} +float machine_pipe_length(const MachinePart& p){float l=0;for(std::size_t i=1;i machine_pipe_diameters(const MachinePart& p){ + if(!p.point_diameters.empty()){require(p.point_diameters.size()==p.points.size(),"Pipe needs one diameter per point");return p.point_diameters;} + std::vector result;result.reserve(p.points.size());const float total=machine_pipe_length(p);float at=0; + for(std::size_t i=0;i0?at/total:0.f));}return result; +} +std::vector machine_pipe_sections(const MachinePart& p){ + const auto diameters=machine_pipe_diameters(p);std::vector result; + for(std::size_t i=1;i{};const auto& diameters=p.point_diameters.empty()?legacy:p.point_diameters;require(!diameters.empty(),"Pipe has no points");distance=std::max(0.f,distance); + for(std::size_t i=1;i0?distance/span:0.f);distance-=span;}return diameters.back(); +} +void machine_insert_pipe_point(MachinePart& p,std::size_t segment,Vec3 point){ + require(p.kind==MachinePartKind::Pipe&&segment+10,"Invalid pipe segment"); + const float t=std::clamp(dot(subtract(point,p.points[segment]),edge)/squared,0.f,1.f); + diameters.insert(diameters.begin()+segment+1,std::lerp(diameters[segment],diameters[segment+1],t)); + p.points.insert(p.points.begin()+segment+1,point);p.point_diameters=std::move(diameters); +} +void machine_remove_pipe_point(MachinePart& p,std::size_t point){ + require(p.kind==MachinePartKind::Pipe&&point>0&&point+1 machine_pipe_path(const MachinePart& p){std::vector v;for(auto point:p.points)v.push_back(machine_world_point(p,point));return v;} +Vec3 machine_part_size(const MachinePart& p){ + if(cylinder_kind(p.kind))return {p.cylinder.bore,p.cylinder.stroke*p.cylinder.compression/(p.cylinder.compression-1),p.cylinder.bore}; + return p.size; +} +float machine_part_volume(const MachinePart& p){ + if(machine_rotational_part(p.kind))return 0; + if(p.kind==MachinePartKind::Pipe){double volume=0;for(const auto& s:machine_pipe_sections(p))volume+=double(pi)/12*s.length*(double(s.inlet_diameter)*s.inlet_diameter+double(s.inlet_diameter)*s.outlet_diameter+double(s.outlet_diameter)*s.outlet_diameter);return float(volume);} + if(cylinder_kind(p.kind))return pi*.25f*p.cylinder.bore*p.cylinder.bore*machine_part_size(p).y; + if(p.kind==MachinePartKind::Carburetor||p.kind==MachinePartKind::Throttle)return pi*.25f*p.size.x*p.size.y*p.size.z; + if((p.kind==MachinePartKind::Junction&&p.junction_method)||p.kind==MachinePartKind::Cap||p.kind==MachinePartKind::Reservoir||p.kind==MachinePartKind::Ecu)return 0; + require(p.kind==MachinePartKind::Chamber||p.kind==MachinePartKind::Junction||p.kind==MachinePartKind::CheckValve,"Gas storage is not declared for this part kind"); + return p.size.x*p.size.y*p.size.z; +} +unsigned machine_port_count(const MachinePart& p){ + if(p.kind==MachinePartKind::Junction&&p.junction_method)return 1; + switch(p.kind){case MachinePartKind::Pipe:case MachinePartKind::CheckValve:return 2; + case MachinePartKind::PistonPump:return 3; + case MachinePartKind::Cylinder:return 4; + case MachinePartKind::Throttle:case MachinePartKind::Carburetor:return 3; + case MachinePartKind::Chamber:case MachinePartKind::Junction:return p.port_coordinates.empty()?6:unsigned(p.port_coordinates.size()); + case MachinePartKind::Reservoir:case MachinePartKind::Cap:case MachinePartKind::Ecu:case MachinePartKind::Shaft:case MachinePartKind::Flywheel:case MachinePartKind::DriveMotor:case MachinePartKind::Brake:return 1;} + throw std::invalid_argument("Port definitions are not implemented for this part kind"); +} +bool machine_control_port(const MachinePart& p,unsigned port){return machine_port_descriptor(p,port).domain==PortDomain::EcuAssignment;} +bool machine_port_available(const MachineDocument& d,MachinePort port){const auto p=d.find(port.part);return p&&port.port=machine_port_count(p))throw std::invalid_argument("Invalid machine port"); + if(machine_port_descriptor(p,port).domain==PortDomain::Rotational){if(cylinder_kind(p.kind))return {0,-machine_part_size(p).y*.5f-.025f,0};return {p.kind==MachinePartKind::Shaft?p.size.x*.5f:-p.size.x*.5f,0,0};} + if(p.kind==MachinePartKind::Pipe)return port?p.points.back():p.points.front(); + if(machine_control_port(p,port))return p.kind==MachinePartKind::Ecu?Vec3{p.size.x*.5f,0,0}:Vec3{0,machine_part_size(p).y*.5f+.025f,0}; + if(p.kind==MachinePartKind::Reservoir||p.kind==MachinePartKind::Cap||(p.kind==MachinePartKind::Junction&&p.junction_method))return {}; + const auto s=machine_part_size(p); + if(portkind!=MachinePartKind::Shaft){std::swap(a,b);std::swap(pa,pb);} + require(pa->kind==MachinePartKind::Shaft&&pb->kind!=MachinePartKind::Shaft,"Attach a component to one shaft; use a coupling between shafts"); + require(machine_port_available(*this,b),"This component already has a shaft");links.push_back({a,b,MachineConnectionKind::Mechanical});return; + } + + + if(machine_control_port(*pa,a.port)||machine_control_port(*pb,b.port)){ + if(pa->kind!=MachinePartKind::Ecu){std::swap(a,b);std::swap(pa,pb);} + require(pa->kind==MachinePartKind::Ecu&&pb->kind!=MachinePartKind::Ecu&&machine_control_port(*pb,b.port),"Connect an ECU output to a cylinder, throttle, or carburetor ECU input"); + require(machine_port_available(*this,b),"This part already has an ECU assigned");links.push_back({a,b,MachineConnectionKind::Control});return; + } + require(int(kind)>=0&&int(kind)<4,"Gas ports require a gas connection method"); + if(pa->kind!=MachinePartKind::Pipe){std::swap(a,b);std::swap(pa,pb);} + require(pa->kind==MachinePartKind::Pipe,"Connect air parts using a pipe"); + require(!occupied(*this,a),"Start with a free pipe endpoint"); + if(!machine_port_available(*this,b)){ + require(pb->kind==MachinePartKind::Pipe,"Use a junction to branch this occupied component port"); + const auto old=std::find_if(links.begin(),links.end(),[&](auto& l){return l.a==b||l.b==b;});require(old!=links.end(),"Invalid occupied port"); + const auto other=old->a==b?old->b:old->a;require(find(other.part)->kind==MachinePartKind::Pipe,"Branch at a junction or at a pipe-to-pipe connection"); + const auto location=machine_port_position(*pb,b.port);auto proposed=pa->points;proposed[a.port?proposed.size()-1:0]=machine_local_point(*pa,location); + for(std::size_t i=1;i=.001f,"Connection would collapse a pipe segment"); + pa->points=std::move(proposed);const auto old_kind=old->kind;const float loss=old->loss;links.erase(old); + auto& node=add(MachinePartKind::Junction,location);node.junction_method=old_kind==MachineConnectionKind::Legacy?2:1;node.turning_loss=loss;const auto node_id=node.id; + links.push_back({b,{node_id,0}});links.push_back({other,{node_id,0}});links.push_back({a,{node_id,0}});return; + } + require(pa->kind==MachinePartKind::Pipe||pb->kind==MachinePartKind::Pipe,"Connect parts using a pipe"); + if(pa->kind!=MachinePartKind::Pipe){std::swap(a,b);std::swap(pa,pb);} + if(pa->kind!=MachinePartKind::Pipe||pb->kind!=MachinePartKind::Pipe) + require(kind==MachineConnectionKind::Orifice||kind==MachineConnectionKind::Inertance,"Use an orifice or inertance for a component port"); + else if(kind==MachineConnectionKind::Inertance)throw std::invalid_argument("Pipe-to-pipe connections use an ideal junction"); + auto proposed=pa->points;proposed[a.port?proposed.size()-1:0]=machine_local_point(*pa,machine_port_position(*pb,b.port)); + for(std::size_t i=1;i=.001f,"Connection collapses a pipe segment; move the pipe first"); + links.push_back({a,b,kind});pa->points=std::move(proposed); +} +void MachineDocument::sync_connections(std::uint32_t moved){ + for(auto& l:links){if(machine_link_domain(*this,l)!=PortDomain::Gas||(moved&&l.a.part!=moved&&l.b.part!=moved))continue;auto a=find(l.a.part),b=find(l.b.part);if(!a||!b)continue; + if(a->kind!=MachinePartKind::Pipe){std::swap(a,b);std::swap(l.a,l.b);} + // Non-pipe parts anchor the endpoint. For two pipes, the moved pipe drives its neighbor. + if(b->kind==MachinePartKind::Pipe&&a->id==moved){b->points[l.b.port?b->points.size()-1:0]=machine_local_point(*b,machine_port_position(*a,l.a.port));} + else a->points[l.a.port?a->points.size()-1:0]=machine_local_point(*a,machine_port_position(*b,l.b.port)); + } +} +void MachineDocument::transform_selection(const std::vector& ids,Vec3 pivot,Vec3 translation,unsigned axis,float degrees){ + const Vec3 r=axis==0?Vec3{degrees,0,0}:axis==1?Vec3{0,degrees,0}:Vec3{0,0,degrees}; + for(auto id:ids)if(auto p=find(id)){p->position=machine_math::add(machine_math::add(pivot,rotate(subtract(p->position,pivot),r)),translation);if(degrees!=0)p->rotation=rotate_euler_world(p->rotation,axis,degrees);} + const auto chosen=[&](unsigned id){return std::find(ids.begin(),ids.end(),id)!=ids.end();}; + for(auto& l:links){if(machine_link_domain(*this,l)!=PortDomain::Gas||(!chosen(l.a.part)&&!chosen(l.b.part)))continue;auto a=find(l.a.part),b=find(l.b.part);if(a->kind!=MachinePartKind::Pipe){std::swap(a,b);std::swap(l.a,l.b);} + if(b->kind==MachinePartKind::Pipe&&chosen(a->id)&&!chosen(b->id))b->points[l.b.port?b->points.size()-1:0]=machine_local_point(*b,machine_port_position(*a,l.a.port)); + else a->points[l.a.port?a->points.size()-1:0]=machine_local_point(*a,machine_port_position(*b,l.b.port)); + } +} +void validate_machine(const MachineDocument& d){ + require(d.probes.size()<=machine_probe_limit,"A machine supports at most 8 measurement channels");std::set probe_ids; + for(const auto& probe:d.probes){require(probe.id&&probe_ids.insert(probe.id).second,"Invalid or duplicate probe ID");require(!probe.name.empty()&&probe.name.size()<=120,"Probe name must have 1..120 characters");require(int(probe.quantity)>=0&&int(probe.quantity)<=8&&range(probe.position,0,1)&&range(probe.reference_afr,1,50),"Invalid probe quantity or position"); + require(!probe.generated_output||probe.quantity==MachineQuantity::Torque||probe.quantity==MachineQuantity::Power,"Generated-output mode applies only to torque and power"); + if(probe.generated_output&&probe.part){const auto target=d.find(probe.part);require(target&&target->kind==MachinePartKind::Shaft,"Generated-output gauge needs a shaft target");} + if(probe.quantity>=MachineQuantity::Rpm){if(probe.part){const auto part=d.find(probe.part);require(part&&machine_quantity_supported(*part,probe.quantity),"Rotational instrument needs a compatible target");}else require(machine_primary_shaft(d)||std::none_of(d.parts.begin(),d.parts.end(),[](const auto& p){return p.kind==MachinePartKind::Shaft;}),"Choose a target for a rotational instrument");}else{const auto part=d.find(probe.part);require(part&&machine_quantity_supported(*part,probe.quantity),"Probe refers to a missing or incompatible part");}} + require(d.parts.size()<=512&&d.links.size()<=2048,"Machine exceeds 512 parts / 2048 connections"); + require(!d.name.empty()&&d.name.size()<=120,"Machine name must have 1..120 characters"); + require(range(d.cells_per_meter,1,500)&&range(d.drive_rpm,0,50000)&&range(d.shaft_inertia,.001f,100)&&range(d.load_torque,0,10000),"Invalid simulation settings"); + require(range(d.friction_constant,0,1000)&&range(d.friction_viscous,0,100)&&range(d.friction_quadratic,0,10)&&range(d.starter_torque,0,1000),"Invalid shaft friction or starter torque"); + require(int(d.solver)>=0&&int(d.solver)<4&&int(d.scene)>=0&&int(d.scene)<5,"Invalid solver or scene"); + if(d.primary_shaft){const auto part=d.find(d.primary_shaft);require(part&&part->kind==MachinePartKind::Shaft,"Primary readout refers to a missing shaft");} + std::set ids;std::set> used;unsigned cells=0; + for(auto& p:d.parts){ + const auto prefix=p.name+": ";require(p.id&&p.id<1000000000&&ids.insert(p.id).second,"Duplicate or invalid part ID"); + require(int(p.kind)>=0&&int(p.kind)=0&&p.junction_method<=2,prefix+"invalid junction method"); + if(p.kind==MachinePartKind::Carburetor||p.kind==MachinePartKind::Throttle||p.kind==MachinePartKind::CheckValve)require(p.diameter<=std::min(p.size.y,p.size.z)+1e-6f,prefix+"throat diameter exceeds body dimensions; enlarge the body first"); + const bool custom_ports=(p.kind==MachinePartKind::Chamber||(p.kind==MachinePartKind::Junction&&!p.junction_method))&&!p.port_coordinates.empty(); + require(p.port_keys.empty()||(custom_ports&&p.port_keys.size()==p.port_coordinates.size()),prefix+"custom port keys must match port coordinates"); + std::set port_keys;for(unsigned index=0;index=1&&c.cylinders<=64&&range(c.maximum_flow,.000000001f,1)&&range(c.initial_mass,0,1000)&&range(c.temperature,100,2000),prefix+"invalid injector supply or capacity");} + if(p.kind==MachinePartKind::Ecu){const auto& e=p.ecu; + if(p.sensor_ecu.enabled){SensorEcu controller(machine_sensor_ecu_config(p));} + require(range(e.idle_rpm,0,30000)&&range(e.idle_base_opening,0,1)&&range(e.idle_max_opening,e.idle_base_opening,1)&&range(e.idle_kp,0,1)&&range(e.idle_ki,0,1),prefix+"invalid idle controller"); + require(range(e.rev_limit_rpm,100,50000)&&range(e.rev_resume_rpm,0,e.rev_limit_rpm-1)&&range(e.rev_limit_cut_time,0,10),prefix+"invalid rev limiter settings"); + require(range(e.stoichiometric_afr,5,50)&&range(e.power_afr,5,50)&&range(e.throttle_time_constant,.00001f,10)&&range(e.throttle_rate,.001f,100000)&&range(e.throttle_closing_time_constant,0,10)&&range(e.throttle_closing_rate,0,100000),prefix+"invalid fuel or throttle actuator settings"); + require(range(e.overrun_resume_rpm,0,e.overrun_cut_rpm)&&range(e.overrun_cut_rpm,0,50000)&&range(e.idle_spark_gain,0,1)&&range(e.idle_minimum_advance,-90,90),prefix+"invalid overrun / idle ignition settings"); + require(!e.ignition_map.empty()&&e.ignition_map.size()<=128,prefix+"ignition map needs 1..128 points");float previous=-1;for(auto point:e.ignition_map){require(range(point.rpm,0,100000)&&point.rpm>previous&&range(point.degrees_btdc,-60,90),prefix+"invalid ignition map");previous=point.rpm;} + } + if(p.kind==MachinePartKind::Carburetor){const auto& c=p.carburetor; + require(range(c.pilot_jet_diameter,.000001f,.02f)&&range(c.main_jet_diameter,.000001f,.02f)&&range(c.needle_jet_diameter,.000001f,.02f)&&range(c.fuel_density,100,2000),prefix+"invalid fuel density or jets"); + require(range(c.bowl_capacity,.000001f,10)&&range(c.bowl_refill_rate,0,10)&&range(c.film_fraction,0,1)&&range(c.film_time_constant,.00001f,10)&&range(c.bowl_head_pressure,0,100000),prefix+"invalid float bowl or fuel film"); + require(range(c.pilot_discharge_coefficient,0,1)&&range(c.main_discharge_coefficient,0,1),prefix+"invalid jet discharge coefficient"); + auto copy=c;for(auto [key,value]:carb_values(copy))require(std::isfinite(*value)&&*value>=0&&*value<=100000,prefix+"invalid carburetor parameter "+key); + require(c.overrun_exit_rpm<=c.overrun_rpm,prefix+"overrun resume must not exceed cut RPM"); + } + if(p.kind==MachinePartKind::Pipe){ + require(p.points.size()>=2&&p.points.size()<=128,prefix+"pipe needs 2..128 path points"); + require(p.point_diameters.empty()||p.point_diameters.size()==p.points.size(),prefix+"pipe needs one diameter per point"); + for(float diameter:p.point_diameters)require(range(diameter,.001f,2),prefix+"point diameters must be 1..2000 mm"); + for(std::size_t i=0;i=1&&c.intake_valve_count<=8&&c.exhaust_valve_count>=1&&c.exhaust_valve_count<=8,prefix+"invalid valve diameter/count"); + require(range(p.maximum_lift,.0001f,.05f)&&range(p.intake_open,0,719)&&range(p.intake_close,p.intake_open+1,720)&&range(p.exhaust_open,0,719)&&range(p.exhaust_close,p.exhaust_open+1,720),prefix+"invalid cam timing / lift"); + require(range(p.valve.mass_kg,.001f,1)&&range(p.valve.spring_rate_n_per_m,100,1000000)&&range(p.valve.damping_n_s_per_m,0,1000)&&range(p.valve.seat_force_n,0,10000),prefix+"invalid valve dynamics"); + require(range(c.afr,5,50)&&range(c.spark_advance,-20,60)&&range(c.heat_transfer,0,10000)&&range(c.wall_area,.00001f,10),prefix+"invalid cylinder calibration"); + require(range(c.burn_duration,1,720)&&range(c.burn_start,-60,120)&&range(c.wiebe_a,.01f,100)&&range(c.wiebe_m,0,10)&&range(c.fuel_lhv,1000,1e9f)&&range(c.energy_variation,0,1)&&range(c.misfire_probability,0,1)&&range(c.ambient_temperature,100,3000),prefix+"invalid combustion calibration"); + for(auto dynamics:{p.valve,p.exhaust_valve})require(range(dynamics.mass_kg,.001f,1)&&range(dynamics.spring_rate_n_per_m,100,1000000)&&range(dynamics.damping_n_s_per_m,0,1000)&&range(dynamics.seat_force_n,0,10000)&&range(dynamics.maximum_lift_m,.0001f,.1f)&&range(dynamics.seat_restitution,0,1)&&range(dynamics.cam_restitution,0,1)&&range(dynamics.stop_restitution,0,1),prefix+"invalid valve dynamics"); + for(auto profile:{&p.intake_profile,&p.exhaust_profile}){require(profile->points.size()<=2048,prefix+"cam has too many points");float previous=-1;for(auto point:profile->points){require(range(point.angle_degrees,0,720)&&point.angle_degrees>previous&&range(point.lift_metres,0,.05f),prefix+"invalid imported cam");previous=point.angle_degrees;}} + } + } + require(cells<=65536,"Machine exceeds 65536 pipe cells"); + validate_machine_sensors(d); + validate_machine_logic(d); + require(d.next_id>0&&d.next_id<1000000000&&(ids.empty()||d.next_id>*ids.rbegin()),"Invalid next part ID"); + for(auto& l:d.links){ + auto a=d.find(l.a.part),b=d.find(l.b.part);require(a&&b&&a!=b,"Connection refers to a missing part or itself"); + require(l.a.portkind==MachinePartKind::Shaft&&b->kind!=MachinePartKind::Shaft,"Invalid shaft attachment");continue;} + if(l.kind==MachineConnectionKind::Control){require(da.domain==PortDomain::EcuAssignment&&da.direction==PortDirection::Output&&db.direction==PortDirection::Input,"Invalid ECU control wire");require(!a->sensor_ecu.enabled||b->kind==MachinePartKind::Cylinder,"Sensor-driven ECU assignments target cylinders; use explicit throttle commands");continue;} + require(da.domain==PortDomain::Gas&&db.domain==PortDomain::Gas,"Gas methods require gas ports"); + require(a->kind==MachinePartKind::Pipe||b->kind==MachinePartKind::Pipe,"A connection must include a pipe"); + require(length(subtract(machine_port_position(*a,l.a.port),machine_port_position(*b,l.b.port)))<.0001f,"Connected ports must occupy the same position"); + require(int(l.kind)>=0&&int(l.kind)<4&&range(l.discharge,.01f,1)&&range(l.inertance_length,.0001f,10)&&range(l.loss,0,100),"Invalid connection properties"); + const bool both=a->kind==MachinePartKind::Pipe&&b->kind==MachinePartKind::Pipe; + require(both?l.kind!=MachineConnectionKind::Inertance:int(l.kind)<=1,"Connection method is incompatible with these ports"); + const auto other=a->kind==MachinePartKind::Pipe?b:a; + if(!both&&(other->kind==MachinePartKind::Reservoir||other->kind==MachinePartKind::Cap||(other->kind==MachinePartKind::Junction&&other->junction_method))) + require(l.kind==MachineConnectionKind::Orifice,"Boundary and ideal-node ports use their component's boundary method"); + if(both)require(l.loss<=10,"Junction turning loss must be in 0..10"); + } + for(const auto& shaft:d.parts)if(shaft.kind==MachinePartKind::Shaft){unsigned prescribed=0;for(const auto& part:d.parts)if(part.kind==MachinePartKind::DriveMotor&&part.drive.speed_control&&machine_shaft_for(d,part.id)==shaft.id){const bool signal_enable=std::any_of(d.logic.nodes.begin(),d.logic.nodes.end(),[&](const auto& node){return node.kind==LogicNodeKind::Command&&node.command==LogicCommand::MotorEnable&&node.part==part.id;});if(part.drive.enabled||signal_enable)++prescribed;}require(prescribed<=1,"A shaft supports only one potentially active ideal speed drive");} + +} +MachineConnectionPreview machine_connection_preview(const MachineDocument& d,MachinePort a,MachinePort b){ + MachineConnectionPreview result; + try{ + auto proposed=d;proposed.connect(a,b);validate_machine(proposed);result.allowed=true; + const auto pa=d.find(a.part),pb=d.find(b.part); + if(machine_port_descriptor(*pa,a.port).domain==PortDomain::Rotational)result.message="Mechanical attachment: component shares the named shaft"; + else if(machine_control_port(*pa,a.port)||machine_control_port(*pb,b.port))result.message="ECU control wire: injection, ignition or throttle control"; + else{ + const auto pipe=pa->kind==MachinePartKind::Pipe?pa:pb;result.pipe=*proposed.find(pipe->id); + result.message=proposed.parts.size()>d.parts.size()?"Connect and create a shared air junction":"Connect air ports; preview shows the new pipe route"; + } + }catch(const std::exception& e){result.message=e.what();} + return result; +} +bool machine_has_fuel_source(const MachineDocument& d,std::uint32_t cylinder){ + const auto target=d.find(cylinder);if(!target||target->kind!=MachinePartKind::Cylinder)return false; + for(const auto& l:d.links)if(l.kind==MachineConnectionKind::Control&&l.b.part==cylinder){const auto ecu=d.find(l.a.part);if(ecu&&!ecu->sensor_ecu.enabled&&ecu->injection_enabled)return true;} + std::vector visited{cylinder}; + for(std::size_t i=0;ikind==MachinePartKind::Carburetor)return true; + if(next->injector){const auto ecu=d.find(next->injector->ecu);if(ecu&&ecu->kind==MachinePartKind::Ecu&&ecu->sensor_ecu.enabled&&ecu->sensor_ecu.strategy!=SensorEcuConfig::Strategy::Ignition)return true;} + if(next->kind==MachinePartKind::Cylinder)continue; + if(std::find(visited.begin(),visited.end(),other.part)==visited.end())visited.push_back(other.part); + } + return false; +} +std::vector machine_checks(const MachineDocument& d){ + std::vector result; + for(const auto& sensor:d.sensors){const auto status=machine_sensor_binding(d,sensor);if(status!=MachineSensorStatus::Valid)result.push_back({sensor.target.part,true,std::string(machine_sensor_status_name(status))+": "+sensor.name,"Open Sense to bind the sensor to a compatible physical part. Unresolved outputs remain invalid.",sensor.id});} + for(const auto& node:d.logic.nodes){const auto problem=logic_binding_problem(d,node);if(!problem.empty())result.push_back({node.part,true,node.name+": "+problem,"Open Logic and repair the explicit sensor/actuator reference.",0,node.id}); + for(const auto& port:logic_node_ports(node))if(port.required&&!std::any_of(d.logic.links.begin(),d.logic.links.end(),[&](const auto& link){return link.to.node==node.id&&link.to.port==port.key;}))result.push_back({node.part,true,node.name+" / "+port.name+": unconnected","Connect a compatible signal in Logic before Test.",0,node.id}); + } + // Fuel-source reachability is a wiring/plumbing check, not a guarantee that + // fuel can pass every valve or that the engine will sustain combustion. + for(const auto& p:d.parts){ + if(((machine_rotational_part(p.kind)&&p.kind!=MachinePartKind::Shaft)||cylinder_kind(p.kind))&&!machine_shaft_for(d,p.id))result.push_back({p.id,true,"No shaft attached","Connect this part's gold mechanical port to a shaft."}); + if(p.kind==MachinePartKind::Cylinder&&!machine_has_fuel_source(d,p.id))result.push_back({p.id,true,"No fuel source", "Wire an injection ECU or pipe a carb to the intake."}); + if(p.kind==MachinePartKind::Ecu&&!std::any_of(d.links.begin(),d.links.end(),[&](const auto& l){return l.kind==MachineConnectionKind::Control&&l.a.part==p.id;}))result.push_back({p.id,true,"ECU has no assigned parts","Connect its blue output to blue part inputs."}); + if(p.kind==MachinePartKind::Ecu&&p.sensor_ecu.enabled&&!std::any_of(d.logic.nodes.begin(),d.logic.nodes.end(),[&](const auto& node){return node.kind==LogicNodeKind::Ecu&&node.part==p.id;}))result.push_back({p.id,true,"Sensor ECU has no signal connector","Use Show ECU connector in Logic, then wire its required measurements."}); + if(p.injector){const auto target=d.find(p.injector->ecu);if(!target||target->kind!=MachinePartKind::Ecu||!target->sensor_ecu.enabled||target->sensor_ecu.strategy==SensorEcuConfig::Strategy::Ignition)result.push_back({p.id,true,"Injector ECU binding is unresolved","Choose a sensor-driven EFI ECU in the chamber inspector."});} + if(p.kind==MachinePartKind::Ecu){std::set shafts;for(const auto& link:d.links)if(link.kind==MachineConnectionKind::Control&&link.a.part==p.id){const auto target=d.find(link.b.part);if(target&&target->kind==MachinePartKind::Cylinder){const auto shaft=machine_shaft_for(d,target->id);if(shaft)shafts.insert(shaft);}}if(shafts.size()>1)result.push_back({p.id,true,"ECU spans multiple shafts","Use a separate ECU for each independent engine shaft."});} + if((p.kind==MachinePartKind::Throttle||p.kind==MachinePartKind::Carburetor)&&!p.pedal_control&&p.opening<.001f&&!std::any_of(d.links.begin(),d.links.end(),[&](const auto& l){return l.kind==MachineConnectionKind::Control&&l.b.part==p.id;}))result.push_back({p.id,true,"Fixed throttle is closed","Increase opening or enable pedal control."}); + } + for(const auto& p:d.parts)for(unsigned i=0;i0?"Choose this primary shaft and hold V in Test to crank.":"Connect a drive to crank this engine."}); + } + return result; +} +std::vector machine_diagnostics(const MachineDocument& d){ + std::vector messages; + for(auto& p:d.parts)for(unsigned i=0;i machine_files(){ + std::vector result;auto directory=machine_directory(); + if(std::filesystem::is_directory(directory))for(auto& e:std::filesystem::directory_iterator(directory))if(e.is_regular_file()&&e.path().filename().string().ends_with(".machine.toml"))result.push_back(e.path()); + std::sort(result.begin(),result.end());return result; +} +std::filesystem::path machine_assembly_directory(){return machine_directory()/"assemblies";} +std::vector machine_assembly_files(){ + std::vector result;const auto directory=machine_assembly_directory(); + if(std::filesystem::is_directory(directory))for(const auto& entry:std::filesystem::directory_iterator(directory))if(entry.is_regular_file()&&entry.path().filename().string().ends_with(".assembly.toml"))result.push_back(entry.path()); + std::sort(result.begin(),result.end());return result; +} +Vec3 machine_selection_center(const MachineDocument& document,const std::vector& ids){ + Vec3 lo{1e6f,1e6f,1e6f},hi{-1e6f,-1e6f,-1e6f}; + const auto include=[&](Vec3 point){lo={std::min(lo.x,point.x),std::min(lo.y,point.y),std::min(lo.z,point.z)};hi={std::max(hi.x,point.x),std::max(hi.y,point.y),std::max(hi.z,point.z)};}; + for(const auto id:ids){const auto* part=document.find(id);require(part,"Selection contains a part that no longer exists"); + if(part->kind==MachinePartKind::Pipe){const auto points=machine_pipe_path(*part);const auto diameters=machine_pipe_diameters(*part);for(std::size_t i=0;ikind==MachinePartKind::Reservoir||part->kind==MachinePartKind::Cap||(part->kind==MachinePartKind::Junction&&part->junction_method);const auto size=symbol?Vec3{.04f,.04f,.04f}:machine_part_size(*part);for(int corner=0;corner<8;++corner)include(machine_world_point(*part,{(corner&1?1:-1)*size.x*.5f,(corner&2?1:-1)*size.y*.5f,(corner&4?1:-1)*size.z*.5f}));} + } + return ids.empty()?Vec3{}:multiply(add(lo,hi),.5f); +} +MachineDocument machine_extract_assembly(const MachineDocument& source,const std::vector& ids,std::string name){ + std::set chosen(ids.begin(),ids.end());require(!chosen.empty(),"Select at least one part to save as an assembly"); + MachineDocument assembly;assembly.name=name.empty()?"Reusable assembly":std::move(name);assembly.cells_per_meter=source.cells_per_meter; + const auto center=machine_selection_center(source,ids); + for(const auto& part:source.parts)if(chosen.contains(part.id)){assembly.parts.push_back(part);assembly.next_id=std::max(assembly.next_id,part.id+1);} + for(auto& part:assembly.parts){part.position=subtract(part.position,center);if(part.injector&&!chosen.contains(part.injector->ecu)){if(const auto ecu=source.find(part.injector->ecu))part.injector->ecu_label=ecu->name;part.injector->ecu=0;}} + for(const auto& link:source.links)if(chosen.contains(link.a.part)&&chosen.contains(link.b.part))assembly.links.push_back(link); + for(const auto& probe:source.probes)if(probe.part&&chosen.contains(probe.part)){ + auto copy=probe;copy.id=std::uint32_t(assembly.probes.size()+1);assembly.probes.push_back(std::move(copy)); + } + for(auto sensor:source.sensors)if(chosen.contains(sensor.target.part)||chosen.contains(sensor.reference.part)){ + for(auto* tap:{&sensor.target,&sensor.reference})if(!chosen.contains(tap->part)){if(const auto host=source.find(tap->part))tap->label=host->name;tap->part=0;} + assembly.next_sensor_id=std::max(assembly.next_sensor_id,sensor.id+1);assembly.sensors.push_back(std::move(sensor)); + } + if(chosen.contains(source.primary_shaft))assembly.primary_shaft=source.primary_shaft; + extract_machine_logic(source,chosen,assembly); + validate_machine(assembly);return assembly; +} +std::vector machine_insert_assembly(MachineDocument& destination,const MachineDocument& assembly,Vec3 center){ + validate_machine(assembly);require(!assembly.parts.empty(),"Assembly contains no parts");require(finite(center),"Assembly placement must be finite"); + require(destination.parts.size()+assembly.parts.size()<=512,"Assembly would exceed the 512-part machine limit"); + require(destination.links.size()+assembly.links.size()<=2048,"Assembly would exceed the 2048-connection limit"); + require(destination.probes.size()+assembly.probes.size()<=machine_probe_limit,"Remove a probe before inserting this assembly (8-channel limit)"); + auto proposed=destination;std::unordered_map mapped;std::vector inserted;inserted.reserve(assembly.parts.size()); + for(const auto& part:assembly.parts){auto copy=part;copy.id=proposed.next_id++;copy.position=add(copy.position,center);mapped.emplace(part.id,copy.id);inserted.push_back(copy.id);proposed.parts.push_back(std::move(copy));} + for(const auto id:inserted)if(auto& injector=proposed.find(id)->injector;injector)injector->ecu=mapped.contains(injector->ecu)?mapped.at(injector->ecu):0; + for(auto link:assembly.links){require(mapped.contains(link.a.part)&&mapped.contains(link.b.part),"Assembly has a connection to a missing part");link.a.part=mapped.at(link.a.part);link.b.part=mapped.at(link.b.part);proposed.links.push_back(link);} + std::uint32_t next_probe=1;for(const auto& probe:proposed.probes)next_probe=std::max(next_probe,probe.id+1); + for(auto probe:assembly.probes){require(mapped.contains(probe.part),"Assembly probe refers to a missing part");probe.id=next_probe++;probe.part=mapped.at(probe.part);proposed.probes.push_back(std::move(probe));} + std::unordered_map sensor_ids; + for(auto sensor:assembly.sensors){const auto original=sensor.id;sensor.id=proposed.next_sensor_id++;sensor_ids.emplace(original,sensor.id);for(auto* tap:{&sensor.target,&sensor.reference})tap->part=mapped.contains(tap->part)?mapped.at(tap->part):0;proposed.sensors.push_back(std::move(sensor));} + insert_machine_logic(proposed,assembly,mapped,sensor_ids); + if(!proposed.primary_shaft&&mapped.contains(assembly.primary_shaft))proposed.primary_shaft=mapped.at(assembly.primary_shaft); + validate_machine(proposed);destination=std::move(proposed);return inserted; +} + +MachineDocument load_machine(const std::filesystem::path& file){ + try{ + const auto table=toml::parse_file(file.string());const auto format=table["format"].value_or(0);require(format>=1&&format<=10,"Unsupported machine file version"); + MachineDocument d;d.name=table["name"].value_or(std::string("Untitled machine")); + d.cells_per_meter=read_number(table,"cells_per_meter",40);d.drive_rpm=read_number(table,"drive_rpm",format==1?1500.f:0.f);d.shaft_inertia=read_number(table,"shaft_inertia",.15f);d.load_torque=read_number(table,"load_torque",0);d.driven=table["driven"].value_or(format==1); + d.friction_constant=read_number(table,"friction_constant",d.friction_constant);d.friction_viscous=read_number(table,"friction_viscous",d.friction_viscous);d.friction_quadratic=read_number(table,"friction_quadratic",0);d.starter_torque=read_number(table,"starter_torque",15); + if(format>=6){const auto id=table["primary_shaft"].value_or(int64_t(0));require(id>=0&&id<1000000000,"Invalid primary shaft ID");d.primary_shaft=std::uint32_t(id);} + d.solver=parse_pipe_solver(table["solver"].value_or(std::string("rusanov")));const auto scene=table["scene"].value_or(2);require(scene>=0&&scene<5,"Invalid scene");d.scene=OutdoorScene(scene); + auto parts=table["parts"].as_array();require(parts&&parts->size()<=512,"Missing parts array or too many parts"); + for(auto& item:*parts){ + auto t=item.as_table();require(t!=nullptr,"Invalid part table");MachinePart p;p.id=read_id((*t)["id"]);p.kind=read_kind((*t)["kind"].value_or(std::string{}));p.name=(*t)["name"].value_or(std::string(machine_part_name(p.kind))); + p.position=read_xyz((*t)["position_m"],p.position);p.rotation=read_xyz((*t)["rotation_deg"]);p.size=read_xyz((*t)["size_m"],p.size); + if(auto points=(*t)["points_m"].as_array()){require(points->size()<=128,"Too many pipe points");p.points.clear();for(auto& point:*points){auto a=point.as_array();require(a&&a->size()==3,"Invalid pipe point");Vec3 v;float* values[]{&v.x,&v.y,&v.z};for(int i=0;i<3;++i){auto n=(*a)[i].value();require(bool(n),"Invalid pipe coordinate");*values[i]=float(*n);}p.points.push_back(v);}} + const auto get=[&](const char* key,float& value){value=read_number(*t,key,value);}; + get("diameter_m",p.diameter);get("outlet_diameter_m",p.outlet_diameter);get("pressure_pa",p.pressure);get("temperature_k",p.temperature);get("opening",p.opening);get("discharge",p.discharge);get("cracking_pressure_pa",p.cracking_pressure);get("phase_deg",p.phase_degrees);get("maximum_lift_m",p.maximum_lift);get("intake_open_deg",p.intake_open);get("intake_close_deg",p.intake_close);get("exhaust_open_deg",p.exhaust_open);get("exhaust_close_deg",p.exhaust_close);get("turning_loss",p.turning_loss); + if(p.kind==MachinePartKind::Pipe){const auto values=(*t)["point_diameters_m"].as_array();require(format<3||values,"Pipe needs point_diameters_m in version 3"); + if((*t).contains("point_diameters_m")){require(values&&values->size()==p.points.size(),"Pipe needs one diameter per point");for(const auto& value:*values){const auto number=value.value();require(bool(number),"Invalid pipe point diameter");p.point_diameters.push_back(float(*number));}} + } + p.pedal_control=(*t)["pedal_control"].value_or(true);p.junction_method=(*t)["junction_method"].value_or(0); + auto& c=p.cylinder;get("bore_m",c.bore);get("stroke_m",c.stroke);get("rod_m",c.rod);get("compression",c.compression);get("intake_valve_m",c.intake_valve_diameter);get("exhaust_valve_m",c.exhaust_valve_diameter);get("afr",c.afr);get("spark_advance_deg",c.spark_advance);get("heat_transfer",c.heat_transfer);get("wall_area_m2",c.wall_area); + c.intake_valve_count=(*t)["intake_valve_count"].value_or(1);c.exhaust_valve_count=(*t)["exhaust_valve_count"].value_or(1); + get("valve_mass_kg",p.valve.mass_kg);get("valve_spring_n_m",p.valve.spring_rate_n_per_m);get("valve_damping",p.valve.damping_n_s_per_m);get("valve_preload_n",p.valve.seat_force_n); + get("burn_duration_deg",c.burn_duration);get("burn_start_deg",c.burn_start);get("wiebe_a",c.wiebe_a);get("wiebe_m",c.wiebe_m);get("fuel_lhv",c.fuel_lhv);get("energy_variation",c.energy_variation);get("misfire_probability",c.misfire_probability);get("ambient_temperature",c.ambient_temperature); + if(auto config=(*t)["intake_dynamics"].as_table())read_numbers(*config,valve_values(p.valve)); + p.exhaust_valve=p.valve; + if(auto config=(*t)["exhaust_dynamics"].as_table())read_numbers(*config,valve_values(p.exhaust_valve)); + if(auto points=(*t)["intake_profile"].as_array())p.intake_profile=read_cam(*points); + if(auto points=(*t)["exhaust_profile"].as_array())p.exhaust_profile=read_cam(*points); + if(auto coordinates=(*t)["port_coordinates"].as_array()){require(coordinates->size()<=128,"Too many ports");for(auto& item:*coordinates){auto a=item.as_array();require(a&&a->size()==3,"Port needs three coordinates");Vec3 v;float* values[]{&v.x,&v.y,&v.z};for(int i=0;i<3;++i){auto n=(*a)[i].value();require(bool(n),"Invalid port coordinate");*values[i]=float(*n);}p.port_coordinates.push_back(v);}} + if((*t).contains("port_keys")){const auto values=(*t)["port_keys"].as_array();require(values&&values->size()<=128,"Invalid custom port keys");for(const auto& value:*values){const auto key=value.value();require(bool(key),"Port key must be a string");p.port_keys.push_back(*key);}} + const bool custom_ports=(p.kind==MachinePartKind::Chamber||(p.kind==MachinePartKind::Junction&&!p.junction_method))&&!p.port_coordinates.empty(); + if(custom_ports&&p.port_keys.empty()){require(format<5,"Version 5 custom ports require stable keys");for(unsigned i=0;isize()<=128,"Too many ignition points");e.ignition_map.clear();for(auto& item:*map){auto pair=item.as_array();require(pair&&pair->size()==2,"Ignition point needs RPM and advance");auto rpm=(*pair)[0].value(),advance=(*pair)[1].value();require(rpm&&advance,"Invalid ignition point");e.ignition_map.push_back({float(*rpm),float(*advance)});}} + } + get("ignition_tdc_deg",p.ignition_tdc_degrees); + require(!t->contains("injector")||(*t)["injector"].is_table(),"Injector must be a table"); + if(auto config=(*t)["injector"].as_table()){ + require(!config->contains("finite_supply")||bool((*config)["finite_supply"].value()),"Injector finite_supply must be Boolean"); + MachineInjectorConfig c;const auto ecu=(*config)["ecu"].value_or(int64_t(0)),count=(*config)["cylinders"].value_or(int64_t(1));require(ecu>=0&&ecu<1000000000&&count>=1&&count<=64,"Invalid injector ECU or cylinder count");c.ecu=std::uint32_t(ecu);c.cylinders=unsigned(count);c.ecu_label=(*config)["ecu_label"].value_or(std::string{});c.finite_supply=(*config)["finite_supply"].value_or(true);read_numbers(*config,{{"maximum_kg_s",&c.maximum_flow},{"initial_kg",&c.initial_mass},{"temperature_k",&c.temperature}});p.injector=c; + } + require(!t->contains("sensor_ecu")||(*t)["sensor_ecu"].is_table(),"Sensor ECU must be a table"); + if(auto config=(*t)["sensor_ecu"].as_table()){ + require(!config->contains("enabled")||bool((*config)["enabled"].value()),"Sensor ECU enabled must be Boolean");require(!config->contains("strategy")||bool((*config)["strategy"].value()),"Sensor ECU strategy must be text"); + auto& c=p.sensor_ecu;c.enabled=(*config)["enabled"].value_or(false);c.strategy=sensor_ecu_strategy_from_key((*config)["strategy"].value_or(std::string("ignition"))); + const auto array=[&](const char* key,std::vector& target){if(!config->contains(key))return;const auto source=(*config)[key].as_array();require(source&&source->size()<=65536,"Invalid ECU fuel table array");target.clear();for(const auto& item:*source){const auto value=item.value();require(bool(value),"Fuel table entries must be numeric");target.push_back(float(*value));}}; + array("fuel_rpm",c.fuel_rpm);array("fuel_tps",c.fuel_tps);array("fuel_kg_cycle",c.fuel_per_cycle); + array("ve_rpm",c.ve_rpm);array("ve_absolute_pa",c.ve_pressure);array("volumetric_efficiency",c.volumetric_efficiency);read_numbers(*config,{{"displacement_m3",&c.displacement},{"air_r_j_kg_k",&c.air_gas_constant},{"target_afr",&c.target_afr}}); + } + if(auto config=(*t)["carburetor"].as_table())read_numbers(*config,carb_values(p.carburetor)); + if(auto config=(*t)["shaft"].as_table()){auto& c=p.shaft;read_numbers(*config,{{"inertia_kg_m2",&c.inertia},{"initial_speed_rad_s",&c.initial_speed},{"initial_angle_rad",&c.initial_angle},{"friction_nm",&c.friction_constant},{"viscous_nm_s",&c.friction_viscous},{"quadratic_nm_s2",&c.friction_quadratic},{"starter_nm",&c.starter_torque}});} + if(auto config=(*t)["drive"].as_table()){auto& c=p.drive;read_numbers(*config,{{"target_speed_rad_s",&c.target_speed},{"torque_nm",&c.torque}});c.speed_control=(*config)["speed_control"].value_or(true);c.enabled=(*config)["enabled"].value_or(true);} + if(auto config=(*t)["brake"].as_table()){auto& c=p.brake;read_numbers(*config,{{"constant_nm",&c.constant},{"viscous_nm_s",&c.viscous},{"quadratic_nm_s2",&c.quadratic}});c.enabled=(*config)["enabled"].value_or(true);} + + d.next_id=std::max(d.next_id,p.id+1);d.parts.push_back(std::move(p)); + } + if(auto links=table["connections"].as_array()){ + require(links->size()<=2048,"Too many connections"); + for(const auto& item:*links){ + const auto t=item.as_table();require(t,"Invalid connection table");MachineLink link;link.a.part=read_id((*t)["a_part"]);link.b.part=read_id((*t)["b_part"]); + if(format>=5){ + const auto a=d.find(link.a.part),b=d.find(link.b.part);require(a&&b,"Connection refers to a missing part"); + const auto ak=(*t)["a_port"].value(),bk=(*t)["b_port"].value(),method=(*t)["method"].value();require(ak&&bk&&method,"Version 5 connections require named ports and method"); + link.a.port=machine_port_index(*a,*ak);link.b.port=machine_port_index(*b,*bk);bool found=false; + for(int i=0;i=0&&ap<128&&bp>=0&&bp<128&&method>=0&&method<=4,"Invalid connection port / method"); + const auto migrate_port=[&](unsigned id,unsigned index){const auto part=d.find(id);require(part,"Connection refers to a missing part");if(part->kind==MachinePartKind::Junction&&part->junction_method){const auto legacy_count=part->port_coordinates.empty()?6:part->port_coordinates.size();require(indexsize()<=machine_probe_limit,"Too many probes");for(const auto& item:*probes){const auto p=item.as_table();require(p,"Invalid probe table");MachineProbe probe;probe.id=read_id((*p)["id"]);const auto part=(*p)["part"].value_or(int64_t(-1));require(part>=0&&part<1000000000,"Invalid probe part");probe.part=std::uint32_t(part);const auto quantity=(*p)["quantity"].value_or(-1);require(quantity>=0&&quantity<=8,"Invalid probe quantity");probe.quantity=MachineQuantity(quantity);probe.name=(*p)["name"].value_or(std::string(machine_quantity_name(probe.quantity)));probe.position=read_number(*p,"position",.5f);probe.reference_afr=read_number(*p,"reference_afr",14.7f);probe.generated_output=(*p)["generated_output"].value_or(format<=5&&(probe.quantity==MachineQuantity::Torque||probe.quantity==MachineQuantity::Power));d.probes.push_back(std::move(probe));}} + if(table.contains("sensors")){ + const auto sensors=table["sensors"].as_array();require(sensors&&sensors->size()<=1024,"Invalid sensor array"); + for(const auto& item:*sensors){const auto s=item.as_table();require(s,"Invalid sensor table");MachineSensor sensor;sensor.id=read_id((*s)["id"]); + const auto key=(*s)["kind"].value_or(std::string{});bool found=false;for(int i=0;i();require(id&&*id>=0&&*id<1000000000,"Invalid sensor host ID");target.part=std::uint32_t(*id);target.position=read_number(*t,"position",.5f);target.label=(*t)["label"].value_or(std::string{});if(!d.find(target.part))target.part=0;}; + tap("target",sensor.target);tap("reference",sensor.reference);d.next_sensor_id=std::max(d.next_sensor_id,sensor.id+1);d.sensors.push_back(std::move(sensor)); + } + } + for(auto [key,value]:{std::pair{"next_id",&d.next_id},std::pair{"next_sensor_id",&d.next_sensor_id}})if(table.contains(key)){const auto id=table[key].value();require(id&&*id>=*value&&*id<1000000000,std::string("Invalid ")+key);*value=std::uint32_t(*id);} + d.logic=read_logic_graph(table);for(auto& n:d.logic.nodes){if(n.kind==LogicNodeKind::Sensor&&!d.find_sensor(n.sensor))n.sensor=0;if((n.kind==LogicNodeKind::Command||n.kind==LogicNodeKind::Ecu)&&!d.find(n.part))n.part=0;}for(auto& p:d.parts)if(p.injector&&!d.find(p.injector->ecu))p.injector->ecu=0; + validate_machine(d);if(format<=5){upgrade_machine_mechanics(d);validate_machine(d);}for(auto& p:d.parts)if(p.kind==MachinePartKind::Pipe&&p.point_diameters.empty())p.point_diameters=machine_pipe_diameters(p);return d; + }catch(const std::exception& error){throw std::runtime_error(file.filename().string()+": "+error.what());} +} +void save_machine(const MachineDocument& d,const std::filesystem::path& file){ + validate_machine(d);toml::table t{{"format",10},{"name",d.name},{"cells_per_meter",double(d.cells_per_meter)},{"solver",pipe_solver_name(d.solver)},{"scene",int(d.scene)},{"primary_shaft",int64_t(d.primary_shaft)},{"next_id",int64_t(d.next_id)},{"next_sensor_id",int64_t(d.next_sensor_id)}}; + t.insert("logic",write_logic_graph(d.logic)); + toml::array parts,links; + for(auto& p:d.parts){ + toml::table pt{{"id",int64_t(p.id)},{"kind",machine_part_key(p.kind)},{"name",p.name},{"position_m",xyz(p.position)},{"rotation_deg",xyz(p.rotation)},{"size_m",xyz(p.size)},{"pedal_control",p.pedal_control},{"junction_method",p.junction_method}}; + const auto put=[&](const char* key,float v){pt.insert(key,double(v));}; + if(p.kind==MachinePartKind::Pipe){toml::array diameters;for(float diameter:machine_pipe_diameters(p))diameters.push_back(double(diameter));pt.insert("point_diameters_m",std::move(diameters));} + else{put("diameter_m",p.diameter);put("outlet_diameter_m",p.outlet_diameter);} + put("pressure_pa",p.pressure);put("temperature_k",p.temperature);put("opening",p.opening);put("discharge",p.discharge);put("cracking_pressure_pa",p.cracking_pressure);put("phase_deg",p.phase_degrees);put("maximum_lift_m",p.maximum_lift);put("intake_open_deg",p.intake_open);put("intake_close_deg",p.intake_close);put("exhaust_open_deg",p.exhaust_open);put("exhaust_close_deg",p.exhaust_close);put("turning_loss",p.turning_loss); + auto& c=p.cylinder;put("bore_m",c.bore);put("stroke_m",c.stroke);put("rod_m",c.rod);put("compression",c.compression);put("intake_valve_m",c.intake_valve_diameter);put("exhaust_valve_m",c.exhaust_valve_diameter);put("afr",c.afr);put("spark_advance_deg",c.spark_advance);put("heat_transfer",c.heat_transfer);put("wall_area_m2",c.wall_area);pt.insert("intake_valve_count",c.intake_valve_count);pt.insert("exhaust_valve_count",c.exhaust_valve_count); + put("valve_mass_kg",p.valve.mass_kg);put("valve_spring_n_m",p.valve.spring_rate_n_per_m);put("valve_damping",p.valve.damping_n_s_per_m);put("valve_preload_n",p.valve.seat_force_n); + put("burn_duration_deg",c.burn_duration);put("burn_start_deg",c.burn_start);put("wiebe_a",c.wiebe_a);put("wiebe_m",c.wiebe_m);put("fuel_lhv",c.fuel_lhv);put("energy_variation",c.energy_variation);put("misfire_probability",c.misfire_probability);put("ambient_temperature",c.ambient_temperature); + auto intake=p.valve,exhaust=p.exhaust_valve;pt.insert("intake_dynamics",write_numbers(valve_values(intake)));pt.insert("exhaust_dynamics",write_numbers(valve_values(exhaust)));pt.insert("intake_profile",write_cam(p.intake_profile));pt.insert("exhaust_profile",write_cam(p.exhaust_profile)); + toml::array coordinates;for(auto v:p.port_coordinates)coordinates.push_back(xyz(v));pt.insert("port_coordinates",std::move(coordinates));pt.insert("injection_enabled",p.injection_enabled); + if((p.kind==MachinePartKind::Chamber||(p.kind==MachinePartKind::Junction&&!p.junction_method))&&!p.port_coordinates.empty()){toml::array stable_keys;for(unsigned i=0;i& values){toml::array data;for(const auto value:values)data.push_back(double(value));sensor.insert(key,std::move(data));};array("fuel_rpm",c.fuel_rpm);array("fuel_tps",c.fuel_tps);array("fuel_kg_cycle",c.fuel_per_cycle);array("ve_rpm",c.ve_rpm);array("ve_absolute_pa",c.ve_pressure);array("volumetric_efficiency",c.volumetric_efficiency);pt.insert("sensor_ecu",std::move(sensor)); + } + if(p.kind==MachinePartKind::Carburetor){auto config=p.carburetor;pt.insert("carburetor",write_numbers(carb_values(config)));} + if(p.kind==MachinePartKind::Shaft||p.kind==MachinePartKind::Flywheel){const auto& c=p.shaft;pt.insert("shaft",toml::table{{"inertia_kg_m2",double(c.inertia)},{"initial_speed_rad_s",double(c.initial_speed)},{"initial_angle_rad",double(c.initial_angle)},{"friction_nm",double(c.friction_constant)},{"viscous_nm_s",double(c.friction_viscous)},{"quadratic_nm_s2",double(c.friction_quadratic)},{"starter_nm",double(c.starter_torque)}});} + if(p.kind==MachinePartKind::DriveMotor){const auto& c=p.drive;pt.insert("drive",toml::table{{"target_speed_rad_s",double(c.target_speed)},{"torque_nm",double(c.torque)},{"speed_control",c.speed_control},{"enabled",c.enabled}});} + if(p.kind==MachinePartKind::Brake){const auto& c=p.brake;pt.insert("brake",toml::table{{"constant_nm",double(c.constant)},{"viscous_nm_s",double(c.viscous)},{"quadratic_nm_s2",double(c.quadratic)},{"enabled",c.enabled}});} + + toml::array points;for(auto v:p.points)points.push_back(xyz(v));pt.insert("points_m",std::move(points));parts.push_back(std::move(pt)); + } + for(auto& l:d.links)links.push_back(toml::table{{"a_part",int64_t(l.a.part)},{"a_port",machine_port_descriptor(*d.find(l.a.part),l.a.port).key},{"b_part",int64_t(l.b.part)},{"b_port",machine_port_descriptor(*d.find(l.b.part),l.b.port).key},{"method",machine_connection_key(l.kind)},{"discharge",double(l.discharge)},{"inertance_length_m",double(l.inertance_length)},{"loss",double(l.loss)}}); + toml::array probes;for(const auto& p:d.probes)probes.push_back(toml::table{{"id",int64_t(p.id)},{"part",int64_t(p.part)},{"name",p.name},{"quantity",int(p.quantity)},{"position",double(p.position)},{"reference_afr",double(p.reference_afr)},{"generated_output",p.generated_output}});t.insert("probes",std::move(probes)); + toml::array sensors;for(const auto& s:d.sensors){const auto tap=[](const MachineSensorTap& tap){return toml::table{{"part",int64_t(tap.part)},{"position",double(tap.position)},{"label",tap.label}};};sensors.push_back(toml::table{{"id",int64_t(s.id)},{"kind",machine_sensor_key(s.kind)},{"name",s.name},{"angle_offset_rad",double(s.angle_offset)},{"target",tap(s.target)},{"reference",tap(s.reference)}});}t.insert("sensors",std::move(sensors)); + t.insert("parts",std::move(parts));t.insert("connections",std::move(links));if(!file.parent_path().empty())std::filesystem::create_directories(file.parent_path()); + auto temporary=file;temporary+=".tmp"; + {std::ofstream out(temporary);out.exceptions(std::ios::failbit|std::ios::badbit);out<size={.2f,.18f,.18f}; + const auto pump=d.add(MachinePartKind::PistonPump,{0,.2f,0}).id; + const auto air=d.add(MachinePartKind::Reservoir,{.4f,.32f,0}).id; + pipe_between(d,{tank,1},{pump,0});pipe_between(d,{pump,1},{air,0});upgrade_machine_mechanics(d);return d; +} +MachineDocument machine_engine_example(){ + MachineDocument d;d.name="Single cylinder engine bench";d.drive_rpm=0;d.driven=false; + const auto inlet=d.add(MachinePartKind::Reservoir,{-.65f,.3f,0}).id; + const auto throttle=d.add(MachinePartKind::Throttle,{-.4f,.3f,0}).id; + const auto chamber=d.add(MachinePartKind::Chamber,{-.2f,.3f,0}).id;d.find(chamber)->size={.1f,.08f,.08f}; + const auto cylinder=d.add(MachinePartKind::Cylinder,{0,.2f,0}).id; + const auto collector=d.add(MachinePartKind::Chamber,{.2f,.3f,0}).id;d.find(collector)->size={.08f,.08f,.08f}; + const auto exhaust=d.add(MachinePartKind::Reservoir,{.65f,.3f,0}).id; + const auto ecu=d.add(MachinePartKind::Ecu,{0,.5f,0}).id;d.connect({ecu,0},{cylinder,2});d.connect({ecu,0},{throttle,2}); + pipe_between(d,{inlet,0},{throttle,0});pipe_between(d,{throttle,1},{chamber,0});pipe_between(d,{chamber,1},{cylinder,0});pipe_between(d,{cylinder,1},{collector,0});pipe_between(d,{collector,1},{exhaust,0});upgrade_machine_mechanics(d);return d; +} +} diff --git a/src/machine_builder.cpp b/src/machine_builder.cpp new file mode 100644 index 0000000..16b23d4 --- /dev/null +++ b/src/machine_builder.cpp @@ -0,0 +1,1114 @@ +#include "FluidSim/Desktop/MachineBuilder.hpp" +#include "FluidSim/Desktop/MachineRenderer3D.hpp" +#include "FluidSim/Desktop/WorkshopLayout.hpp" +#include "FluidSim/Desktop/WindowPresentation.hpp" +#include "FluidSim/Desktop/PortAppearance.hpp" +#include "FluidSim/Desktop/LogicEditor.hpp" +#include "FluidSim/SoundEngine.hpp" +#include "FluidSim/ScenarioCatalog.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fluidsim::port::desktop { +using namespace machine_math; +namespace { +const sf::Color background{18,21,26},panel{27,31,38},border{54,62,75},muted{158,168,183},text_color{229,233,240},accent{116,170,235},amber{234,187,106},invalid{238,118,128}; +const sf::Color axis_color[]{sf::Color{242,101,112},sf::Color{115,218,147},sf::Color{102,162,255}}; +const sf::Color probe_colors[]{sf::Color{83,211,190},sf::Color{249,187,86},sf::Color{133,179,255},sf::Color{225,136,238},sf::Color{240,128,139},sf::Color{174,219,117},sf::Color{104,214,235},sf::Color{223,190,133}}; +constexpr Vec3 axes[]{{1,0,0},{0,1,0},{0,0,1}}; +bool boundary_symbol(const MachinePart& p){return p.kind==MachinePartKind::Reservoir||p.kind==MachinePartKind::Cap||(p.kind==MachinePartKind::Junction&&p.junction_method);} +void label(sf::RenderWindow& w,const sf::Font* font,const std::string& str,float x,float y,unsigned size=12,sf::Color color=text_color){if(font){sf::Text t(*font,str,size);t.setPosition({x,y});t.setFillColor(color);w.draw(t);}} +std::string fit_text(const sf::Font* font,std::string s,float width,unsigned size=12){ + if(!font||width<=0)return {}; + sf::Text measured(*font,s,size);if(measured.getLocalBounds().size.x<=width)return s; + while(!s.empty()){s.pop_back();measured.setString(s+"...");if(measured.getLocalBounds().size.x<=width)return s+"...";}return {}; +} +void fitted_label(sf::RenderWindow& w,const sf::Font* font,const std::string& str,float x,float y,float width,unsigned size=12,sf::Color color=text_color){label(w,font,fit_text(font,str,width,size),x,y,size,color);} +void wrapped_label(sf::RenderWindow& w,const sf::Font* font,const std::string& text,float x,float y,float width,unsigned size,int max_lines,sf::Color color){ + if(!font)return; + std::istringstream words(text);std::string word,line;int row=0;sf::Text measure(*font,"",size); + while(words>>word){const auto candidate=line.empty()?word:line+" "+word;measure.setString(candidate); + if(!line.empty()&&measure.getLocalBounds().size.x>width){fitted_label(w,font,line,x,y+row*(size+5),width,size,color);if(++row>=max_lines)return;line=word;}else line=candidate; + }if(row1e-5f?std::clamp(((p.x-a.x)*d.x+(p.y-a.y)*d.y)/denominator,0.f,1.f):0;return screen_distance(p,a+d*t);} +std::string filename_slug(std::string s){for(char& c:s){if(c>='A'&&c<='Z')c+=32;else if(!((c>='a'&&c<='z')||(c>='0'&&c<='9')||c=='-'||c=='_'))c='-';}return s.empty()?"untitled":s;} +class TestRun { + MachineSimulation simulation_; + SimulationRingBuffer ring_{8192}; + SoundEngine audio_{ring_}; + mutable std::mutex mutex_; + std::jthread worker_; + std::string error_; + std::vector pending_; + std::uint64_t samples_{};std::size_t probe_count_{}; + AudioFrame advance(){auto frame=simulation_.process();if(++samples_%16==0&&probe_count_){if(pending_.size()>=32768)pending_.erase(pending_.begin(),pending_.begin()+16384);pending_.push_back(simulation_.measure());}return frame;} + void halt(){worker_.request_stop();audio_.stop();if(worker_.joinable())worker_.join();ring_.discard();} + void begin(){ + const auto fill=std::max(4,int(std::ceil(2048*audio_.speed())));for(int i=0;i frames;{std::lock_guard guard(mutex_);for(int i=0;i1)throw std::invalid_argument("Playback speed must be 0.01..1");halt();audio_.set_speed(value);if(!paused)begin();} + double speed()const{return audio_.speed();} + void step(){if(!paused)return;std::lock_guard guard(mutex_);for(int i=0;i<441;++i)advance();} + void tune(const MachineDocument& d,bool probes_changed=false){std::lock_guard guard(mutex_);simulation_.apply_tuning(d);probe_count_=d.probes.size();if(probes_changed){pending_.clear();if(probe_count_)pending_.push_back(simulation_.measure());}} + std::vector take_measurements(){std::lock_guard guard(mutex_);std::vector result;result.swap(pending_);return result;} + void controls(float pedal,bool starter,AudioListener listener){std::lock_guard guard(mutex_);simulation_.set_controls(pedal,starter);simulation_.set_listener(listener);} + void logic_control(std::uint32_t node,bool value){std::lock_guard guard(mutex_);simulation_.set_logic_control(node,value);} + void logic_control(std::uint32_t node,double value){std::lock_guard guard(mutex_);simulation_.set_logic_control(node,value);} + void release_logic_buttons(){std::lock_guard guard(mutex_);simulation_.release_logic_buttons();} + MachineSnapshot snapshot()const{std::lock_guard guard(mutex_);return simulation_.snapshot();} + std::uint64_t underruns()const{return audio_.underrun_blocks();} + std::size_t buffered()const{return ring_.available_samples();} + std::string error()const{std::lock_guard guard(mutex_);return error_;} +}; +} +struct MachineBuilder::Impl { + enum class Tool {Select,Move,Rotate,Connect,Route}; + enum class Modal {None,Save,Open,Import,Assemblies,SaveAssembly,Unsaved,Rename}; + enum class Pending {Exit,CloseWindow,New,Open,PlayFile,Pump,Engine,Logic,Import}; + struct Action {sf::FloatRect rect;std::string title;std::function run;bool active{},enabled{true};std::optional icon{};std::string subtitle{};}; + struct Field { + std::string name;float* value{};int* integer{};float scale{1},step{1},minimum{-1e6f},maximum{1e6f};int precision{2}; + bool* boolean{};bool requires_restart{}; + float get()const{return value?*value:integer?float(*integer):float(*boolean);} + void set(float x)const{if(value)*value=x;else if(integer)*integer=int(std::round(x));else *boolean=x>=.5f;} + }; + MachineDocument doc; + MachineRenderer3D renderer; + LogicEditor logic_editor; + int workspace_mode{};bool logic_focus{}; + std::filesystem::path file; + std::vector undo,redo; + std::optional clipboard; + std::uint32_t selected{}; + std::set selected_ids; + OrbitCamera camera; + sf::Vector2u last_size{1920,1080}; + Tool tool{Tool::Move}; + int inspector{},field_scroll{},link_index{-1},point_index{}; + std::optional placing; + std::optional pending_port; + std::optional connection_hover; + std::optional connection_preview; + bool port_drag{};sf::Vector2i port_drag_start{}; + std::vector checks; + bool checks_dirty{true};int check_scroll{}; + sf::Vector2i mouse{},previous_mouse{}; + bool orbit{},pan{},pedal_drag{},dirty{},done{},close_window{},shift_held{},control_held{},starter_mouse{},snap{true}; + bool box_select{};sf::Vector2i box_start{}; + float grid{.01f},plane_y{.25f},pedal{.2f}; + bool starter{}; + std::string status{"Pick a part, then click the grid to place it. Dimensions are in millimetres."}; + std::unique_ptr test; + MachineSnapshot live; + double test_speed{1}; + std::deque trace; + std::vector recorded_probes; + int probe_index{};float trace_window{.2f}; + int sensor_index{};bool sensor_reference{};float sensor_add_position{.5f}; + bool scope_visible{}; + bool pedal_available()const{return std::any_of(doc.parts.begin(),doc.parts.end(),[&](const auto& p){return (p.kind==MachinePartKind::Ecu&&!p.sensor_ecu.enabled)||((p.kind==MachinePartKind::Throttle||p.kind==MachinePartKind::Carburetor)&&p.pedal_control&&!logic_command_owner(doc,p.id,LogicCommand::ThrottleOpening));});} + float left_width{260},right_width{340},trace_height{210};bool left_closed{},right_closed{}; + int resize_panel{},library_scroll{},library_tab{},library_category{},preview_kind{}; + std::string search_text;bool search_focus{},search_replace{}; + const sf::Font* ui_font{}; + WorkshopLayout layout(sf::Vector2u size)const{return WorkshopLayout::calculate(size,left_width,right_width,left_closed,right_closed);} + sf::FloatRect scope_rect(sf::Vector2u size)const{auto v=layout(size).viewport;const float height=std::clamp(trace_height,170.f,std::max(170.f,v.size.y-220));return {{v.position.x,v.position.y+v.size.y-height},{v.size.x,height}};} + sf::FloatRect workspace_area(sf::Vector2u size)const{auto v=layout(size).viewport;if(scope_visible)v.size.y=std::max(100.f,scope_rect(size).position.y-v.position.y-8);return v;} + sf::FloatRect view(sf::Vector2u size)const{auto v=workspace_area(size);if(workspace_mode==2)v.size.x=(v.size.x-8)*.5f;return v;} + sf::FloatRect logic_view(sf::Vector2u size)const{auto v=workspace_area(size);if(workspace_mode==2){v.position.x+=(v.size.x+8)*.5f;v.size.x=(v.size.x-8)*.5f;}return v;} + bool graph_panels()const{return workspace_mode==1||(workspace_mode==2&&logic_focus);} + LogicEditorHooks logic_hooks(){return {doc,test?&live:nullptr,shift_held, + [this](const auto& action){return edit(action,false);}, + [this](std::uint32_t node,bool value){if(test){test->logic_control(node,value);collect();}}, + [this](std::uint32_t node,double value){if(test){test->logic_control(node,value);collect();}}, + [this]{if(test)test->release_logic_buttons();}, + [this](std::uint32_t id){selected_ids.clear();selected=id;if(id&&doc.find(id))selected_ids.insert(id);else selected=0;field_scroll=point_index=0;inspector=0;}, + [this](std::uint32_t id){if(!doc.find(id))return;if(workspace_mode==1)workspace_mode=2;logic_focus=true;selected_ids={id};selected=id;fit(true);}, + [this](const std::string& message){status=message;}, + [this]{request(Pending::Logic);}};} + void change_workspace(int next){if(next<0||next>2)return;if(next!=workspace_mode){auto h=logic_hooks();logic_editor.deactivate(h);finish_drag();placing.reset();pending_port.reset();context_menu.reset();edit_name.clear();search_focus=false;workspace_mode=next;logic_focus=next!=0;save_preferences();}} + void show_logic_node(std::uint32_t id,sf::Vector2u size){change_workspace(1);right_closed=false;logic_focus=true;logic_editor.reveal(id,doc,logic_view(size));} + LogicPosition new_logic_position()const{float right=-300;for(const auto& n:doc.logic.nodes)right=std::max(right,n.position.x);return {right+300,0};} + void show_sensor_logic(std::uint32_t sensor,sf::Vector2u size){const auto source=doc.find_sensor(sensor);if(!source)return;std::uint32_t id=0;for(const auto& n:doc.logic.nodes)if(n.kind==LogicNodeKind::Sensor&&n.sensor==sensor){id=n.id;break;}if(!id){if(test){status="Return to Build to add this sensor to Logic.";return;}const auto kind=source->kind;const auto name=source->name;if(!edit([&]{auto& n=doc.logic.add(LogicNodeKind::Sensor,new_logic_position());id=n.id;n.sensor=sensor;n.sensor_kind=kind;n.name=name;n.binding_label=name;},false))return;}show_logic_node(id,size);} + void show_command_logic(std::uint32_t part,sf::Vector2u size){const auto source=doc.find(part);if(!source)return;LogicCommand command=LogicCommand::MotorEnable;if(source->kind==MachinePartKind::Brake)command=LogicCommand::BrakeEnable;else if(source->kind==MachinePartKind::Throttle||source->kind==MachinePartKind::Carburetor)command=LogicCommand::ThrottleOpening;else if(source->kind==MachinePartKind::Shaft)command=LogicCommand::Starter;else if(source->kind!=MachinePartKind::DriveMotor)return;std::uint32_t id=0;if(const auto n=logic_command_owner(doc,part,command))id=n->id;if(!id){if(test){status="Return to Build to create a signal command owner.";return;}const auto name=source->name;if(!edit([&]{auto& n=doc.logic.add(LogicNodeKind::Command,new_logic_position());id=n.id;n.part=part;n.command=command;n.name=std::string(logic_command_name(command))+" / "+name;if(n.name.size()>120)n.name.resize(120);n.binding_label=name;},false))return;}show_logic_node(id,size);} + void show_ecu_logic(std::uint32_t part_id,sf::Vector2u size){ + const auto source=doc.find(part_id);if(!source||!source->sensor_ecu.enabled)return;std::uint32_t id=0;for(const auto& node:doc.logic.nodes)if(node.kind==LogicNodeKind::Ecu&&node.part==part_id)id=node.id; + if(!id){if(test){status="Return to Build to add the ECU connector.";return;}const auto name=source->name;const auto strategy=source->sensor_ecu.strategy;if(!edit([&]{auto& node=doc.logic.add(LogicNodeKind::Ecu,new_logic_position());id=node.id;node.part=part_id;node.ecu_strategy=strategy;node.name="ECU / "+name;if(node.name.size()>120)node.name.resize(120);node.binding_label=name;},false))return;} + show_logic_node(id,size); + } + std::filesystem::path preference_path()const{ + if(const char* path=std::getenv("FLUIDSIM_UI_SETTINGS");path&&*path)return path; + if(const char* root=std::getenv("LOCALAPPDATA"))return std::filesystem::path(root)/"FluidSimCpp"/"workshop-ui.txt"; + if(const char* root=std::getenv("XDG_CONFIG_HOME"))return std::filesystem::path(root)/"FluidSimCpp"/"workshop-ui.txt"; + if(const char* root=std::getenv("HOME"))return std::filesystem::path(root)/".config"/"FluidSimCpp"/"workshop-ui.txt"; + return {}; + } + void save_preferences()const{try{const auto path=preference_path();if(path.empty())return;std::filesystem::create_directories(path.parent_path());std::ofstream out(path);out<>l>>r>>lc>>rc&&std::isfinite(l)&&std::isfinite(r)){left_width=std::clamp(l,220.f,420.f);right_width=std::clamp(r,280.f,520.f);left_closed=lc;right_closed=rc;float th;if(in>>th&&std::isfinite(th))trace_height=std::clamp(th,170.f,440.f);int mode;bool focus;if(in>>mode>>focus&&mode>=0&&mode<=2){workspace_mode=mode;logic_focus=focus;}}}catch(...){}} + std::vector library_items()const{ + std::vector result;auto query=search_text;for(auto& c:query)c=char(std::tolower(static_cast(c))); + const int count=library_tab?int(doc.parts.size()):machine_part_kind_count; + for(int i=0;i(c))); + const bool engine=kind==MachinePartKind::Cylinder||kind==MachinePartKind::PistonPump||kind==MachinePartKind::Carburetor; + if(!library_tab&&((library_category==1&&(engine||kind==MachinePartKind::Ecu||machine_rotational_part(kind)))||(library_category==2&&!engine)||(library_category==3&&!machine_rotational_part(kind))||(library_category==4&&kind!=MachinePartKind::Ecu)))continue; + if(query.empty()||title.find(query)!=std::string::npos)result.push_back(i); + }return result; + } + void meters(){if(workspace_mode){auto h=logic_hooks();logic_editor.deactivate(h);}if(workspace_mode==1)change_workspace(2);logic_focus=false;right_closed=false;inspector=4;scope_visible=true;edit_name.clear();} + void select_probe(int index){if(doc.probes.empty())return;probe_index=std::clamp(index,0,int(doc.probes.size())-1);selection(doc.probes[probe_index].part);meters();} + void select_sensor(int index){if(doc.sensors.empty())return;sensor_index=std::clamp(index,0,int(doc.sensors.size())-1);const auto host=doc.sensors[sensor_index].target.part;if(doc.find(host))selection(host);right_closed=false;inspector=5;edit_name.clear();} + void add_sensor(MachineSensorKind kind){if(test||!part())return;if(edit([&]{doc.add_sensor(kind,selected,sensor_add_position);})){sensor_reference=false;select_sensor(int(doc.sensors.size())-1);status=kind==MachineSensorKind::DifferentialPressure?"Positive tap added. Select the reference part, open Sense and bind the reference tap.":"Ideal sensor attached. Sense shows its outputs; Test samples at every simulation boundary.";}} + std::optional sensor_screen(std::size_t index,sf::Vector2u size)const{ + const auto world=machine_sensor_position(doc,doc.sensors[index].target);if(!world)return {};const auto viewport=view(size);const auto anchor=builder_project(camera,viewport,*world);if(!anchor)return {};int overlap=0; + for(std::size_t i=0;i probe_world(std::size_t index)const{const auto& probe=doc.probes[index];const auto p=doc.find(probe.part);if(!p)return {};return p->kind==MachinePartKind::Pipe?GeometryPath{machine_pipe_path(*p)}.position(machine_pipe_length(*p)*probe.position):p->position;} + std::optional probe_screen(std::size_t index,sf::Vector2u size)const{ + const auto world=probe_world(index);if(!world)return {};const auto viewport=view(size);const auto anchor=builder_project(camera,viewport,*world);if(!anchor)return {};int overlap=0; + for(std::size_t i=0;itake_measurements()){trace.push_back(sample);if(trace.size()>32768)trace.pop_front();}live=test->snapshot();}} + void tune(){if(!test)return;const bool changed=recorded_probes!=doc.probes;test->tune(doc,changed);if(changed){trace.clear();recorded_probes=doc.probes;}} + Modal modal{Modal::None};Pending pending{Pending::Exit}; + std::vector files; + std::filesystem::path pending_file; + std::string pending_preset; + const std::vector import_ids{"single-450","twin","flat6","v8-h"}; + int file_scroll{}; + std::string input,edit_name,edit_buffer; + bool replace_input{true}; + bool save_then_pending{},rename_part{}; + std::optional context_menu; + struct Drag { + int axis{-1};bool rotate{},checkpointed{};float parameter{},angle{};sf::Vector2i mouse{};MachineDocument original;Vec3 origin{},plane_start{};std::vector ids; + } drag; + Impl(){camera.target={0,.22f,0};camera.distance=1.65f;camera.pitch=26;camera.yaw=-28;} + MachinePart* part(){return doc.find(selected);} + const MachinePart* part()const{return doc.find(selected);} + bool linked(MachinePort p)const{for(auto& l:doc.links)if(l.a==p||l.b==p)return true;return false;} + float snapped(float x,float step=0)const{step=step>0?step:grid;return snap?std::round(x/step)*step:x;} + void checkpoint(){undo.push_back(doc);if(undo.size()>80)undo.erase(undo.begin());redo.clear();dirty=true;checks_dirty=true;} + void refresh_checks(){if(checks_dirty){checks=machine_checks(doc);checks_dirty=false;check_scroll=std::clamp(check_scroll,0,std::max(0,int(checks.size())-1));}} + bool edit(const std::function& action,bool synchronize=true){ + const auto before=doc; + try{action();if(!test&&synchronize)doc.sync_connections(selected);validate_machine(doc);tune();undo.push_back(before);if(undo.size()>80)undo.erase(undo.begin());redo.clear();dirty=checks_dirty=true;if(!test&&before.probes!=doc.probes){trace.clear();recorded_probes=doc.probes;}return true;} + catch(const std::exception& e){doc=before;status=e.what();return false;} + } + void selection(std::uint32_t id,bool extend=false){ + if(!extend)selected_ids.clear(); + if(id){if(extend&&selected_ids.contains(id))selected_ids.erase(id);else selected_ids.insert(id);} + selected=selected_ids.contains(id)?id:selected_ids.empty()?0:*selected_ids.rbegin();field_scroll=0;point_index=0;link_index=-1;edit_name.clear();inspector=0;sensor_add_position=.5f; + if(id)logic_editor.select_part_reference(id,doc); + } + void clean_selection(){std::erase_if(selected_ids,[&](auto id){return doc.find(id)==nullptr;});if(!selected_ids.contains(selected))selected=selected_ids.empty()?0:*selected_ids.begin();} + std::vector selection_parts()const{return std::vector(selected_ids.begin(),selected_ids.end());} + void undo_action(){if(undo.empty())return;if(test)try{test->tune(undo.back(),doc.probes!=undo.back().probes);}catch(const std::exception& e){status=e.what();return;}redo.push_back(doc);const bool changed=doc.probes!=undo.back().probes;doc=std::move(undo.back());undo.pop_back();if(changed){trace.clear();recorded_probes=doc.probes;}dirty=checks_dirty=true;clean_selection();pending_port.reset();} + void redo_action(){if(redo.empty())return;if(test)try{test->tune(redo.back(),doc.probes!=redo.back().probes);}catch(const std::exception& e){status=e.what();return;}undo.push_back(doc);const bool changed=doc.probes!=redo.back().probes;doc=std::move(redo.back());redo.pop_back();if(changed){trace.clear();recorded_probes=doc.probes;}dirty=checks_dirty=true;clean_selection();pending_port.reset();} + void fit(bool selection_only=false){ + if(doc.parts.empty()){camera.target={0,.22f,0};camera.distance=1.65f;return;} + Vec3 lo{1e6f,1e6f,1e6f},hi{-1e6f,-1e6f,-1e6f}; + const auto include=[&](Vec3 p){lo={std::min(lo.x,p.x),std::min(lo.y,p.y),std::min(lo.z,p.z)};hi={std::max(hi.x,p.x),std::max(hi.y,p.y),std::max(hi.z,p.z)};}; + for(auto& p:doc.parts){if(selection_only&&!selected_ids.contains(p.id))continue;if(p.kind==MachinePartKind::Pipe){const auto diameters=machine_pipe_diameters(p);const float r=*std::max_element(diameters.begin(),diameters.end())*.5f;for(auto v:machine_pipe_path(p)){include(add(v,{r,r,r}));include(subtract(v,{r,r,r}));}}else{auto s=boundary_symbol(p)?Vec3{.04f,.04f,.04f}:machine_part_size(p);for(int c=0;c<8;++c)include(machine_world_point(p,{(c&1?1:-1)*s.x/2,(c&2?1:-1)*s.y/2,(c&4?1:-1)*s.z/2}));}} + if(lo.x>hi.x)return; + const auto viewport=view(last_size);const float aspect=std::max(1.f,viewport.size.y/viewport.size.x);camera.target=multiply(add(lo,hi),.5f);camera.distance=std::clamp((length(subtract(hi,lo))*1.4f+.15f)*aspect,.35f,200.f); + } + void set_document(MachineDocument d,std::filesystem::path path={}){stop();logic_editor.reset_document();scope_visible=false;trace.clear();recorded_probes.clear();sensor_index=0;sensor_reference=false;doc=std::move(d);for(auto& p:doc.parts)if(p.kind==MachinePartKind::Pipe)p.point_diameters=machine_pipe_diameters(p);file=std::move(path);undo.clear();redo.clear();selection(0);tool=Tool::Move;placing.reset();pending_port.reset();dirty=false;checks_dirty=true;check_scroll=0;fit();status="Design loaded. F focuses selection; H opens checks. Test, then hold V to start an engine.";} + void start(){ + auto hooks=logic_hooks();logic_editor.deactivate(hooks);edit_name.clear();placing.reset();pending_port.reset(); + try{validate_machine(doc);trace.clear();recorded_probes=doc.probes;test=std::make_unique(doc,test_speed);collect();const auto warnings=machine_diagnostics(doc);unsigned dry=0;for(auto& p:live.parts)if(doc.find(p.id)->kind==MachinePartKind::Cylinder&&!p.fuel_source)++dry; + const auto primary=doc.find(machine_primary_shaft(doc));status="Testing "+std::to_string(live.cells)+" cells. "+std::to_string(warnings.size())+" sealed air ports. "+(dry?std::to_string(dry)+" cylinders have no fuel source. ":"")+(primary&&primary->shaft.starter_torque>0?"Hold V to start. ":"")+"Tab returns to Build.";} + catch(const std::exception& e){status=std::string("Cannot test: ")+e.what();test.reset();} + } + void stop(){auto hooks=logic_hooks();logic_editor.deactivate(hooks);if(test){collect();test.reset();status="Back in Build. Live tuning is kept; recorded traces remain available in Meters.";}starter=false;} + void speed(double value){try{if(!std::isfinite(value))throw std::invalid_argument("Speed must be finite");value=std::clamp(value,.01,1.);if(test)test->speed(value);test_speed=value;collect();}catch(const std::exception& e){status=e.what();}} + void add_probe(MachineQuantity quantity){ + if(doc.probes.size()>=machine_probe_limit){status="Eight channels are already in use. Remove a channel first.";return;} + const auto p=part();if(quantity=MachineQuantity::Rpm?(p&&machine_quantity_supported(*p,quantity)?p->id:machine_primary_shaft(doc)):(p?p->id:0); + if(!target){status="Select a shaft, motor, brake or other compatible measurement target.";return;} + const auto measured=doc.find(target);std::string metric=machine_quantity_name(quantity); + if(quantity==MachineQuantity::Torque)metric=measured->kind==MachinePartKind::DriveMotor?"Drive torque":measured->kind==MachinePartKind::Brake?"Brake torque":measured->kind==MachinePartKind::Shaft?"Net shaft torque":"Part torque"; + if(quantity==MachineQuantity::Power)metric=measured->kind==MachinePartKind::DriveMotor?"Drive power":measured->kind==MachinePartKind::Brake?"Absorbed power":measured->kind==MachinePartKind::Shaft?"Kinetic power":"Part power"; + MachineProbe probe;for(const auto& old:doc.probes)probe.id=std::max(probe.id,old.id);++probe.id;probe.part=target;probe.quantity=quantity;probe.name=metric+" / "+measured->name; + probe.name=short_text(probe.name,120);edit([&]{doc.probes.push_back(probe);});probe_index=int(doc.probes.size())-1;meters();if(!test){trace.clear();recorded_probes=doc.probes;}status="Instrument added. Pipe position is measured from Start to End."; + } + bool export_csv(const std::filesystem::path& path){try{collect();if(trace.empty()||recorded_probes.empty())throw std::runtime_error("Run a test with instruments before exporting");if(!path.parent_path().empty())std::filesystem::create_directories(path.parent_path());std::ofstream out(path);out.exceptions(std::ios::badbit|std::ios::failbit);out<<"time_s";for(const auto& probe:recorded_probes){std::string title=probe.name;for(std::size_t at=0;(at=title.find('"',at))!=std::string::npos;at+=2)title.insert(at,1,'"');out<<",\""<1e-5f?(plane_y-ray.origin.y)/ray.direction.y:1; + if(t<.03f||t>100)t=camera.distance; + auto p=add(ray.origin,multiply(ray.direction,t));return {snapped(p.x),plane_y,snapped(p.z)}; + } + Vec3 gizmo_origin()const{ + auto p=part();if(!p)return {}; + if(selected_ids.size()>1){Vec3 total{};for(auto id:selected_ids)if(auto q=doc.find(id)){const auto point=q->kind==MachinePartKind::Pipe?GeometryPath{machine_pipe_path(*q)}.position(machine_pipe_length(*q)*.5f):q->position;total=add(total,point);}return multiply(total,1.f/selected_ids.size());} + if(tool==Tool::Route&&p->kind==MachinePartKind::Pipe)return machine_world_point(*p,p->points[std::clamp(point_index,0,int(p->points.size())-1)]); + if(p->kind==MachinePartKind::Pipe)return GeometryPath{machine_pipe_path(*p)}.position(machine_pipe_length(*p)*.5f); + return p->position; + } + float gizmo_length(sf::Vector2u size)const{return camera.distance*.828427f/view(size).size.y*76;} + void insert_bend(sf::Vector2f screen,sf::Vector2u size){ + auto p=part();if(!p||p->kind!=MachinePartKind::Pipe||test)return; + const auto at=builder_pipe_location(camera,view(size),*p,screen); + if(!at||at->screen_distance>16){status="Ctrl-click near a pipe centerline to insert a bend.";return;} + if(p->points.size()>=128){status="A pipe supports at most 128 route points.";return;} + if(length(subtract(at->local_point,p->points[at->segment]))<.001f||length(subtract(at->local_point,p->points[at->segment+1]))<.001f){status="Pick farther from an existing point (at least 1 mm).";return;} + edit([&]{machine_insert_pipe_point(*p,at->segment,at->local_point);point_index=int(at->segment+1);tool=Tool::Route;}); + status="Bend inserted. Drag its X/Y/Z handles; Ctrl+Z undoes the edit."; + } + std::optional pick_port(sf::Vector2f screen,sf::Vector2u size,bool free_only)const{ + std::optional result;float best=13; + for(auto& p:doc.parts)for(unsigned i=0;ipart)->kind==MachinePartKind::Pipe&&std::abs(d-best)<.5f&&length(subtract(machine_port_position(p,i),machine_port_position(*doc.find(result->part),result->port)))<1e-5f; + if(dkind==MachinePartKind::Pipe||boundary_symbol(*foreground))for(auto& p:doc.parts)if(boundary_symbol(p)){ + if(foreground&&foreground->kind==MachinePartKind::Pipe&&!std::any_of(doc.links.begin(),doc.links.end(),[&](const auto& l){return (l.a.part==foreground->id&&l.b.part==p.id)||(l.b.part==foreground->id&&l.a.part==p.id);}))continue; + if(auto pos=builder_project(camera,view(size),p.position)){const float distance=screen_distance(screen,*pos);if(distance1e-5f?(dot(q,a)-b*dot(q,ray.direction))/den:0; + } + float rotation_angle(BuilderRay ray,Vec3 origin,int axis)const{ + const auto n=axes[axis];float den=dot(ray.direction,n);if(std::abs(den)<.001f)return 0; + float t=dot(subtract(origin,ray.origin),n)/den;auto v=subtract(add(ray.origin,multiply(ray.direction,t)),origin); + return std::atan2(dot(v,axes[(axis+2)%3]),dot(v,axes[(axis+1)%3]))*180/pi; + } + Vec3 plane_point(BuilderRay ray,Vec3 origin)const{auto n=unit(subtract(camera.target,camera.listener().position));const float den=dot(ray.direction,n);return std::abs(den)>1e-6f?add(ray.origin,multiply(ray.direction,dot(subtract(origin,ray.origin),n)/den)):origin;} + int hit_gizmo(sf::Vector2f screen,sf::Vector2u size)const{ + if(!part()||test||tool==Tool::Select||tool==Tool::Connect)return -1; + const auto viewport=view(size);const auto origin=gizmo_origin();auto center=builder_project(camera,viewport,origin);if(!center)return -1; + const float radius=gizmo_length(size);float best=9;int found=-1; + for(int axis=0;axis<3;++axis){ + if(tool==Tool::Rotate){for(int i=0;i<64;++i){auto ring=[&](int k){float a=2*pi*k/64;return add(origin,add(multiply(axes[(axis+1)%3],radius*std::cos(a)),multiply(axes[(axis+2)%3],radius*std::sin(a))));};auto a=builder_project(camera,viewport,ring(i)),b=builder_project(camera,viewport,ring(i+1));if(a&&b){float d=line_distance(screen,*a,*b);if(dkind==MachinePartKind::Pipe&&(point_index==0||point_index==int(part()->points.size())-1)&&linked({selected,unsigned(point_index?1:0)})){status="This endpoint is attached. Move its connected part or disconnect the port first.";return true;} + drag.axis=axis;drag.rotate=tool==Tool::Rotate;drag.checkpointed=false;drag.original=doc;drag.origin=gizmo_origin();drag.mouse=mouse;drag.ids.assign(selected_ids.begin(),selected_ids.end()); + auto ray=builder_ray(camera,view(size),screen);drag.parameter=axis_parameter(ray,drag.origin,axis);drag.angle=rotation_angle(ray,drag.origin,axis);return true; + } + void move_drag(sf::Vector2f screen,sf::Vector2u size){ + if(drag.axis<0)return; + if(!drag.checkpointed&&screen==sf::Vector2f(drag.mouse))return; + if(!drag.checkpointed){checkpoint();drag.checkpointed=true;} + doc=drag.original;auto p=part();if(!p)return;const auto axis=drag.axis;auto ray=builder_ray(camera,view(size),screen); + if(axis==3){auto delta=subtract(plane_point(ray,drag.origin),drag.plane_start);delta={snapped(delta.x),snapped(delta.y),snapped(delta.z)};doc.transform_selection(drag.ids,drag.origin,delta);return;} + if(drag.rotate){float angle=rotation_angle(ray,drag.origin,axis)-drag.angle;while(angle>180)angle-=360;while(angle< -180)angle+=360; + if(std::abs(dot(ray.direction,axes[axis]))<.08f)angle=(mouse.x-drag.mouse.x)*.5f; + doc.transform_selection(drag.ids,drag.origin,{},unsigned(axis),snapped(angle,15));return; + }else{const float delta=snapped(axis_parameter(ray,drag.origin,axis)-drag.parameter);const auto offset=multiply(axes[axis],delta); + if(tool==Tool::Route&&p->kind==MachinePartKind::Pipe&&selected_ids.size()==1)p->points[point_index]=machine_local_point(*p,add(drag.origin,offset));else{doc.transform_selection(drag.ids,drag.origin,offset);return;} + }doc.sync_connections(selected); + } + void finish_drag(){if(drag.axis<0)return;if(drag.checkpointed)try{validate_machine(doc);}catch(const std::exception& e){doc=std::move(drag.original);status=e.what();if(!undo.empty())undo.pop_back();}drag.axis=-1;} + Vec3 selection_center()const{return selected_ids.empty()?camera.target:machine_selection_center(doc,selection_parts());} + void copy_selection(){if(test||selected_ids.empty()){status="Select one or more parts to copy.";return;}try{clipboard=machine_extract_assembly(doc,selection_parts(),"Copied selection");status="Copied "+std::to_string(clipboard->parts.size())+" parts. Ctrl+V pastes a connected copy.";}catch(const std::exception& e){status=e.what();}} + bool paste_fragment(const MachineDocument& fragment,Vec3 center,bool copy_names){ + if(test)return false; + std::vector inserted; + if(!edit([&]{inserted=machine_insert_assembly(doc,fragment,center);if(copy_names)for(auto id:inserted)if(auto* p=doc.find(id))p->name=short_text(p->name,110)+" copy";}))return false; + selection(0);selected_ids.insert(inserted.begin(),inserted.end());selected=*selected_ids.rbegin();tool=Tool::Move;placing.reset();pending_port.reset(); + status="Inserted "+std::to_string(inserted.size())+" parts with internal connections. Drag the group or use G / R to position it."; + return true; + } + void paste(){if(test)return;if(clipboard)paste_fragment(*clipboard,add(selection_center(),{.08f,0,.08f}),false);else status="Nothing copied yet. Select parts and press Ctrl+C.";} + void duplicate(){if(!part()||test)return;try{auto fragment=machine_extract_assembly(doc,selection_parts(),"Duplicated selection");paste_fragment(fragment,add(selection_center(),{.08f,0,.08f}),true);}catch(const std::exception& e){status=e.what();}} + void remove(){if(!selected||test)return;checkpoint();for(auto id:selected_ids)doc.remove(id);selection(0);pending_port.reset();trace.clear();recorded_probes=doc.probes;} + void request(Pending next,std::filesystem::path path={}){ + stop();pending=next;pending_file=std::move(path);if(dirty){modal=Modal::Unsaved;return;}execute_pending(); + } + void execute_pending(){ + modal=Modal::None; + try{switch(pending){case Pending::Exit:done=true;break;case Pending::CloseWindow:done=true;close_window=true;break;case Pending::New:set_document({});break;case Pending::Open:set_document(load_machine(pending_file),pending_file);break;case Pending::PlayFile:set_document(load_machine(pending_file),pending_file);start();break;case Pending::Pump:set_document(machine_pump_example());dirty=true;break;case Pending::Engine:set_document(machine_engine_example());dirty=true;break;case Pending::Logic:set_document(machine_logic_example());change_workspace(1);dirty=true;break;case Pending::Import:set_document(machine_from_engine(scenario_example(pending_preset)));dirty=true;break;}} + catch(const std::exception& e){status=e.what();} + } + void open_library(){files=machine_files();file_scroll=0;modal=Modal::Open;input.clear();} + void open_assemblies(){files=machine_assembly_files();file_scroll=0;modal=Modal::Assemblies;input.clear();} + void save_assembly(){if(test||selected_ids.empty()){status="Select one or more parts to save as an assembly.";return;}modal=Modal::SaveAssembly;input=filename_slug(doc.name).substr(0,80)+"-assembly";replace_input=true;} + void insert_assembly(const std::filesystem::path& path){try{auto assembly=load_machine(path);const auto center=add(selected_ids.empty()?camera.target:selection_center(),{.18f,0,.18f});if(paste_fragment(assembly,center,false))fit(true);}catch(const std::exception& e){status=e.what();}} + bool save(bool save_as=false){ + try{if(file.empty()||save_as){modal=Modal::Save;input=file.empty()?filename_slug(doc.name):file.filename().string().substr(0,file.filename().string().size()-13);replace_input=true;return false;} + save_machine(doc,file);dirty=false;status="Saved "+file.filename().string();return true; + }catch(const std::exception& e){status=e.what();return false;} + } + std::vector all_fields(){ + std::vector f; + if(inspector>=3)return f; + const auto add=[&](const std::string& name,float& value,float scale,float step,float lo,float hi,int precision=2){f.push_back({name,&value,nullptr,scale,step,lo,hi,precision});}; + const auto toggle=[&](const std::string& name,bool& value){f.push_back({name,nullptr,nullptr,1,1,0,1,0,&value});}; + const auto vector=[&](const std::string& name,Vec3& v,float scale,float step,float lo,float hi){add(name+" X",v.x,scale,step,lo,hi);add(name+" Y",v.y,scale,step,lo,hi);add(name+" Z",v.z,scale,step,lo,hi);}; + if(inspector==2||!part()){ + add("Cells / m",doc.cells_per_meter,1,5,1,500,0);add("Grid mm",grid,1000,5,.001f,1,0);add("Place Y mm",plane_y,1000,10,-10,10,0);return f; + } + if(inspector==1){ + if(link_index>=0&&link_indexkind==MachinePartKind::Pipe&&b->kind==MachinePartKind::Pipe;const auto other=a->kind==MachinePartKind::Pipe?b:a; + if(both)add("Turning loss",l.loss,1,.1f,0,10); + else if(other->kind!=MachinePartKind::Reservoir&&other->kind!=MachinePartKind::Cap&&!(other->kind==MachinePartKind::Junction&&other->junction_method)){ + add("Discharge Cd",l.discharge,1,.05f,.01f,1); + if(l.kind==MachineConnectionKind::Inertance){add("Inertance mm",l.inertance_length,1000,1,.0001f,10);add("Loss",l.loss,1,.1f,0,100);} + } + } + return f; + } + auto& p=*part();vector("Position mm",p.position,1000,10,-100,100);vector("Rotation deg",p.rotation,1,15,-36000,36000); + if(machine_rotational_part(p.kind)){f.clear(); + if(p.kind==MachinePartKind::Shaft){add("Initial RPM",p.shaft.initial_speed,60/(2*pi),100,-10000,10000,0);add("Initial angle deg",p.shaft.initial_angle,180/pi,15,-1000000,1000000,1);add("Inertia kg m2",p.shaft.inertia,1,.01f,.000001f,10000,4);add("Friction Nm",p.shaft.friction_constant,1,.1f,0,100000,3);add("Viscous Nm s",p.shaft.friction_viscous,1,.001f,0,10000,5);add("Windage Nm s2",p.shaft.friction_quadratic,1,.00001f,0,1000,7);add("Starter Nm",p.shaft.starter_torque,1,1,0,10000,1);} + else if(p.kind==MachinePartKind::Flywheel)add("Inertia kg m2",p.shaft.inertia,1,.01f,.000001f,10000,4); + else if(p.kind==MachinePartKind::DriveMotor){if(p.drive.speed_control)add("Target RPM",p.drive.target_speed,60/(2*pi),100,-10000,10000,0);else add("Torque command Nm",p.drive.torque,1,1,-1000000,1000000,2);} + else{add("Brake torque Nm",p.brake.constant,1,1,0,1000000,2);add("Brake viscous Nm s",p.brake.viscous,1,.001f,0,10000,5);add("Brake windage",p.brake.quadratic,1,.00001f,0,1000,7);} + vector("Position mm",p.position,1000,10,-100,100);vector("Rotation deg",p.rotation,1,15,-36000,36000);vector("Size mm",p.size,1000,5,.002f,10);return f; + } + if(p.kind==MachinePartKind::Ecu){auto& e=p.ecu;f.clear(); + add("Idle RPM",e.idle_rpm,1,100,0,30000,0);add("Idle base %",e.idle_base_opening,100,1,0,1);add("Idle max %",e.idle_max_opening,100,1,0,1);add("Idle P gain",e.idle_kp,1,.00001f,0,1,6);add("Idle I gain",e.idle_ki,1,.00001f,0,1,6); + toggle("Rev limiter",e.rev_limit_enabled);add("Limit RPM",e.rev_limit_rpm,1,100,100,50000,0);add("Resume RPM",e.rev_resume_rpm,1,100,0,49999,0);add("Cut time ms",e.rev_limit_cut_time,1000,5,0,10,1);toggle("Limit fuel cut",e.rev_limit_fuel_cut_enabled);toggle("Limit spark cut",e.rev_limit_ignition_cut_enabled); + add("Cruise AFR",e.stoichiometric_afr,1,.1f,5,50);add("Power AFR",e.power_afr,1,.1f,5,50);add("Throttle ms",e.throttle_time_constant,1000,1,.00001f,10,1);add("Throttle /s",e.throttle_rate,1,10,.001f,100000);add("Closing ms",e.throttle_closing_time_constant,1000,1,0,10,1);add("Closing /s",e.throttle_closing_rate,1,10,0,100000); + toggle("Overrun fuel cut",e.overrun_fuel_cut_enabled);toggle("Overrun spark cut",e.overrun_ignition_cut_enabled);add("Overrun RPM",e.overrun_cut_rpm,1,100,0,50000,0);add("Overrun resume",e.overrun_resume_rpm,1,100,0,50000,0);add("Idle spark gain",e.idle_spark_gain,1,.001f,0,1,5);add("Min spark deg",e.idle_minimum_advance,1,1,-90,90); + for(std::size_t i=0;i fields(){auto f=all_fields();if(part()&&std::any_of(doc.links.begin(),doc.links.end(),[&](const auto& l){return l.kind==MachineConnectionKind::Control&&l.b.part==selected;}))std::erase_if(f,[](const auto& field){return field.name=="Opening %"||field.name=="Spark BTDC deg";}); + if(logic_command_owner(doc,selected,LogicCommand::ThrottleOpening))std::erase_if(f,[](const auto& field){return field.name=="Opening %";}); + if(logic_command_owner(doc,selected,LogicCommand::MotorSpeed)||logic_command_owner(doc,selected,LogicCommand::MotorTorque))std::erase_if(f,[](const auto& field){return field.name=="Target RPM"||field.name=="Torque command Nm";}); + for(auto& field:f){const auto& n=field.name; + field.requires_restart=n.starts_with("SD ")||n.starts_with("Injector ")||n.starts_with("Fuel RPM ")||n.starts_with("Fuel TPS ")||n.starts_with("Fuel cell ")||n=="Pickup TDC deg"||inspector==1||n.starts_with("Position ")||n.starts_with("Rotation ")||n.starts_with("Size ")||n.starts_with("Point ")||n.starts_with("Initial ")||n.starts_with("Inlet ")||n.starts_with("Outlet ")||n.starts_with("Intake ")||n.starts_with("Exhaust ")||n=="Inertia kg m2"||n=="Cells / m"||n=="Grid mm"||n=="Place Y mm"||n=="Bore mm"||n=="Stroke mm"||n=="Rod mm"||n=="Compression"||n=="Phase deg"||n=="Max lift mm"||n=="Throat dia mm"||n=="Discharge Cd"||n=="Turning loss"; + }return f;} + void connect_target(MachinePort target){ + if(!pending_port){pending_port=target;selection(target.part);status="Choose a target port, or drag to it. Escape cancels.";return;} + if(*pending_port==target){pending_port.reset();return;} + const auto preview=machine_connection_preview(doc,*pending_port,target); + if(!preview.allowed){status=preview.message;return;} + const auto source=*pending_port;if(edit([&]{doc.connect(source,target);},false)){pending_port.reset();status="Connected. Ctrl+Z undoes the connection.";} + } + bool change_field(Field field,float value){ + if(test&&field.requires_restart){status="Locked during Test: return to Build to change geometry or compiled settings.";return false;} + const bool applied=edit([&]{field.set(std::clamp(value,field.minimum,field.maximum));if(auto p=part();p&&p->kind==MachinePartKind::Cylinder&&(field.name=="Max lift mm"||field.name.find("open deg")!=std::string::npos||field.name.find("close deg")!=std::string::npos)){p->intake_profile.points.clear();p->exhaust_profile.points.clear();}}); + if(applied&&test)status="Applied live: "+field.name; + return applied; + } + void commit_field(){ + if(edit_name.empty())return; + try{std::size_t used=0;const float v=std::stof(edit_buffer,&used);if(used!=edit_buffer.size()||!std::isfinite(v))throw std::invalid_argument("number");for(auto& f:fields())if(f.name==edit_name){if(change_field(f,v/f.scale))edit_name.clear();return;}} + catch(...){status="Enter a finite number, using a decimal point. Escape cancels this edit.";} + } + std::vector actions(sf::Vector2u size); + bool event(const sf::Event&,sf::Vector2u); + bool modal_event(const sf::Event&,sf::Vector2u); + void draw(sf::RenderWindow&,const sf::Font*); + void draw_overlay(sf::RenderWindow&,const sf::Font*); + void draw_meters(sf::RenderWindow&,const sf::Font*); + void draw_modal(sf::RenderWindow&,const sf::Font*); +}; + +std::vector MachineBuilder::Impl::actions(sf::Vector2u size){ + const auto ui=layout(size);const float w=ui.width,h=ui.height,right=ui.inspector_x;std::vector a;refresh_checks();bool inspector_controls=false; + const auto button=[&](float x,float y,float width,const std::string& title,std::function run,bool active=false,bool enabled=true){ + // Existing part-specific controls share the inspector's adaptive columns. + if(inspector_controls&&x>=right+11&&x=170){const float scale=(ui.inspector_width-24)/268;x=right+12+(x-right-12)*scale;width*=scale;} + a.push_back({{{x,y},{width,28}},title,std::move(run),active,enabled}); + }; + button(12,10,58,"Menu",[this]{request(Pending::Exit);}); + button(78,10,48,"New",[this]{request(Pending::New);},false,!test); + button(134,10,52,"Open",[this]{open_library();},false,!test); + button(194,10,52,"Save",[this]{save();}); + button(254,10,52,"Undo",[this]{undo_action();},false,!undo.empty()); + button(314,10,52,"Redo",[this]{redo_action();},false,!redo.empty()); + button(374,10,94,"Assemblies",[this]{open_assemblies();},false,!test); + button(w-248,10,84,"Reset UI",[this]{left_width=260;right_width=340;left_closed=right_closed=false;save_preferences();status="Default panel layout restored.";}); + button(w-156,10,144,test?"Stop / Build [Tab]":"Run Test [Tab]",[this]{if(test)stop();else start();},true); + button(12,54,108,left_closed?"Show library":"Hide library",[this]{left_closed=!left_closed;save_preferences();}); + button(128,54,108,right_closed?"Show inspector":"Hide inspector",[this]{right_closed=!right_closed;save_preferences();}); + const char* views[]{"3D","Logic","Split"};for(int i=0;i<3;++i)button(w-220+i*68,54,62,views[i],[this,i]{change_workspace(i);},workspace_mode==i); + const float vx=std::max(248.f,ui.viewport.position.x); + if(!test&&graph_panels())button(vx,54,128,"Fit graph [Home]",[this,size]{logic_editor.fit(doc.logic,logic_view(size));}); + else if(!test){ + const char* names[]{"Select Q","Move G","Rotate R","Ports C","Route P"}; + for(int i=0;i<5;++i)button(vx+i*70,54,64,names[i],[this,i]{tool=Tool(i);placing.reset();pending_port.reset();},int(tool)==i); + button(vx+350,54,72,snap?"Snap: on":"Snap: off",[this]{snap=!snap;},snap); + button(vx+430,54,60,"Fit",[this]{fit();}); + }else{ + button(vx,54,94,test->paused?"Resume":"Pause",[this]{test->pause();collect();},test->paused); + button(vx+102,54,94,"Step 10 ms",[this]{test->controls(pedal,starter,camera.listener());test->step();collect();},false,test->paused); + button(vx+204,54,80,number(test_speed,2)+"x",[this]{const double speeds[]{1,.5,.25,.1,.01};int i=0;while(i<4&&std::abs(test_speed-speeds[i])>.001)++i;speed(speeds[(i+1)%5]);}); + const auto primary=doc.find(machine_primary_shaft(doc));const bool has_starter=primary&&primary->shaft.starter_torque>0; + button(vx+292,54,98,has_starter?"Hold to start":"No starter",[this]{starter=true;starter_mouse=true;},starter,has_starter); + button(vx+398,54,80,"Meters",[this]{right_closed=false;meters();}); + } + if(graph_panels()){if(scope_visible){const auto v=scope_rect(size);button(v.position.x+v.size.x-38,v.position.y+8,30,"X",[this]{scope_visible=false;});}return a;} + if(!left_closed){ + const float half=(ui.left.size.x-32)/2; + button(20,108,half,"Library",[this]{library_tab=0;library_scroll=0;},library_tab==0); + button(28+half,108,half,"Objects",[this]{library_tab=1;library_scroll=0;},library_tab==1); + if(!library_tab){const char* categories[]{"All","Gas","Engine","Mech","ECU"};const float cw=(ui.left.size.x-48)/5; + for(int i=0;i<5;++i)button(20+i*(cw+6),192,cw,categories[i],[this,i]{library_category=i;library_scroll=0;},library_category==i); + } + const auto items=library_items();const int rows=ui.library_rows();library_scroll=std::clamp(library_scroll,0,std::max(0,int(items.size())-rows)); + for(int row=0;row0); + button(right+220,272,60,"Next",[this]{select_sensor(sensor_index+1);},false,any&&sensor_index+1kind==MachinePartKind::Pipe){for(int direction:{-1,1})button(right+(direction<0?12:228),386,52,direction<0?"- 5%":"+ 5%",[this,direction]{edit([&]{auto& sensor=doc.sensors[sensor_index];auto& tap=sensor_reference&&sensor.kind==MachineSensorKind::DifferentialPressure?sensor.reference:sensor.target;tap.position=std::clamp(tap.position+.05f*direction,0.f,1.f);});},false,!test);} + if(sensor.kind==MachineSensorKind::Crank){for(int direction:{-1,1})button(right+(direction<0?12:228),386,52,direction<0?"-15":"+15",[this,direction]{edit([&]{doc.sensors[sensor_index].angle_offset+=direction*pi/12;});},false,!test);} + button(right+12,426,268,std::string("Bind ")+(sensor_reference&&differential?"reference":"host")+" to selected part",[this]{edit([&]{auto& sensor=doc.sensors[sensor_index];auto& tap=sensor_reference&&sensor.kind==MachineSensorKind::DifferentialPressure?sensor.reference:sensor.target;tap={selected,tap.position,part()->name};});},false,!test&&p&&machine_sensor_supported(*p,sensor.kind,sensor_reference&&differential)); + button(right+12,466,128,"Focus host (F)",[this,id=tap.part]{selection(id);fit(true);inspector=5;},false,host); + button(right+148,466,132,"Remove sensor",[this]{edit([&]{doc.remove_sensor(doc.sensors[sensor_index].id);});sensor_index=std::max(0,sensor_index-1);},false,!test); + } + }else if(inspector==4){ + const char* names[]{"Pressure","Temp","Flow","AFR","Lambda","Charge AFR","RPM","Torque","Power"}; + for(int i=0;i<9;++i){const auto q=MachineQuantity(i);button(right+12+(i%3)*90.f,196+(i/3)*32.f,84,names[i],[this,q]{add_probe(q);},false,doc.probes.size()=MachineQuantity::Rpm||(p&&machine_quantity_supported(*p,q))));} + const bool any=!doc.probes.empty();probe_index=std::clamp(probe_index,0,std::max(0,int(doc.probes.size())-1)); + button(right+12,304,60,"Previous",[this]{select_probe(probe_index-1);},false,any&&probe_index>0); + button(right+220,304,60,"Next",[this]{select_probe(probe_index+1);},false,any&&probe_index+1kind==MachinePartKind::Pipe){button(right+12,390,52,"- 5%",[this]{edit([&]{auto& p=doc.probes[probe_index];p.position=std::max(0.f,p.position-.05f);});});button(right+228,390,52,"+ 5%",[this]{edit([&]{auto& p=doc.probes[probe_index];p.position=std::min(1.f,p.position+.05f);});});} + if(probe.quantity==MachineQuantity::Lambda){button(right+12,426,52,"- 0.1",[this]{edit([&]{auto& p=doc.probes[probe_index];p.reference_afr=std::max(1.f,p.reference_afr-.1f);});});button(right+228,426,52,"+ 0.1",[this]{edit([&]{auto& p=doc.probes[probe_index];p.reference_afr=std::min(50.f,p.reference_afr+.1f);});});} + button(right+12,470,128,"Focus part (F)",[this,part=probe.part]{selection(part);fit(true);inspector=4;},false,owner); + button(right+148,470,132,"Remove probe",[this]{edit([&]{doc.probes.erase(doc.probes.begin()+probe_index);});probe_index=std::max(0,probe_index-1);}); + } + const float windows[]{.05f,.2f,1,5};for(int i=0;i<4;++i)button(right+12+i*68.f,514,62,number(windows[i],windows[i]<1?2:0)+" s",[this,value=windows[i]]{trace_window=value;},trace_window==windows[i]); + button(right+12,554,268,"Export captured CSV",[this]{export_csv(std::filesystem::path("verification/measurements")/(filename_slug(doc.name)+"-"+std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())+".csv"));},false,!trace.empty()&&!recorded_probes.empty()); + button(right+12,590,268,"Clear captured trace",[this]{if(test)test->take_measurements();trace.clear();},false,!trace.empty()); + }else if(inspector==3){ + const int rows=std::max(1,int((h-328)/84));check_scroll=std::clamp(check_scroll,0,std::max(0,int(checks.size())-rows)); + button(right+12,175,60,"< Prev",[this,rows]{check_scroll=std::max(0,check_scroll-rows);},false,check_scroll>0); + button(right+220,175,60,"Next >",[this,rows]{check_scroll+=rows;},false,check_scroll+rowsname,37):doc.name)+"\n"+issue.advice}); + } + }else if(inspector==2||!p){ + const auto primary=machine_primary_shaft(doc);const auto shaft=doc.find(primary); + button(right+12,210,268,shaft?"Readout: "+shaft->name:"Choose a primary shaft",[this]{std::vector shafts;for(const auto& p:doc.parts)if(p.kind==MachinePartKind::Shaft)shafts.push_back(p.id);if(shafts.empty()){status="Add a shaft from the Mechanical category.";return;}auto at=std::find(shafts.begin(),shafts.end(),doc.primary_shaft);const auto next=at==shafts.end()||++at==shafts.end()?shafts.front():*at;edit([&]{doc.primary_shaft=next;});}); + button(right+12,246,128,std::string("Solver: ")+pipe_solver_name(doc.solver),[this]{checkpoint();doc.solver=PipeSolver((int(doc.solver)+1)%4);},false,!test); + button(right+148,246,132,std::string("Scene: ")+scene_name(doc.scene),[this]{checkpoint();doc.scene=OutdoorScene((int(doc.scene)+1)%5);},false,!test); + button(right+12,h-110,268,"Rename machine",[this]{rename_part=false;modal=Modal::Rename;input=doc.name;replace_input=true;},false,!test); + }else if(inspector==0){ + if(p->kind==MachinePartKind::DriveMotor||p->kind==MachinePartKind::Brake||p->kind==MachinePartKind::Throttle||p->kind==MachinePartKind::Carburetor||(p->kind==MachinePartKind::Shaft&&p->shaft.starter_torque>0))button(right+ui.inspector_width-92,101,80,"Command",[this,size]{show_command_logic(selected,size);}); + a.push_back({{{right+12,170},{ui.inspector_width-24,22}},p->name,[this]{rename_part=true;modal=Modal::Rename;input=part()->name;replace_input=true;},false,!test}); + if(machine_rotational_part(p->kind)||p->kind==MachinePartKind::Cylinder||p->kind==MachinePartKind::PistonPump){ + if(p->kind==MachinePartKind::DriveMotor){const auto owner=logic_command_owner(doc,p->id,LogicCommand::MotorEnable);button(right+12,210,268,p->drive.speed_control?"Mode: ideal speed drive":"Mode: torque motor",[this]{edit([&]{part()->drive.speed_control=!part()->drive.speed_control;});field_scroll=0;edit_name.clear();});button(right+12,246,268,owner?"Enable: signal / "+owner->name:p->drive.enabled?"Motor: enabled":"Motor: disabled",[this]{edit([&]{part()->drive.enabled=!part()->drive.enabled;});},false,!owner);} + else if(p->kind==MachinePartKind::Brake){const auto owner=logic_command_owner(doc,p->id,LogicCommand::BrakeEnable);button(right+12,210,268,owner?"Enable: signal / "+owner->name:p->brake.enabled?"Brake: enabled":"Brake: disabled",[this]{edit([&]{part()->brake.enabled=!part()->brake.enabled;});},false,!owner);} + else if(p->kind==MachinePartKind::Shaft)button(right+12,210,268,doc.primary_shaft==p->id?"Primary readout shaft":"Use as primary readout",[this]{edit([&]{doc.primary_shaft=selected;});},doc.primary_shaft==p->id); + if(p->kind!=MachinePartKind::DriveMotor&&p->kind!=MachinePartKind::Shaft){const auto attached=doc.find(machine_shaft_for(doc,p->id));button(right+12,246,268,attached?"Shaft: "+attached->name:"No shaft / connect gold ports",[this]{const auto shaft=machine_shaft_for(doc,selected);if(shaft){selection(shaft);fit(true);}},false,attached);} + }else if(p->kind==MachinePartKind::Pipe){ + button(right+12,210,60,"< Point",[this]{point_index=std::max(0,point_index-1);},false,!test); + button(right+160,210,60,"Point >",[this]{point_index=std::min(int(part()->points.size())-1,point_index+1);},false,!test); + button(right+228,210,52,"Route",[this]{tool=Tool::Route;},tool==Tool::Route,!test); + button(right+12,246,128,"Add path point",[this]{auto p=part();if(!p||p->points.size()>=128)return;edit([&]{int i=std::clamp(point_index,0,int(p->points.size())-2);auto v=multiply(add(p->points[i],p->points[i+1]),.5f);machine_insert_pipe_point(*p,i,v);point_index=i+1;tool=Tool::Route;});},false,!test); + button(right+148,246,132,"Remove point",[this]{auto p=part();if(!p||point_index<=0||point_index>=int(p->points.size())-1)return;edit([&]{machine_remove_pipe_point(*p,point_index);--point_index;});},false,!test&&p->points.size()>2&&point_index>0&&point_indexpoints.size())-1); + }else if(p->kind==MachinePartKind::Junction){ + const char* mode[]{"Stored volume","Characteristic node (0 L)","Legacy node (0 L)"}; + button(right+12,210,268,mode[p->junction_method],[this]{edit([this]{auto p=part();p->junction_method=(p->junction_method+1)%3;p->port_coordinates.clear();p->port_keys.clear(); + for(auto& link:doc.links){MachinePort* node=nullptr;MachinePort other;if(link.a.part==p->id){node=&link.a;other=link.b;}else if(link.b.part==p->id){node=&link.b;other=link.a;}if(!node)continue; + if(p->junction_method){node->port=0;continue;}auto pipe=doc.find(other.part);auto path=machine_pipe_path(*pipe);const auto direction=inverse_rotate(unit(subtract(path[other.port?path.size()-2:1],p->position)),p->rotation);const float denominator=std::max({std::abs(direction.x),std::abs(direction.y),std::abs(direction.z)});node->port=unsigned(p->port_coordinates.size());p->port_coordinates.push_back(multiply(direction,.5f/std::max(denominator,1e-6f))); + } + });},false,!test); + }else if(p->kind==MachinePartKind::Chamber){ + button(right+12,210,268,p->injector?"Injector: attached / remove":"Attach fuel injector",[this]{edit([&]{if(part()->injector)part()->injector.reset();else part()->injector=MachineInjectorConfig{};});},false,!test); + if(p->injector){const auto ecu=doc.find(p->injector->ecu);button(right+12,246,268,ecu?"Fuel ECU: "+ecu->name:"Fuel ECU: choose EFI target",[this]{edit([&]{auto& c=*part()->injector;std::vector ids{0};for(const auto& target:doc.parts)if(target.kind==MachinePartKind::Ecu&&target.sensor_ecu.enabled&&target.sensor_ecu.strategy!=SensorEcuConfig::Strategy::Ignition)ids.push_back(target.id);auto at=std::find(ids.begin(),ids.end(),c.ecu);c.ecu=at==ids.end()||++at==ids.end()?0:*at;if(const auto target=doc.find(c.ecu))c.ecu_label=target->name;});},false,!test);} + }else if(p->kind==MachinePartKind::Throttle||p->kind==MachinePartKind::Carburetor){const auto owner=logic_command_owner(doc,p->id,LogicCommand::ThrottleOpening);bool controlled=false;for(auto& l:doc.links)if(l.kind==MachineConnectionKind::Control&&l.b.part==p->id)controlled=true;button(right+12,210,268,owner?"Opening: signal / "+owner->name:controlled?"Opening: assigned ECU":p->pedal_control?"Control: test pedal":"Control: fixed opening",[this]{edit([&]{part()->pedal_control=!part()->pedal_control;});},false,!controlled&&!owner);} + else if(p->kind==MachinePartKind::Ecu){ + button(right+12,210,268,!p->sensor_ecu.enabled?"ECU: Legacy ideal":std::string("ECU: ")+sensor_ecu_strategy_name(p->sensor_ecu.strategy),[this]{edit([&]{auto& p=*part();if(!p.sensor_ecu.enabled){p.sensor_ecu.enabled=true;p.sensor_ecu.strategy=SensorEcuConfig::Strategy::Ignition;}else if(p.sensor_ecu.strategy==SensorEcuConfig::Strategy::SpeedDensity)p.sensor_ecu.enabled=false;else p.sensor_ecu.strategy=next_sensor_ecu_strategy(p.sensor_ecu.strategy);if(p.sensor_ecu.enabled)std::erase_if(doc.links,[&](const auto& link){return link.kind==MachineConnectionKind::Control&&link.a.part==p.id&&doc.find(link.b.part)->kind!=MachinePartKind::Cylinder;});for(auto& node:doc.logic.nodes)if(node.kind==LogicNodeKind::Ecu&&node.part==p.id){node.ecu_strategy=p.sensor_ecu.strategy;if(!p.sensor_ecu.enabled)node.part=0;std::erase_if(doc.logic.links,[&](const auto& wire){return wire.to.node==node.id&&(wire.to.port=="tps"||wire.to.port=="map"||wire.to.port=="iat");});}});field_scroll=0;edit_name.clear();},false,!test); + if(p->sensor_ecu.enabled)button(right+12,246,268,"Show ECU connector in Logic",[this,size]{show_ecu_logic(selected,size);}); + else {button(right+12,246,128,p->injection_enabled?"Injection: ON":"Injection: OFF",[this]{edit([&]{part()->injection_enabled=!part()->injection_enabled;});},false,!test);button(right+148,246,132,p->ecu.idle_control==EcuConfig::IdleControl::Feedback?"Idle: feedback":"Idle: fixed",[this]{edit([&]{auto& e=part()->ecu;e.idle_control=e.idle_control==EcuConfig::IdleControl::Feedback?EcuConfig::IdleControl::FixedOpening:EcuConfig::IdleControl::Feedback;});});} + } + button(right+12,h-110,128,"Duplicate Ctrl+D",[this]{duplicate();},false,!test); + button(right+148,h-110,132,"Delete Del",[this]{remove();},false,!test); + }else if(inspector==1){ + std::vector connected;for(std::size_t i=0;i",[this,connected]{auto i=std::find(connected.begin(),connected.end(),link_index);link_index=connected[(i-connected.begin()+1)%connected.size()];},false,!test); + const auto& link=doc.links[link_index];const bool both=doc.find(link.a.part)->kind==MachinePartKind::Pipe&&doc.find(link.b.part)->kind==MachinePartKind::Pipe; + const auto other=doc.find(doc.find(link.a.part)->kind==MachinePartKind::Pipe?link.b.part:link.a.part); + const bool boundary=machine_link_domain(doc,link)!=PortDomain::Gas||(!both&&(other->kind==MachinePartKind::Reservoir||other->kind==MachinePartKind::Cap||(other->kind==MachinePartKind::Junction&&other->junction_method))); + button(right+12,246,268,machine_link_domain(doc,link)!=PortDomain::Gas?machine_connection_name(link.kind):boundary?"Component boundary method":both?(link.kind==MachineConnectionKind::Legacy?"Legacy junction":"Characteristic junction"):machine_connection_name(link.kind),[this,both]{checkpoint();auto& l=doc.links[link_index];l.kind=both?(l.kind==MachineConnectionKind::Legacy?MachineConnectionKind::Characteristic:MachineConnectionKind::Legacy):(l.kind==MachineConnectionKind::Inertance?MachineConnectionKind::Orifice:MachineConnectionKind::Inertance);},false,!test&&!boundary); + button(right+12,h-110,268,"Disconnect selected link",[this]{checkpoint();doc.links.erase(doc.links.begin()+link_index);link_index=-1;},false,!test); + } + } + } + inspector_controls=false; + if(scope_visible){const auto v=scope_rect(size);button(v.position.x+v.size.x-38,v.position.y+8,30,"X",[this]{scope_visible=false;});} + if(context_menu){ + auto p=part(); + auto pos=*context_menu;pos.x=std::clamp(pos.x,view(size).position.x,std::max(view(size).position.x,w-210));pos.y=std::clamp(pos.y,92.f,std::max(92.f,h-420)); + const std::string titles[]{"Configure part","Move","Rotate","Copy Ctrl+C","Paste Ctrl+V","Duplicate Ctrl+D","Save as assembly...","Delete"}; + for(int i=0;i<8;++i)button(pos.x,pos.y+i*33.f,184,titles[i],[this,i]{context_menu.reset();pending_port.reset();inspector=0;if(i==1)tool=Tool::Move;else if(i==2)tool=Tool::Rotate;else if(i==3)copy_selection();else if(i==4)paste();else if(i==5)duplicate();else if(i==6)save_assembly();else if(i==7)remove();},false,(!test||i==0)&&(i!=4||clipboard.has_value())); + if(p&&p->kind==MachinePartKind::Pipe&&selected_ids.size()==1){const auto at=*context_menu;button(pos.x,pos.y+264,184,"Insert bend here",[this,at,size]{context_menu.reset();pending_port.reset();inspector=0;insert_bend(at,size);},false,!test);} + if(p){const auto at=*context_menu;button(pos.x,pos.y+(p->kind==MachinePartKind::Pipe?297:264),184,"Add sensor here...",[this,at,size]{context_menu.reset();pending_port.reset();right_closed=false;inspector=5;if(const auto p=part();p&&p->kind==MachinePartKind::Pipe)if(const auto location=builder_pipe_location(camera,view(size),*p,at)){float distance=length(subtract(location->local_point,p->points[location->segment]));for(std::size_t i=0;isegment;++i)distance+=length(subtract(p->points[i+1],p->points[i]));sensor_add_position=std::clamp(distance/machine_pipe_length(*p),0.f,1.f);}status="Choose a supported sensor. Pipe taps use the clicked arclength location.";},false,!test);} + } + return a; +} + +bool MachineBuilder::Impl::modal_event(const sf::Event& event,sf::Vector2u size){ + const float w=float(size.x),h=float(size.y),x=w*.5f-280,y=h*.5f-185;const auto cancel=[&]{modal=Modal::None;save_then_pending=false;}; + const auto submit=[&]{ + try{ + if(modal==Modal::Rename){if(input.empty()||input.size()>120)throw std::invalid_argument("Use a name with 1..120 characters");checkpoint();if(rename_part&&part())part()->name=input;else doc.name=input;modal=Modal::None;} + else if(modal==Modal::Save){auto path=machine_directory()/(filename_slug(input)+".machine.toml");save_machine(doc,path);file=path;dirty=false;modal=Modal::None;status="Saved "+path.filename().string();if(save_then_pending){save_then_pending=false;execute_pending();}} + else if(modal==Modal::SaveAssembly){if(input.empty()||input.size()>100)throw std::invalid_argument("Use an assembly name with 1..100 characters");auto assembly=machine_extract_assembly(doc,selection_parts(),input);auto path=machine_assembly_directory()/(filename_slug(input)+".assembly.toml");save_machine(assembly,path);files=machine_assembly_files();modal=Modal::None;status="Saved reusable assembly "+path.filename().string();} + }catch(const std::exception& e){status=e.what();} + }; + if(event.is()){request(Pending::CloseWindow);return true;} + if(auto key=event.getIf()){ + if(key->code==sf::Keyboard::Key::Escape){cancel();return true;} + if(key->code==sf::Keyboard::Key::Enter){submit();return true;} + if(key->code==sf::Keyboard::Key::Backspace){if(replace_input)input.clear();else if(!input.empty())input.pop_back();replace_input=false;return true;} + } + if(auto text=event.getIf();text&&(modal==Modal::Save||modal==Modal::SaveAssembly||modal==Modal::Rename)){ + if(text->unicode>=32&&text->unicode<127){if(replace_input)input.clear();replace_input=false;if(input.size()<100)input+=char(text->unicode);}return true; + } + if(auto wheel=event.getIf();wheel&&(modal==Modal::Open||modal==Modal::Assemblies)){file_scroll=std::clamp(file_scroll+(wheel->delta<0?1:-1),0,std::max(0,int(files.size())-5));return true;} + if(auto click=event.getIf();click&&click->button==sf::Mouse::Button::Left){const sf::Vector2f pos(click->position); + if(modal==Modal::Unsaved){ + if(sf::FloatRect({x+20,y+250},{160,36}).contains(pos)){save_then_pending=true;if(save())execute_pending();} + else if(sf::FloatRect({x+200,y+250},{160,36}).contains(pos)){dirty=false;execute_pending();} + else if(sf::FloatRect({x+380,y+250},{160,36}).contains(pos))cancel(); + }else if(modal==Modal::Import){ + if(sf::FloatRect({x+20,y+326},{520,30}).contains(pos)){cancel();return true;} + for(std::size_t row=0;row();key&&(key->code==sf::Keyboard::Key::LShift||key->code==sf::Keyboard::Key::RShift)){shift_held=true;return true;} + if(auto key=event.getIf();key&&(key->code==sf::Keyboard::Key::LShift||key->code==sf::Keyboard::Key::RShift)){shift_held=false;return true;} + if(auto key=event.getIf();key&&(key->code==sf::Keyboard::Key::LControl||key->code==sf::Keyboard::Key::RControl)){control_held=true;return true;} + if(auto key=event.getIf();key&&(key->code==sf::Keyboard::Key::LControl||key->code==sf::Keyboard::Key::RControl)){control_held=false;return true;} + if(modal!=Modal::None)return modal_event(event,size); + if(event.is()){request(Pending::CloseWindow);return true;} + if(event.is()){auto h=logic_hooks();logic_editor.deactivate(h);orbit=pan=pedal_drag=shift_held=control_held=box_select=port_drag=false;resize_panel=0;save_preferences();finish_drag();starter=false;starter_mouse=false;if(test)test->release_logic_buttons();return true;} + if(auto press=event.getIf()){ + const auto ui=layout(size);const auto pos=sf::Vector2f(press->position); + if(workspace_mode==2){if(logic_view(size).contains(pos)){commit_field();logic_focus=true;edit_name.clear();}else if(view(size).contains(pos)&&logic_focus){auto h=logic_hooks();if(logic_editor.editing_text()&&logic_editor.event(event,h,ui,logic_view(size),true))return true;logic_editor.deactivate(h);logic_focus=false;}} + if(press->button==sf::Mouse::Button::Left&&!graph_panels()&&!left_closed&&ui.search.contains(pos)){commit_field();search_focus=true;search_replace=false;return true;} + search_focus=false; + if(press->button==sf::Mouse::Button::Left&&pos.y>=96&&pos.y=ui.viewport.position.x&&pos.x<=ui.viewport.position.x+ui.viewport.size.x){resize_panel=3;return true;} + if(std::abs(pos.x-(ui.left.position.x+ui.left.size.x+3))<5&&!left_closed){resize_panel=1;return true;} + if(std::abs(pos.x-(ui.inspector_x-4))<5&&!right_closed){resize_panel=2;return true;} + } + } + if(workspace_mode&&!resize_panel){ + if(const auto key=event.getIf();key&&key->control&&key->code==sf::Keyboard::Key::F&&graph_panels())left_closed=false; + bool dispatch=graph_panels()||logic_editor.active_gesture(); + if(const auto moved=event.getIf())dispatch|=logic_view(size).contains(sf::Vector2f(moved->position)); + if(const auto wheel=event.getIf())dispatch|=logic_view(size).contains(sf::Vector2f(wheel->position)); + if(const auto press=event.getIf())dispatch|=logic_view(size).contains(sf::Vector2f(press->position)); + if(dispatch){auto h=logic_hooks();if(logic_editor.event(event,h,layout(size),logic_view(size),graph_panels())){if(const auto moved=event.getIf())mouse=previous_mouse=moved->position;return true;}} + } + if(search_focus){ + if(auto key=event.getIf()){ + if(key->code==sf::Keyboard::Key::Escape||key->code==sf::Keyboard::Key::Enter)search_focus=false; + else if(key->code==sf::Keyboard::Key::Backspace&&!search_text.empty()){if(search_replace)search_text.clear();else search_text.pop_back();search_replace=false;library_scroll=0;} + return true; + } + if(auto typed=event.getIf()){if(typed->unicode>=32&&typed->unicode<127){if(search_replace){search_text.clear();search_replace=false;}if(search_text.size()<80)search_text+=char(typed->unicode);library_scroll=0;}return true;} + } + if(!edit_name.empty()){ + if(auto key=event.getIf()){ + if(key->code==sf::Keyboard::Key::Escape){edit_name.clear();return true;} + if(key->code==sf::Keyboard::Key::Enter){commit_field();return true;} + if(key->code==sf::Keyboard::Key::Backspace){if(replace_input)edit_buffer.clear();else if(!edit_buffer.empty())edit_buffer.pop_back();replace_input=false;return true;} + } + if(auto text=event.getIf()){const auto c=text->unicode;if((c>='0'&&c<='9')||c=='-'||c=='+'||c=='.'||c=='e'||c=='E'){if(replace_input)edit_buffer.clear();replace_input=false;if(edit_buffer.size()<24)edit_buffer+=char(c);}return true;} + if(event.is())commit_field();else if(event.is())return true; + } + if(auto key=event.getIf()){ + using K=sf::Keyboard::Key;const auto k=key->code; + if(key->control){if(k==K::F){left_closed=false;search_focus=true;search_replace=true;return true;}if(k==K::A&&!test){selected_ids.clear();for(auto& p:doc.parts)selected_ids.insert(p.id);selected=selected_ids.empty()?0:*selected_ids.begin();return true;}if(k==K::S){save(key->shift);return true;}if(k==K::O&&!test){open_library();return true;}if(k==K::Z){if(key->shift)redo_action();else undo_action();return true;}if(k==K::Y){redo_action();return true;}if(k==K::C&&!test){copy_selection();return true;}if(k==K::V&&!test){paste();return true;}if(k==K::D){duplicate();return true;}if(k==K::N&&!test){request(Pending::New);return true;}} + if(k==K::Escape){if(placing||pending_port||context_menu||tool==Tool::Connect){placing.reset();pending_port.reset();context_menu.reset();if(tool==Tool::Connect){tool=Tool::Move;status="Move: drag a part or its X/Y/Z handles. Press C to connect ports.";}return true;}if(selected){selection(0);return true;}status="Workshop stays open. Use Menu to return to the scenario menu, or close the app window.";return true;} + if(k==K::Tab){if(test)stop();else start();return true;}if(k==K::Home){fit();return true;} + if(k==K::F){fit(true);return true;}if(k==K::H){if(graph_panels())change_workspace(0);right_closed=false;inspector=inspector==3?0:3;edit_name.clear();return true;}if(k==K::M){if(inspector==4&&!graph_panels()){inspector=0;scope_visible=false;}else meters();return true;} + if(test){if(k==K::Space){test->pause();collect();}else if(k==K::PageDown)speed(test_speed*.5);else if(k==K::PageUp)speed(test_speed*2);else if(k==K::W||k==K::Up)pedal=std::min(1.f,pedal+.05f);else if(k==K::S||k==K::Down)pedal=std::max(0.f,pedal-.05f);else if(k==K::V)starter=true;return true;} + if(k==K::I){modal=Modal::Import;return true;} + if(k==K::Delete){remove();return true;}if(k==K::Q)tool=Tool::Select;else if(k==K::G){tool=Tool::Move;status="Move: drag a part or its X/Y/Z handles. Press C to connect ports.";}else if(k==K::R)tool=Tool::Rotate;else if(k==K::C){tool=Tool::Connect;status="Connect: hover a target for a green/red preview. Esc or G returns to Move.";}else if(k==K::P){tool=Tool::Route;status="Route: Ctrl-click the pipe centerline to insert a bend, then drag its handles.";}else if(k==K::X)snap=!snap;else return false; + placing.reset();pending_port.reset();return true; + } + if(auto key=event.getIf();key&&key->code==sf::Keyboard::Key::V)starter=false; + if(auto moved=event.getIf()){ + mouse=moved->position;auto delta=mouse-previous_mouse;previous_mouse=mouse; + if(resize_panel){if(resize_panel==1)left_width=std::clamp(float(mouse.x)-8,220.f,420.f);else if(resize_panel==2)right_width=std::clamp(float(size.x-mouse.x)-8,280.f,520.f);else trace_height=std::clamp(float(size.y-mouse.y)-58,170.f,440.f);return true;} + if(pedal_drag){const auto v=view(size);pedal=std::clamp((mouse.x-v.position.x-120)/std::max(100.f,v.size.x-160),0.f,1.f);} + else if(orbit)camera.orbit(delta);else if(pan)camera.pan(delta,view(size).size.y);else if(drag.axis>=0)move_drag(sf::Vector2f(mouse),size); + return true; + } + if(auto release=event.getIf()){ + if(release->button==sf::Mouse::Button::Left&&resize_panel){resize_panel=0;save_preferences();return true;} + if(release->button==sf::Mouse::Button::Left&&port_drag){ + port_drag=false; + if(screen_distance(sf::Vector2f(port_drag_start),sf::Vector2f(release->position))>5&&view(size).contains(sf::Vector2f(release->position))){ + if(auto target=pick_port(sf::Vector2f(release->position),size,false);target&&pending_port&&*target!=*pending_port)connect_target(*target); + }return true; + } + if(release->button==sf::Mouse::Button::Left&&box_select){box_select=false;const float x=std::min(box_start.x,release->position.x),y=std::min(box_start.y,release->position.y);const sf::FloatRect region{{x,y},{float(std::abs(box_start.x-release->position.x)),float(std::abs(box_start.y-release->position.y))}}; + for(auto& p:doc.parts){auto point=p.kind==MachinePartKind::Pipe?GeometryPath{machine_pipe_path(p)}.position(machine_pipe_length(p)*.5f):p.position;if(auto screen=builder_project(camera,view(size),point);screen&®ion.contains(*screen))selected_ids.insert(p.id);}selected=selected_ids.empty()?0:*selected_ids.rbegin();return true;} + if(release->button==sf::Mouse::Button::Left){finish_drag();orbit=false;pedal_drag=false;if(starter_mouse){starter=false;starter_mouse=false;}} + if(release->button==sf::Mouse::Button::Right)orbit=false; + if(release->button==sf::Mouse::Button::Middle)pan=false; + return true; + } + if(auto wheel=event.getIf()){ + const sf::Vector2f pos(wheel->position); + if(view(size).contains(pos))camera.distance=std::clamp(camera.distance*std::exp(-wheel->delta*.12f),.08f,250.f); + else if(!left_closed&&layout(size).library.contains(pos)){ + const auto items=library_items();library_scroll=std::clamp(library_scroll+(wheel->delta<0?1:-1),0,std::max(0,int(items.size())-layout(size).library_rows())); + }else if(!right_closed&&layout(size).right.contains(pos)){ + if(inspector==3){refresh_checks();const int rows=std::max(1,int((float(size.y)-328)/84));check_scroll=std::clamp(check_scroll+(wheel->delta<0?1:-1),0,std::max(0,int(checks.size())-rows));} + else{auto f=fields();field_scroll=std::clamp(field_scroll+(wheel->delta<0?1:-1),0,std::max(0,int(f.size())-layout(size).field_rows()));} + } + return true; + } + if(auto press=event.getIf()){ + mouse=previous_mouse=press->position;const sf::Vector2f screen(mouse);const auto viewport=view(size); + if(press->button==sf::Mouse::Button::Left){auto list=actions(size);for(auto i=list.rbegin();i!=list.rend();++i)if(i->rect.contains(screen)){if(i->enabled)i->run();return true;}} + context_menu.reset(); + if(!viewport.contains(screen)){ + const auto ui=layout(size); + if(press->button==sf::Mouse::Button::Left&&!right_closed&&ui.right.contains(screen)){ + auto f=fields();const int row=int((screen.y-WorkshopLayout::field_top)/WorkshopLayout::field_height),index=field_scroll+row; + if(screen.y>=WorkshopLayout::field_top&&row>=0&&row=value.position.y){ + if(screen.x>=value.position.x+value.size.x+38)change_field(field,field.get()+field.step/field.scale); + else if(screen.x>=value.position.x+value.size.x+6)change_field(field,field.get()-field.step/field.scale); + else if(value.contains(screen)){if(field.boolean)change_field(field,field.get()?0:1);else{edit_name=field.name;edit_buffer=number(field.get()*field.scale,field.precision);replace_input=true;}} + }return true; + } + } + return true; + } + if(press->button==sf::Mouse::Button::Middle){pan=true;return true;} + const bool alt=sf::Keyboard::isKeyPressed(sf::Keyboard::Key::LAlt)||sf::Keyboard::isKeyPressed(sf::Keyboard::Key::RAlt); + if(press->button==sf::Mouse::Button::Right){if(alt){orbit=true;return true;}auto id=pick(screen,size);if(id){if(!selected_ids.contains(id))selection(id);else{selected=id;field_scroll=point_index=0;link_index=-1;edit_name.clear();inspector=0;}context_menu=screen;}else orbit=true;return true;} + if(press->button!=sf::Mouse::Button::Left)return true; + if(alt){orbit=true;return true;} + if(!placing&&!pending_port&&tool!=Tool::Route)for(std::size_t i=0;ipart)); + if(tool==Tool::Connect||pending_port||(visible_port&&tool!=Tool::Route)){ + if(port_hit){const bool beginning=!pending_port;connect_target(*port_hit);if(beginning&&pending_port){port_drag=true;port_drag_start=mouse;}return true;} + if(tool==Tool::Connect){selection(pick(screen,size));return true;} + pending_port.reset(); + } + if(tool==Tool::Route&&(control_held||sf::Keyboard::isKeyPressed(sf::Keyboard::Key::LControl)||sf::Keyboard::isKeyPressed(sf::Keyboard::Key::RControl))){insert_bend(screen,size);return true;} + if(begin_drag(screen,size))return true; + if(tool==Tool::Route&&part()&&part()->kind==MachinePartKind::Pipe&&selected_ids.size()==1){auto& p=*part();for(std::size_t i=0;i1)segment(w,*anchor,*at,probe_colors[i]);circle(w,*at,7,background,probe_colors[i]);label(w,font,"P"+std::to_string(i+1),at->x+10,at->y-17,11,probe_colors[i]);} + for(std::size_t i=0;ix-5,at->y-5},{10,10}},inspector==5&&sensor_index==int(i)?color:background,color);label(w,font,"S"+std::to_string(sensor.id),at->x+8,at->y-7,10,color); + if(inspector==5&&sensor_index==int(i)&&sensor.kind==MachineSensorKind::DifferentialPressure)if(const auto reference=machine_sensor_position(doc,sensor.reference))if(const auto at=project(*reference)){circle(w,*at,6,background,color);label(w,font,"reference (-)",at->x+10,at->y-20,10,color);} + } + if(!test&&(tool==Tool::Connect||pending_port||selected)){ + for(auto& p:doc.parts)if(tool==Tool::Connect||pending_port||selected_ids.contains(p.id))for(unsigned i=0;i0)continue; + if(auto pos=project(machine_port_position(p,i))){const auto descriptor=machine_port_descriptor(p,i);const bool pending=pending_port&&*pending_port==e;bool candidate=true; + if(pending_port&&!pending){const auto source=doc.find(pending_port->part);candidate=source->id!=p.id&&port_compatibility(machine_port_descriptor(*source,pending_port->port),descriptor).allowed&&(descriptor.domain!=PortDomain::Gas||source->kind==MachinePartKind::Pipe||p.kind==MachinePartKind::Pipe);} + const auto color=candidate?port_color(descriptor.domain):border;const bool physical=descriptor.direction==PortDirection::Bidirectional; + circle(w,*pos,physical?4.f:5.5f,filled_port(descriptor)&&candidate?color:background,color); + if(physical){segment(w,*pos+sf::Vector2f{-7,0},*pos+sf::Vector2f{-4,0},color);segment(w,*pos+sf::Vector2f{4,0},*pos+sf::Vector2f{7,0},color);if(linked(e))circle(w,*pos,1.3f,color);} + if(pending)circle(w,*pos,9,sf::Color::Transparent,amber);} + } + if(pending_port){if(auto p=doc.find(pending_port->part))if(auto start=project(machine_port_position(*p,pending_port->port))){auto end=sf::Vector2f(mouse);if(connection_hover)if(auto target=project(machine_port_position(*doc.find(connection_hover->part),connection_hover->port)))end=*target; + const auto color=port_color(machine_port_descriptor(*p,pending_port->port).domain);segment(w,*start,end,color);if(connection_preview)circle(w,end,10,sf::Color::Transparent,connection_preview->allowed?accent:invalid);}} + auto hovered=pending_port?pick_port(sf::Vector2f(mouse),size,false):pick_port(sf::Vector2f(mouse),size,true);if(!hovered)hovered=pick_port(sf::Vector2f(mouse),size,false); + if(auto port=hovered){auto p=doc.find(port->part);auto pos=project(machine_port_position(*p,port->port));if(pos){const float x=std::clamp(pos->x+12,viewport.position.x+8,viewport.position.x+viewport.size.x-268),y=std::clamp(pos->y-48,viewport.position.y+8,viewport.position.y+viewport.size.y-110); + box(w,{{x,y},{260,40}},background);label(w,font,short_text(p->name,35),x+7,y+5,11);label(w,font,machine_port_name(*p,port->port),x+7,y+22,10,muted);}} + } + if(!test&&(tool==Tool::Connect||pending_port)){const float y=viewport.position.y+viewport.size.y-61;box(w,{{viewport.position.x+12,y},{viewport.size.x-24,49}},background); + const auto message=connection_preview?std::string(connection_preview->allowed?"READY: ":"CANNOT CONNECT: ")+connection_preview->message:pending_port?"Choose a target. Air parts connect through pipes; blue pins connect ECU controls.":"Click or drag a port to connect. Neutral rings = gas; blue pins = ECU assignment."; + wrapped_label(w,font,message,viewport.position.x+22,y+9,viewport.size.x-44,12,2,connection_preview&&!connection_preview->allowed?invalid:accent); + } + if(part()&&!test){ + const auto origin=gizmo_origin();const float radius=gizmo_length(size);const auto center=project(origin); + if(center&&(tool==Tool::Move||tool==Tool::Route||tool==Tool::Rotate)){ + for(int axis=0;axis<3;++axis){auto color=drag.axis==axis?sf::Color::White:axis_color[axis]; + if(tool==Tool::Rotate){for(int i=0;i<96;++i){const auto ring=[&](int k){float a=2*pi*k/96;return add(origin,add(multiply(axes[(axis+1)%3],radius*std::cos(a)),multiply(axes[(axis+2)%3],radius*std::sin(a))));};auto a=project(ring(i)),b=project(ring(i+1));if(a&&b)segment(w,*a,*b,color);}} + else if(auto end=project(add(origin,multiply(axes[axis],radius)))){segment(w,*center,*end,color);circle(w,*end,4,color);label(w,font,std::string(1,"XYZ"[axis]),end->x+7,end->y-8,13,color);} + }circle(w,*center,3,amber); + } + if(tool==Tool::Route&&part()->kind==MachinePartKind::Pipe&&selected_ids.size()==1){auto& p=*part(); + const auto diameters=machine_pipe_diameters(p);for(std::size_t i=0;ix+9,pos->y+6,11,active?amber:text_color);} + } + } + if(box_select){const float x=float(std::min(box_start.x,mouse.x)),y=float(std::min(box_start.y,mouse.y));box(w,{{x,y},{float(std::abs(mouse.x-box_start.x)),float(std::abs(mouse.y-box_start.y))}},sf::Color(50,150,200,40),accent);} + if(placing){const auto p=placement(sf::Vector2f(mouse),size);if(auto at=project(p))label(w,font,std::string(machine_part_name(*placing))+" "+number(p.x*1000,0)+", "+number(p.y*1000,0)+", "+number(p.z*1000,0)+" mm",at->x+16,at->y+12,11,accent);} + if(doc.parts.empty()){ + if(test)fitted_label(w,font,"Logic-only simulation / inspect signals in Logic",viewport.position.x+16,viewport.position.y+92,viewport.size.x-32,12,muted); + else{fitted_label(w,font,doc.logic.nodes.empty()?"BUILD A MACHINE":"LOGIC-ONLY MACHINE",viewport.position.x+32,viewport.position.y+40,viewport.size.x-64,22);fitted_label(w,font,doc.logic.nodes.empty()?"Choose a part on the left, then click to place it.":"Open Logic to edit this machine's signal graph.",viewport.position.x+32,viewport.position.y+80,viewport.size.x-64,13,muted);fitted_label(w,font,"Try an example bench to explore a working circuit.",viewport.position.x+32,viewport.position.y+107,viewport.size.x-64,13,muted);} + } + label(w,font,test?"TEST / LIVE SIMULATION":"BUILD / TRUE SIZE",viewport.position.x+16,viewport.position.y+12,11,test?accent:muted); + if(test){label(w,font,number(live.time,3)+" s "+(live.primary_shaft?number(live.rpm,0)+" RPM":"No primary shaft")+" "+number(test_speed,2)+"x"+(test->paused?" PAUSED":"")+" "+std::to_string(live.cells)+" cells",viewport.position.x+16,viewport.position.y+34,12); + bool fuel=false,cut=false,floating=false;unsigned cylinders=0;for(auto& p:live.parts){if(doc.find(p.id)->kind==MachinePartKind::Cylinder){++cylinders;fuel|=p.fuel_source;}cut|=p.fuel_cut||p.ignition_cut;floating|=p.valve_float;} + bool torque_drive=false,speed_drive=false;for(const auto& runtime:live.parts){const auto p=doc.find(runtime.id);if(p->kind==MachinePartKind::DriveMotor&&runtime.actuator_enabled&&runtime.shaft==live.primary_shaft){if(p->drive.speed_control)speed_drive=true;else torque_drive=true;}} + const auto state=std::string(speed_drive?"SPEED CONTROLLED":torque_drive?"TORQUE DRIVEN":live.primary_shaft?"FREE SHAFT":"LOGIC / NO PRIMARY SHAFT")+(cut?" / FUEL OR SPARK CUT":"")+(floating?" / VALVE FLOAT":"")+(cylinders&&!fuel?" / NO FUEL SOURCE":"");label(w,font,state,viewport.position.x+16,viewport.position.y+57,11,(cut||floating||(cylinders&&!fuel))?amber:accent); + }else if(selected_ids.size()>1)label(w,font,std::to_string(selected_ids.size())+" selected / Ctrl+C copy / Ctrl+D duplicate / right-click to save assembly",viewport.position.x+16,viewport.position.y+35,11,accent); + else if(auto p=part();p&&p->kind==MachinePartKind::Pipe){const auto diameters=machine_pipe_diameters(*p);label(w,font,number(machine_pipe_length(*p)*1000,1)+" mm long / point "+std::to_string(point_index+1)+" bore "+number(diameters[std::clamp(point_index,0,int(diameters.size())-1)]*1000,1)+" mm / "+number(machine_part_volume(*p)*1e6,1)+" mL",viewport.position.x+16,viewport.position.y+35,11,accent);} + // Orientation widget uses the same camera transform as picking and rendering. + const auto center=project(camera.target);if(center){const sf::Vector2f corner{viewport.position.x+48,viewport.position.y+viewport.size.y-(!test&&(tool==Tool::Connect||pending_port)?98:48)}; + for(int i=0;i<3;++i)if(auto end=project(add(camera.target,multiply(axes[i],camera.distance*.06f)))){auto delta=*end-*center;segment(w,corner,corner+delta,axis_color[i]);label(w,font,std::string(1,"XYZ"[i]),corner.x+delta.x,corner.y+delta.y-12,11,axis_color[i]);}} + if(test&&pedal_available()){float x=viewport.position.x+120,y=viewport.position.y+viewport.size.y-52;const sf::FloatRect track{{x,y+20},{std::max(100.f,viewport.size.x-160),12}};box(w,track,background);box(w,{{track.position.x,track.position.y},{track.size.x*pedal,track.size.y}},accent);label(w,font,"Pedal "+number(pedal*100,0)+"% W/S or click the bar",x,y-1,11,muted);} + w.setView(sf::View(sf::FloatRect({0,0},sf::Vector2f(size)))); +} + +void MachineBuilder::Impl::draw_meters(sf::RenderWindow& w,const sf::Font* font){ + if(!scope_visible)return; + const auto size=w.getSize(); + const auto panel_rect=scope_rect(size);box(w,panel_rect,background,border); + if(recorded_probes.empty()||trace.empty()){label(w,font,"OSCILLOSCOPE / add instruments and run Test",panel_rect.position.x+14,panel_rect.position.y+14,12,accent);return;} + const auto index=std::clamp(probe_index,0,int(recorded_probes.size())-1);const auto& probe=recorded_probes[index];const auto color=probe_colors[index];const float scale=machine_quantity_scale(probe.quantity); + const auto latest=trace.back().values[index];label(w,font,short_text(probe.name,45)+" / "+(std::isfinite(latest)?number(latest*scale,3):"--")+" "+machine_quantity_unit(probe.quantity),panel_rect.position.x+14,panel_rect.position.y+10,12,color); + const double end=std::max(double(trace_window),trace.back().time),begin=end-trace_window;double sum=0;std::size_t count=0;float low=std::numeric_limits::infinity(),high=-low; + for(const auto& sample:trace)if(sample.time>=begin&&std::isfinite(sample.values[index])){const float v=sample.values[index]*scale;low=std::min(low,v);high=std::max(high,v);sum+=v;++count;} + if(!count){label(w,font,"No defined reading in this time window.",panel_rect.position.x+14,panel_rect.position.y+66,11,muted);return;} + const float margin=std::max((high-low)*.08f,std::max(.001f,std::abs(high)*.005f));const bool signed_value=probe.quantity==MachineQuantity::MassFlow||probe.quantity==MachineQuantity::Torque||probe.quantity==MachineQuantity::Power;const float minimum=signed_value?low-margin:std::max(0.f,low-margin),maximum=high+margin; + const sf::FloatRect graph{{panel_rect.position.x+72,panel_rect.position.y+39},{panel_rect.size.x-90,panel_rect.size.y-82}}; + for(int i=0;i<5;++i){const float y=graph.position.y+graph.size.y*i/4;segment(w,{graph.position.x,y},{graph.position.x+graph.size.x,y},border);} + fitted_label(w,font,number(maximum,2),panel_rect.position.x+8,graph.position.y-5,58,10,muted);fitted_label(w,font,number(minimum,2),panel_rect.position.x+8,graph.position.y+graph.size.y-7,58,10,muted); + const int columns=std::max(1,int(graph.size.x));std::vector lower(columns,std::numeric_limits::infinity()),upper(columns,-std::numeric_limits::infinity());std::vector missing(columns); + for(const auto& sample:trace)if(sample.time>=begin){const int x=std::clamp(int((sample.time-begin)/trace_window*(columns-1)),0,columns-1);if(std::isfinite(sample.values[index])){const float value=sample.values[index]*scale;lower[x]=std::min(lower[x],value);upper[x]=std::max(upper[x],value);}else missing[x]=true;} + const auto point=[&](int x,float value){return sf::Vector2f{graph.position.x+x,graph.position.y+graph.size.y*(1-(value-minimum)/(maximum-minimum))};}; + std::vector vertices;vertices.reserve(columns*4);std::optional previous; + for(int x=0;x ghost;connection_hover.reset();connection_preview.reset(); + if(!test&&pending_port&&viewport.contains(sf::Vector2f(mouse))){connection_hover=pick_port(sf::Vector2f(mouse),size,false);if(connection_hover&&*connection_hover!=*pending_port){connection_preview=machine_connection_preview(doc,*pending_port,*connection_hover);if(connection_preview->allowed&&connection_preview->pipe)ghost=connection_preview->pipe;}} + if(placing&&viewport.contains(sf::Vector2f(mouse))){MachineDocument preview;ghost=preview.add(*placing,placement(sf::Vector2f(mouse),size));} + if(workspace_mode!=1){renderer.draw(w,doc,test?&live:nullptr,viewport,camera,selected,ghost?&*ghost:nullptr,std::vector(selected_ids.begin(),selected_ids.end()));draw_overlay(w,font);} + draw_meters(w,font); + box(w,{{8,8},{width-16,34}},panel);box(w,{{8,48},{width-16,40}},panel); + box(w,ui.left);box(w,ui.right); + fitted_label(w,font,doc.name+(dirty?" *":""),490,17,width-756,14); + const auto p=part(); + if(!graph_panels()){ + if(!left_closed){ + box(w,ui.search,background,search_focus?accent:border); + fitted_label(w,font,search_text.empty()&&!search_focus?"Search parts or objects...":search_text+(search_focus?"|":""),ui.search.position.x+9,ui.search.position.y+7,ui.search.size.x-18,12,search_focus?text_color:muted); + const auto items=library_items(); + if(library_tab)fitted_label(w,font,std::to_string(items.size())+" objects / click to select, F to focus",20,198,ui.left.size.x-24,11,muted); + if(items.empty())wrapped_label(w,font,"No matching items. Clear the search or choose another category.",20,236,ui.left.size.x-24,12,3,muted); + const int total=int(items.size()),rows=ui.library_rows(); + if(total>rows){const float track=ui.library.size.y;box(w,{{ui.left.position.x+ui.left.size.x-6,ui.library.position.y},{2,track}},border);box(w,{{ui.left.position.x+ui.left.size.x-6,ui.library.position.y+track*library_scroll/total},{2,track*rows/total}},accent);} + const float py=height-238;box(w,{{20,py},{ui.left.size.x-24,100}},background); + const auto kind=p?p->kind:placing?*placing:MachinePartKind(preview_kind); + renderer.draw_icon(w,kind,{{28,py+12},{76,60}}); + fitted_label(w,font,machine_part_name(kind),112,py+12,ui.left.size.x-126,12); + fitted_label(w,font,p?"Selected part":"Placement preview",112,py+36,ui.left.size.x-126,10,muted); + fitted_label(w,font,std::to_string(doc.parts.size())+" parts / "+std::to_string(doc.links.size())+" links",28,py+78,ui.left.size.x-40,11,muted); + }else label(w,font,"L",ui.left.position.x+9,111,12,muted); + if(!right_closed){ + const float content=ui.inspector_width-24; + const bool header_button=(inspector==5&&!doc.sensors.empty())||(inspector==0&&p&&(p->kind==MachinePartKind::DriveMotor||p->kind==MachinePartKind::Brake||p->kind==MachinePartKind::Throttle||p->kind==MachinePartKind::Carburetor||(p->kind==MachinePartKind::Shaft&&p->shaft.starter_torque>0))); + fitted_label(w,font,inspector==5?"SENSOR ATTACHMENTS":inspector==4?"INSTRUMENTS":inspector==3?"DIAGNOSTICS":inspector==2?"MACHINE SETUP":"INSPECTOR",right+12,109,content-(header_button?92:0),13,muted); + if(inspector==5){ + fitted_label(w,font,test?"Attachments locked / outputs sampled live":"Add a sensor to the selected part",right+12,175,content,11,muted); + if(doc.sensors.empty())wrapped_label(w,font,"Select a physical part, then add a compatible sensor above. Sensors do not consume gas ports or instrument channels.",right+12,316,content,12,5,muted); + else{ + sensor_index=std::clamp(sensor_index,0,int(doc.sensors.size())-1);const auto& sensor=doc.sensors[sensor_index];const bool reference=sensor_reference&&sensor.kind==MachineSensorKind::DifferentialPressure;const auto& tap=reference?sensor.reference:sensor.target;const auto host=doc.find(tap.part); + fitted_label(w,font,std::to_string(sensor_index+1)+" / "+std::to_string(doc.sensors.size()),right+ui.inspector_width*.43f,280,60,11,accent); + fitted_label(w,font,"S"+std::to_string(sensor.id)+" / "+sensor.name,right+12,312,content,12,port_color(PortDomain::Number)); + fitted_label(w,font,std::string(reference?"Reference: ":"Host: ")+(host?host->name:"unresolved / "+tap.label),right+12,334,content,11,host?muted:amber); + if(host&&host->kind==MachinePartKind::Pipe)fitted_label(w,font,"Tap "+number(tap.position*100,0)+"% from Start",right+12+70*content/268,394,128*content/268,10); + else if(sensor.kind==MachineSensorKind::Crank)fitted_label(w,font,number(sensor.angle_offset*180/pi,1)+" deg offset",right+12+70*content/268,394,128*content/268,10); + else if(sensor.kind!=MachineSensorKind::DifferentialPressure)fitted_label(w,font,"Ideal measurement / no added gas volume",right+12,362,content,11,muted); + const MachineSensorState* state=nullptr;if(test)for(const auto& s:live.sensors)if(s.id==sensor.id)state=&s; + const auto status=state?state->status:machine_sensor_binding(doc,sensor);fitted_label(w,font,machine_sensor_status_name(status),right+12,508,content,11,status==MachineSensorStatus::Valid?accent:amber); + for(unsigned i=0;ioutputs[i].validity==SignalValidity::Valid)reading=std::holds_alternative(state->outputs[i].value)?(std::get(state->outputs[i].value)?"true":"false"):number(std::get(state->outputs[i].value),output.quantity==SignalQuantity::Pressure?0:3); + fitted_label(w,font,output.name+": "+reading+(output.domain==PortDomain::Number?" "+std::string(signal_quantity_unit(output.quantity)):""),right+30,y,content-18,11,color); + } + if(state)fitted_label(w,font,"Sample "+number(state->outputs[0].timestamp,6)+" s",right+12,638,content,10,muted); + } + }else if(inspector==3){ + const auto warnings=std::count_if(checks.begin(),checks.end(),[](const auto& c){return c.warning;}); + fitted_label(w,font,std::to_string(warnings)+" warnings / "+std::to_string(checks.size()-warnings)+" notes",right+92,182,content-184,11,warnings?amber:accent); + if(checks.empty())wrapped_label(w,font,"No missing fuel assignments, unused ECUs or sealed pipe/cylinder ports found.",right+12,225,content,12,5,accent); + }else if(inspector==4){ + fitted_label(w,font,"Add a measurement to the selected part",right+12,175,content,11,muted); + if(!doc.probes.empty()){ + probe_index=std::clamp(probe_index,0,int(doc.probes.size())-1);const auto& probe=doc.probes[probe_index];const auto owner=doc.find(probe.part); + fitted_label(w,font,std::to_string(probe_index+1)+" / "+std::to_string(doc.probes.size()),right+ui.inspector_width*.43f,312,60,11,accent); + fitted_label(w,font,probe.name,right+12,346,content,12,probe_colors[probe_index]); + fitted_label(w,font,owner?owner->name:"Shared shaft instrument",right+12,368,content,11,muted); + if(owner&&owner->kind==MachinePartKind::Pipe)fitted_label(w,font,"Tap "+number(probe.position*100,0)+"% from Start",right+82,398,content-150,10); + if(probe.quantity==MachineQuantity::Lambda)fitted_label(w,font,"AFR "+number(probe.reference_afr,2),right+82,434,content-150,10); + if(probe.generated_output)fitted_label(w,font,"Engine output / excludes bench drive and load",right+12,398,content,10,muted); + }else wrapped_label(w,font,"Select a gas part and add a measurement. RPM, torque and power use the shared shaft.",right+12,346,content,12,4,muted); + }else if(inspector==2||!p){ + fitted_label(w,font,doc.name,right+12,175,content,14); + fitted_label(w,font,"Geometry in SI / display units on each field",right+12,195,content,11,muted); + }else if(inspector==0){ + if(selected_ids.size()>1)fitted_label(w,font,std::to_string(selected_ids.size())+" selected / editing active part",right+12,195,content,11,amber); + else { + std::string summary=p->kind==MachinePartKind::Ecu?"ECU / blue assignment pins":number(machine_part_volume(*p)*1e6,1)+" mL gas volume"; + if(machine_rotational_part(p->kind))summary=p->kind==MachinePartKind::Shaft?"Independent rotational node":"Attach to a named shaft / no gas storage"; + if(test&&p->kind!=MachinePartKind::Ecu)for(const auto& state:live.parts)if(state.id==p->id)summary=machine_rotational_part(p->kind)?number(state.shaft_speed*60/(2*pi),0)+" RPM / "+number(state.torque,2)+" Nm":number(state.pressure*.001f,2)+" kPa / "+number(state.temperature,0)+" K"; + if(p->injector){summary="Attached injector / "+std::string(p->injector->finite_supply?"finite regulated rail":"ideal regulated rail");if(test)for(const auto& state:live.parts)if(state.id==p->id)summary=state.injector_fault?"Injector command FAULT / closed":state.injector_empty?"Injector fuel supply EMPTY":number(state.fuel_flow*1000000,1)+" mg/s / "+(p->injector->finite_supply?number(state.injector_remaining*1000,2)+" g remaining":"ideal rail");} + if(p->kind==MachinePartKind::Ecu&&p->sensor_ecu.enabled){summary="Wired crank timing / explicit fuel targets";if(test)for(const auto& state:live.parts)if(state.id==p->id)summary=state.sensor_fault?"ECU sensor FAULT / outputs cut":state.rev_limited?"ECU rev limiter / outputs cut":number(state.spark_advance,1)+" deg BTDC / "+number(state.fuel_flow*1000000,1)+" mg/s per cylinder";} + fitted_label(w,font,summary,right+12,195,content,11,accent); + } + if(p->kind==MachinePartKind::Pipe)fitted_label(w,font,"Point "+std::to_string(point_index+1)+" / "+std::to_string(p->points.size()),right+88,216,std::max(40.f,content-190),11); + if(p->kind==MachinePartKind::Shaft){double total=p->shaft.inertia;for(const auto& leaf:doc.parts)if(leaf.kind==MachinePartKind::Flywheel&&machine_shaft_for(doc,leaf.id)==p->id)total+=leaf.shaft.inertia;fitted_label(w,font,"Total connected inertia: "+number(total,4)+" kg m2",right+12,250,content,11,muted);} + }else{ + fitted_label(w,font,"CONNECTIONS",right+12,175,content,13); + if(link_index>=0&&link_indexname,right+12,194,content,11,muted);} + else fitted_label(w,font,"Click or drag between visible ports.",right+12,195,content,11,muted); + } + auto f=fields();const int rows=ui.field_rows();field_scroll=std::clamp(field_scroll,0,std::max(0,int(f.size())-rows)); + if(!f.empty())fitted_label(w,font,"PROPERTIES / "+std::to_string(f.size())+" fields",right+12,280,content,10,muted); + for(int row=0;rowrows)fitted_label(w,font,"Scroll: "+std::to_string(field_scroll+1)+" - "+std::to_string(std::min(int(f.size()),field_scroll+rows))+" of "+std::to_string(f.size()),right+12,height-134,content,10,accent);} + }else label(w,font,"I",right+9,111,12,muted); + } + if(workspace_mode){auto hooks=logic_hooks();logic_editor.draw(w,font,hooks,ui,logic_view(size),graph_panels());} + std::string hover_help; + // Actions own both their draw rectangle and hit target. Fit every label. + for(const auto& action:actions(size)){ + const bool hover=action.rect.contains(sf::Vector2f(mouse));const auto fill=action.active?sf::Color(38,57,79):hover&&action.enabled?sf::Color(40,46,57):panel; + box(w,action.rect,fill,action.active?accent:border); + const float inset=action.icon?44.f:8.f; + if(hover&&fit_text(font,action.title,action.rect.size.x-inset-8,11)!=action.title)hover_help=action.title; + if(hover&&!action.enabled&&action.title=="Step 10 ms")hover_help="Pause the simulation to advance exactly 10 ms."; + fitted_label(w,font,action.title,action.rect.position.x+inset,action.rect.position.y+6,action.rect.size.x-inset-8,11,action.enabled?(action.active?accent:text_color):sf::Color(100,110,125)); + if(action.icon)renderer.draw_icon(w,*action.icon,{{action.rect.position.x+4,action.rect.position.y+1},{35,26}}); + if(!action.subtitle.empty())wrapped_label(w,font,action.subtitle,action.rect.position.x+8,action.rect.position.y+28,action.rect.size.x-16,10,2,muted); + } + // A single bounded status row never competes with panel content. + box(w,{{8,height-50},{width-16,42}},panel); + fitted_label(w,font,status,20,height-44,width-40,11,amber); + fitted_label(w,font,"Shift: select group Ctrl+C/V: copy/paste F: focus Ctrl+S: save Tab: Build/Test Escape: cancel",20,height-25,width-40,10,muted); + // Panel dividers are distinct targets, never hidden over a field. + box(w,{{ui.left.position.x+ui.left.size.x+2,100},{2,ui.left.size.y-8}},resize_panel==1?accent:border); + box(w,{{right-5,100},{2,ui.right.size.y-8}},resize_panel==2?accent:border); + if(!hover_help.empty()&&modal==Modal::None){ + const float tw=std::min(420.f,width-40),tx=std::clamp(float(mouse.x)+14,12.f,width-tw-12); + const float ty=std::clamp(float(mouse.y)+32,92.f,height-100); + box(w,{{tx,ty},{tw,48}},background,accent);wrapped_label(w,font,hover_help,tx+10,ty+8,tw-20,12,2,text_color); + } + if(modal!=Modal::None)draw_modal(w,font); +} + +MachineBuilder::MachineBuilder():impl_(std::make_unique()){} +MachineBuilder::~MachineBuilder()=default; +const MachineDocument& MachineBuilder::document()const{return impl_->doc;} +std::vector MachineBuilder::selected_parts()const{return {impl_->selected_ids.begin(),impl_->selected_ids.end()};} +void MachineBuilder::set_document(MachineDocument doc,std::filesystem::path file){validate_machine(doc);impl_->set_document(std::move(doc),std::move(file));} +bool MachineBuilder::load_file(const std::filesystem::path& file){try{set_document(load_machine(file),file);return true;}catch(const std::exception& error){impl_->status=error.what();return false;}} +void MachineBuilder::start_test(){impl_->start();} +void MachineBuilder::stop_test(){impl_->stop();} +bool MachineBuilder::testing()const{return bool(impl_->test);} +MachinePlaybackStats MachineBuilder::playback_stats()const{return {impl_->live.time,impl_->test?impl_->test->underruns():0,impl_->test?impl_->test->buffered():0,impl_->test_speed,impl_->test&&impl_->test->paused};} +void MachineBuilder::set_test_speed(double speed){impl_->speed(speed);} +void MachineBuilder::pause_test(){if(impl_->test){impl_->test->pause();impl_->collect();}} +void MachineBuilder::step_test(){if(impl_->test){impl_->test->controls(impl_->pedal,impl_->starter,impl_->camera.listener());impl_->test->step();impl_->collect();}} +std::vector MachineBuilder::measurements()const{return {impl_->trace.begin(),impl_->trace.end()};} +bool MachineBuilder::export_measurements(const std::filesystem::path& path){return impl_->export_csv(path);} +bool MachineBuilder::finished()const{return impl_->done;} +bool MachineBuilder::wants_close_window()const{return impl_->close_window;} +bool MachineBuilder::handle_event(const sf::Event& event,sf::Vector2u size){return impl_->event(event,size);} +sf::FloatRect MachineBuilder::viewport(sf::Vector2u size){return WorkshopLayout::calculate(size).viewport;} +sf::FloatRect MachineBuilder::logic_viewport(sf::Vector2u size)const{return impl_->logic_view(size);} +WorkshopView MachineBuilder::workspace_view()const{return WorkshopView(impl_->workspace_mode);} +void MachineBuilder::set_workspace_view(WorkshopView view){impl_->change_workspace(int(view));} +std::uint32_t MachineBuilder::selected_logic_node()const{return impl_->logic_editor.selected_node();} +std::vector MachineBuilder::logic_values()const{return impl_->live.logic;} +sf::FloatRect MachineBuilder::logic_node_rectangle(std::uint32_t id,sf::Vector2u size)const{const auto node=impl_->doc.logic.find(id);if(!node)throw std::out_of_range("Unknown logic node");return impl_->logic_editor.node_rect(*node,impl_->logic_view(size));} +sf::Vector2f MachineBuilder::logic_pin_position(std::uint32_t id,const std::string& key,sf::Vector2u size)const{const auto node=impl_->doc.logic.find(id);if(!node)throw std::out_of_range("Unknown logic node");return impl_->logic_editor.pin_position(*node,key,impl_->logic_view(size));} +sf::FloatRect MachineBuilder::workspace_viewport(sf::Vector2u size)const{return impl_->view(size);} +void MachineBuilder::restore_workspace_preferences(){impl_->restore_preferences();} +const OrbitCamera& MachineBuilder::camera()const{return impl_->camera;} +void MachineBuilder::update(){ + if(impl_->test){const auto error=impl_->test->error();if(!error.empty()){impl_->stop();impl_->status="Test stopped: "+error;return;}impl_->test->controls(impl_->pedal,impl_->starter,impl_->camera.listener());impl_->collect();} +} +void MachineBuilder::draw(sf::RenderWindow& w,const sf::Font* font){impl_->draw(w,font);} +void run_machine_builder(sf::RenderWindow& window,const sf::Font* font,const std::filesystem::path& file,bool play){ + window.setMinimumSize(sf::Vector2u{1280,720}); + maximize_window(window); + window.setTitle("FluidSim - Machine workshop");MachineBuilder builder;builder.restore_workspace_preferences(); + const bool loaded=file.empty()||builder.load_file(file); + if(play&&loaded)builder.start_test(); + while(window.isOpen()&&!builder.finished()){ + while(auto event=window.pollEvent())builder.handle_event(*event,window.getSize()); + if(builder.finished())break; + builder.update();builder.draw(window,font);window.display(); + } + if(builder.wants_close_window())window.close(); +} +} diff --git a/src/machine_import.cpp b/src/machine_import.cpp new file mode 100644 index 0000000..d71eeca --- /dev/null +++ b/src/machine_import.cpp @@ -0,0 +1,71 @@ +#include "FluidSim/Machine.hpp" +#include "FluidSim/Scenario.hpp" +#include "FluidSim/ScenarioCatalog.hpp" +#include +#include + +namespace fluidsim::port { +using namespace machine_math; +MachineDocument machine_from_engine(const ScenarioExample& definition){ + if(definition.kind==ScenarioKind::TwoStroke)throw std::invalid_argument("Two-stroke preset import needs a crankcase/scavenge assembly; choose a four-stroke layout"); + FourStrokeScenario source(44100,definition);const auto& e=source.engine_config;const auto& g=e.geometry;const int n=e.cylinders; + if(e.has_silenced_exhaust())throw std::invalid_argument("Choose an unsilenced layout; profiled silencer assemblies are not yet imported"); + MachineDocument d;d.name=definition.title+" / editable";if(d.name.size()>120)d.name.resize(120);d.driven=false;d.drive_rpm=e.initial_rpm;d.shaft_inertia=e.crankshaft.inertia; + d.friction_constant=e.crankshaft.friction_constant;d.friction_viscous=e.crankshaft.friction_viscous;d.friction_quadratic=e.crankshaft.friction_quadratic;d.starter_torque=e.starter_torque;d.cells_per_meter=e.cells_per_meter>0?e.cells_per_meter:40; + std::vector pipes,cylinders; + for(std::size_t i=0;i1e-6f*native.cell_areas.front())throw std::invalid_argument("This layout contains a profiled duct; choose single-450, twin, flat6, or v8-h"); + auto& p=d.add(MachinePartKind::Pipe);p.name="Pipe "+std::to_string(i)+" / "+definition.id;p.position=GeometryPath{g.pipes[i].points}.position(g.pipes[i].length()*.5f); + p.points.clear();for(auto v:g.pipes[i].points)p.points.push_back(subtract(v,p.position));p.point_diameters.assign(p.points.size(),diameter);const auto state=native.state(0);p.pressure=state.pressure;p.temperature=state.pressure/(state.rho*gas_constant);pipes.push_back(p.id); + } + for(int i=0;i0){open=j?profile.points[j-1].angle_degrees:0;break;}for(std::size_t j=profile.points.size();j-->0;)if(profile.points[j].lift_metres>0){close=j+1center);p.name=id;p.size=geometry->size;p.temperature=id=="intake_plenum"?300.f:700.f;return p.id;}; + const auto attach=[&](int index,bool left,unsigned owner,unsigned port){d.connect({pipes.at(index),left?0u:1u},{owner,port});}; + const auto attach_box=[&](int index,bool left,unsigned owner){auto p=d.find(owner);const auto endpoint=machine_port_position(*d.find(pipes.at(index)),left?0:1);const auto local=machine_local_point(*p,endpoint);const auto s=machine_part_size(*p);unsigned port=unsigned(p->port_coordinates.size());p->port_coordinates.push_back({local.x/s.x,local.y/s.y,local.z/s.z});attach(index,left,owner,port);}; + const auto plenum=box("intake_plenum"); + for(int i=0;ipoint_diameters.back();const auto start=subtract(end,multiply(direction,body_length+stub_length));inlet->points.back()=machine_local_point(*inlet,start); + auto& metering=d.add(e.carbureted?MachinePartKind::Carburetor:MachinePartKind::Throttle,multiply(add(start,subtract(end,multiply(direction,stub_length))),.5f));const auto metering_id=metering.id; + metering.name=e.carbureted?"Carburetor":"Electronic throttle";metering.rotation={0,-std::asin(std::clamp(direction.z,-1.f,1.f))*180/pi,std::atan2(direction.y,direction.x)*180/pi};metering.size={body_length,inlet_diameter,inlet_diameter}; + metering.diameter=e.throttle_diameter;metering.carburetor=e.fuel;metering.discharge=.7f; + d.connect({inlet_id,1},{metering_id,0});auto& stub=d.add(MachinePartKind::Pipe);const auto stub_id=stub.id;stub.name="Inlet metering stub";stub.position={};stub.points={machine_port_position(*d.find(metering_id),1),end};stub.point_diameters={inlet_diameter,inlet_diameter};d.connect({stub_id,0},{metering_id,1}); + const auto local=machine_local_point(*d.find(plenum),end);const auto size=d.find(plenum)->size;const unsigned port=unsigned(d.find(plenum)->port_coordinates.size());d.find(plenum)->port_coordinates.push_back({local.x/size.x,local.y/size.y,local.z/size.z});d.connect({stub_id,1},{plenum,port}); + auto& ecu=d.add(MachinePartKind::Ecu,add(d.find(plenum)->position,{0,.25f,0}));const auto ecu_id=ecu.id;ecu.ecu=e.ecu;ecu.injection_enabled=!e.carbureted;ecu.name=e.carbureted?"Ignition / idle ECU":"Injection / ignition ECU"; + for(auto id:cylinders)d.connect({ecu_id,0},{id,2}); + d.connect({ecu_id,0},{metering_id,2}); + const auto mouth=[&](int index,bool left){const auto position=machine_port_position(*d.find(pipes.at(index)),left?0:1);auto& p=d.add(MachinePartKind::Reservoir,position);p.name=left?"Intake atmosphere":"Exhaust atmosphere";const auto id=p.id;attach(index,left,id,0);};mouth(0,true); + int junction_index=0; + const auto junction=[&](const std::vector>& ends){ + if(definition.junctions.model=="volume"){const auto id=box("junction_"+std::to_string(junction_index));for(auto [index,left]:ends)attach_box(index,left,id);} + else{const auto pos=machine_port_position(*d.find(pipes.at(ends.front().first)),ends.front().second?0:1);auto& p=d.add(MachinePartKind::Junction,pos);const auto id=p.id;p.name="Exhaust junction "+std::to_string(junction_index+1);p.junction_method=definition.junctions.model=="legacy"?2:1;p.turning_loss=definition.junctions.turning_loss;for(auto [index,left]:ends)attach(index,left,id,0);} + ++junction_index; + }; + if(e.american_headers()){ + junction({{9,false},{10,false},{17,true}});junction({{11,false},{12,false},{18,true}});junction({{13,false},{15,false},{19,true}});junction({{14,false},{16,false},{20,true}});junction({{17,false},{18,false},{21,true}});junction({{19,false},{20,false},{22,true}}); + if(definition.junctions.h_pipe){junction({{21,false},{23,true},{25,true}});junction({{22,false},{24,true},{25,false}});}else{d.connect({pipes[21],1},{pipes[23],0});d.connect({pipes[22],1},{pipes[24],0});} + mouth(23,false);mouth(24,false); + }else if(e.staged_headers()){ + const int exit=1+2*n,groups=e.header_groups(),secondary=exit+(e.dual_outlets()?2:1); + for(int group=0;group> ends;for(int i=0;i> ends{{exit,true}};for(int group=0;group> ends{{exit,true}};for(int i=0;i +#include +#include +#include +#include +#include +#include + +namespace fluidsim::port { +namespace { +constexpr const char* keys[]{"boolean_constant","switch","button","number_constant","slider","number_input","and","or","not","xor","nand","nor","greater","less","equal","window","hysteresis","add","subtract","multiply","divide","minimum","maximum","absolute","clamp","gain_offset","map_range","select","boolean_to_number","sensor","command","lamp","readout","boolean_delay","number_delay","ecu"}; +constexpr const char* names[]{"Boolean constant","Toggle switch","Momentary button","Number constant","Number slider","Numeric input","AND","OR","NOT","XOR","NAND","NOR","Greater than","Less than","Equal within tolerance","Range / window","Hysteresis threshold","Add","Subtract","Multiply by scalar","Divide by scalar","Minimum","Maximum","Absolute value","Clamp","Gain / offset","Map range","Number selector","Boolean to number","Sensor reference","Actuator command","Boolean lamp","Number readout","Boolean delay","Number delay","ECU connector"}; +static_assert(std::size(keys)==logic_node_kind_count&&std::size(names)==logic_node_kind_count); +void require(bool value,const std::string& message){if(!value)throw std::invalid_argument(message);} +MachinePortDescriptor boolean(std::string key,std::string name,PortDirection direction,bool required=true){MachinePortDescriptor p;p.key=std::move(key);p.name=std::move(name);p.domain=PortDomain::Boolean;p.direction=direction;p.cardinality=direction==PortDirection::Output?PortCardinality::Multiple:PortCardinality::Single;p.required=required&&direction==PortDirection::Input;return p;} +MachinePortDescriptor number(const LogicNode& n,std::string key,std::string name,PortDirection direction,bool output_type=false){auto p=boolean(std::move(key),std::move(name),direction);p.domain=PortDomain::Number;p.quantity=output_type?n.output_quantity:n.quantity;p.pressure_reference=output_type?n.output_pressure_reference:n.pressure_reference;return p;} +} +const char* logic_node_key(LogicNodeKind kind){require(int(kind)>=0&&int(kind)=0&&int(kind) logic_node_ports(const LogicNode& n,const MachineDocument* context){ + logic_node_key(n.kind);std::vector ports; + const auto input=[&](const char* key,const char* name){ports.push_back(number(n,key,name,PortDirection::Input));}; + const auto output=[&](bool mapped=false){ports.push_back(number(n,"out","Output",PortDirection::Output,mapped));}; + const auto bit_input=[&](const char* key,const char* name){ports.push_back(boolean(key,name,PortDirection::Input));}; + const auto bit_output=[&]{ports.push_back(boolean("out","Output",PortDirection::Output));}; + switch(n.kind){ + case LogicNodeKind::BooleanConstant:case LogicNodeKind::Switch:case LogicNodeKind::Button:bit_output();break; + case LogicNodeKind::NumberConstant:case LogicNodeKind::Slider:case LogicNodeKind::NumberInput:output();if(n.kind!=LogicNodeKind::NumberConstant){ports.back().minimum=n.minimum;ports.back().maximum=n.maximum;}break; + case LogicNodeKind::And:case LogicNodeKind::Or:case LogicNodeKind::Xor:case LogicNodeKind::Nand:case LogicNodeKind::Nor: + require(n.input_count>=2&&n.input_count<=16,"Boolean gates need 2..16 inputs");for(unsigned i=0;iparts.begin(),context->parts.end(),[&](const auto& part){return part.kind==MachinePartKind::Cylinder&&machine_shaft_for(*context,part.id)==shaft;}))p.minimum=0.;} + ports.push_back(std::move(p));break; + } + case LogicNodeKind::Lamp:bit_input("in","State");break; + case LogicNodeKind::Readout:input("in","Reading");break; + } + for(const auto& p:ports)validate_port_descriptor(p); + return ports; +} +LogicNode* LogicGraph::find(std::uint32_t id){for(auto& n:nodes)if(n.id==id)return &n;return nullptr;} +const LogicNode* LogicGraph::find(std::uint32_t id)const{for(const auto& n:nodes)if(n.id==id)return &n;return nullptr;} +LogicNode& LogicGraph::add(LogicNodeKind kind,LogicPosition position){LogicNode n;n.id=next_id++;n.kind=kind;n.position=position;n.name=std::string(logic_node_name(kind))+" "+std::to_string(n.id);nodes.push_back(std::move(n));return nodes.back();} +void LogicGraph::remove(std::uint32_t id){std::erase_if(links,[&](const auto& link){return link.from.node==id||link.to.node==id;});std::erase_if(nodes,[&](const auto& n){return n.id==id;});} +void LogicGraph::disconnect(const LogicEndpoint& endpoint){std::erase_if(links,[&](const auto& link){return link.from==endpoint||link.to==endpoint;});} +MachinePortDescriptor logic_port(const LogicGraph& graph,const LogicEndpoint& endpoint){const auto node=graph.find(endpoint.node);require(node,"Logic connection refers to a missing node");for(const auto& p:logic_node_ports(*node))if(p.key==endpoint.port)return p;throw std::invalid_argument(node->name+": unknown logic port '"+endpoint.port+"'");} +void LogicGraph::connect(LogicEndpoint a,LogicEndpoint b){const auto pa=logic_port(*this,a),pb=logic_port(*this,b);const auto compatible=port_compatibility(pa,pb);require(compatible.allowed,compatible.message);if(pa.direction==PortDirection::Input)std::swap(a,b);auto proposed=*this;proposed.links.push_back({std::move(a),std::move(b)});validate_logic_graph(proposed);*this=std::move(proposed);} +PortCompatibility logic_connection_preview(const LogicGraph& graph,LogicEndpoint a,LogicEndpoint b){try{auto proposed=graph;proposed.connect(std::move(a),std::move(b));return {true,"Compatible typed signal; output may fan out"};}catch(const std::exception& e){return {false,e.what()};}} +std::vector logic_evaluation_order(const LogicGraph& graph,std::vector* cycle_path){ + if(cycle_path)cycle_path->clear(); + std::unordered_map> inputs;for(const auto& l:graph.links){const auto from=graph.find(l.from.node),to=graph.find(l.to.node);require(from&&to,"Logic wire refers to a missing node");if(!logic_memory_node(from->kind)&&!logic_memory_node(to->kind))inputs[to->id].push_back(from->id);} + std::unordered_map marks;std::vector order,path; + std::function visit=[&](std::uint32_t id){if(marks[id]==2)return;if(marks[id]==1){std::ostringstream message;message<<"Combinational cycle: ";const auto start=std::find(path.begin(),path.end(),id);if(cycle_path){cycle_path->assign(start,path.end());cycle_path->push_back(id);}for(auto p=start;p!=path.end();++p)message<name<<" -> ";message<name;throw std::invalid_argument(message.str());}marks[id]=1;path.push_back(id);for(const auto source:inputs[id])visit(source);path.pop_back();marks[id]=2;order.push_back(id);}; + for(const auto& node:graph.nodes)if(!logic_memory_node(node.kind))visit(node.id); + return order; +} +void validate_logic_graph(const LogicGraph& graph){ + require(graph.nodes.size()<=2048&&graph.links.size()<=8192,"Logic exceeds 2048 nodes / 8192 wires");std::set ids; + for(const auto& n:graph.nodes){require(n.id&&n.id<1000000000&&ids.insert(n.id).second,"Invalid or duplicate logic node ID");logic_node_key(n.kind);logic_command_key(n.command);machine_sensor_key(n.sensor_kind); + require(!n.name.empty()&&n.name.size()<=120&&n.binding_label.size()<=120,"Logic names must have 1..120 characters");require(std::isfinite(n.position.x)&&std::isfinite(n.position.y)&&std::abs(n.position.x)<=1e6&&std::abs(n.position.y)<=1e6,"Invalid logic canvas position"); + require(n.part<1000000000&&n.sensor<1000000000,"Invalid logic physical binding"); + sensor_ecu_strategy_key(n.ecu_strategy); + for(const auto value:{n.value,n.minimum,n.maximum,n.low,n.high,n.tolerance,n.gain,n.offset,n.false_value,n.true_value,n.input_low,n.input_high,n.output_low,n.output_high})require(std::isfinite(value),n.name+": numeric parameters must be finite"); + require(n.minimum<=n.maximum&&n.low<=n.high&&n.tolerance>=0,n.name+": reversed bounds or negative tolerance"); + if(n.kind==LogicNodeKind::MapRange)require(n.input_low!=n.input_high&&std::isfinite(n.input_high-n.input_low),n.name+": mapping input range must be nonzero and finite"); + if(n.kind==LogicNodeKind::Slider||n.kind==LogicNodeKind::NumberInput)require(n.value>=n.minimum&&n.value<=n.maximum,n.name+": initial value is outside the control range"); + std::set ports;for(const auto& p:logic_node_ports(n))require(ports.insert(p.key).second,"Duplicate logic port key"); + } + require(graph.next_id>0&&graph.next_id<1000000000&&(ids.empty()||graph.next_id>*ids.rbegin()),"Invalid next logic node ID"); + std::set> used; + for(const auto& l:graph.links){const auto a=logic_port(graph,l.from),b=logic_port(graph,l.to);const auto compatible=port_compatibility(a,b);require(compatible.allowed,compatible.message);require(a.direction==PortDirection::Output&&b.direction==PortDirection::Input,"Logic wires must be output -> input");require(used.emplace(l.to.node,l.to.port).second,"A logic input has multiple drivers");} + logic_evaluation_order(graph); +} +std::vector logic_required_inputs(const LogicGraph& graph){std::vector result;for(const auto& node:graph.nodes)for(const auto& port:logic_node_ports(node))if(port.required&&!std::any_of(graph.links.begin(),graph.links.end(),[&](const auto& link){return link.to.node==node.id&&link.to.port==port.key;}))result.push_back(node.name+" / "+port.name+": required signal is unconnected");return result;} +bool logic_topology_equal(const LogicGraph& a,const LogicGraph& b){ + if(a.nodes.size()!=b.nodes.size()||a.links!=b.links)return false; + for(std::size_t i=0;ikind!=MachinePartKind::Ecu||!part->sensor_ecu.enabled)return "Choose a sensor-driven ECU target";if(part->sensor_ecu.strategy!=node.ecu_strategy)return "ECU strategy differs from the connector's saved schema";return {};} + if(node.kind==LogicNodeKind::Sensor){const auto sensor=doc.find_sensor(node.sensor);if(!sensor)return "Unresolved sensor reference";if(sensor->kind!=node.sensor_kind)return "Sensor kind differs from the node's saved port schema";return {};} + if(node.kind!=LogicNodeKind::Command)return {}; + const auto part=doc.find(node.part);if(!part)return "Unresolved command target"; + switch(node.command){ + case LogicCommand::MotorEnable:if(part->kind!=MachinePartKind::DriveMotor)return "Motor enable needs a drive motor";break; + case LogicCommand::MotorSpeed:if(part->kind!=MachinePartKind::DriveMotor||!part->drive.speed_control)return "Speed target needs a motor in ideal speed mode";break; + case LogicCommand::MotorTorque:if(part->kind!=MachinePartKind::DriveMotor||part->drive.speed_control)return "Torque target needs a motor in torque mode";break; + case LogicCommand::BrakeEnable:if(part->kind!=MachinePartKind::Brake)return "Brake enable needs a brake / load";break; + case LogicCommand::ThrottleOpening: + if(part->kind!=MachinePartKind::Throttle&&part->kind!=MachinePartKind::Carburetor)return "Opening command needs a throttle / carburetor"; + if(std::any_of(doc.links.begin(),doc.links.end(),[&](const auto& link){return link.kind==MachineConnectionKind::Control&&link.b.part==part->id;}))return "Opening already belongs to an assigned ECU"; + break; + case LogicCommand::Starter:if(part->kind!=MachinePartKind::Shaft||part->shaft.starter_torque<=0)return "Starter command needs a shaft with declared starter torque";break; + }return {}; +} +void validate_machine_logic(const MachineDocument& doc){ + validate_logic_graph(doc.logic);std::set> owners; + std::set ecu_owners;for(const auto& node:doc.logic.nodes)if(node.kind==LogicNodeKind::Ecu&&node.part)require(ecu_owners.insert(node.part).second,"An ECU has multiple signal connectors"); + for(const auto& n:doc.logic.nodes)if(n.kind==LogicNodeKind::Command&&n.part&&doc.find(n.part)){const int field=n.command==LogicCommand::MotorTorque?int(LogicCommand::MotorSpeed):int(n.command);require(owners.emplace(n.part,field).second,"Multiple signal command owners target the same actuator field");} +} +const LogicNode* logic_command_owner(const MachineDocument& doc,std::uint32_t part,LogicCommand command){for(const auto& node:doc.logic.nodes)if(node.kind==LogicNodeKind::Command&&node.part==part&&node.command==command)return &node;return nullptr;} +} diff --git a/src/machine_mechanics.cpp b/src/machine_mechanics.cpp new file mode 100644 index 0000000..763e511 --- /dev/null +++ b/src/machine_mechanics.cpp @@ -0,0 +1,32 @@ +#include "FluidSim/Machine.hpp" +#include +#include + +namespace fluidsim::port { +bool machine_rotational_part(MachinePartKind kind){return kind==MachinePartKind::Shaft||kind==MachinePartKind::Flywheel||kind==MachinePartKind::DriveMotor||kind==MachinePartKind::Brake;} +std::uint32_t machine_shaft_for(const MachineDocument& document,std::uint32_t id){ + const auto part=document.find(id);if(!part)return 0;if(part->kind==MachinePartKind::Shaft)return id; + for(const auto& link:document.links)if(link.kind==MachineConnectionKind::Mechanical){const auto other=link.a.part==id?link.b.part:link.b.part==id?link.a.part:0;if(other){const auto shaft=document.find(other);if(shaft&&shaft->kind==MachinePartKind::Shaft)return other;}} + return 0; +} +std::uint32_t machine_primary_shaft(const MachineDocument& document){ + if(document.primary_shaft){const auto part=document.find(document.primary_shaft);return part&&part->kind==MachinePartKind::Shaft?part->id:0;} + std::uint32_t found=0;for(const auto& part:document.parts)if(part.kind==MachinePartKind::Shaft){if(found)return 0;found=part.id;}return found; +} +bool machine_shaft_driven(const MachineDocument& document,std::uint32_t shaft){for(const auto& part:document.parts)if(part.kind==MachinePartKind::DriveMotor&&part.drive.enabled&&part.drive.speed_control&&machine_shaft_for(document,part.id)==shaft)return true;return false;} +void upgrade_machine_mechanics(MachineDocument& document){ + if(std::any_of(document.parts.begin(),document.parts.end(),[](const auto& p){return p.kind==MachinePartKind::Shaft;}))return; + std::vector attached;Vec3 center{}; + for(const auto& part:document.parts)if(part.kind==MachinePartKind::Cylinder||part.kind==MachinePartKind::PistonPump){attached.push_back(part.id);center=machine_math::add(center,part.position);} + // Old gas-only documents can still have explicit RPM instruments or a drive. + const bool rotational_probe=std::any_of(document.probes.begin(),document.probes.end(),[](const auto& p){return p.quantity>=MachineQuantity::Rpm;}); + if(attached.empty()&&!document.driven&&document.drive_rpm==0&&!rotational_probe)return; + if(!attached.empty())center=machine_math::multiply(center,1.f/attached.size()); + center.y-=.15f; + auto& shaft=document.add(MachinePartKind::Shaft,center);const auto id=shaft.id;shaft.name="Main crankshaft";shaft.shaft={document.shaft_inertia,document.drive_rpm*2*pi/60,0,document.friction_constant,document.friction_viscous,document.friction_quadratic,document.starter_torque};document.primary_shaft=id; + for(auto part:attached)document.connect({id,0},{part,machine_port_index(*document.find(part),"shaft")}); + if(document.driven){auto& drive=document.add(MachinePartKind::DriveMotor,machine_math::add(center,{-.28f,0,0}));const auto motor=drive.id;drive.name="Bench speed drive";drive.drive.target_speed=document.drive_rpm*2*pi/60;document.connect({id,0},{motor,0});} + if(document.load_torque>0){auto& brake=document.add(MachinePartKind::Brake,machine_math::add(center,{.28f,0,0}));const auto load=brake.id;brake.name="Bench load";brake.brake.constant=document.load_torque;document.connect({id,0},{load,0});} + for(auto& probe:document.probes)if(probe.quantity>=MachineQuantity::Rpm&&probe.part==0)probe.part=id; +} +} diff --git a/src/machine_ports.cpp b/src/machine_ports.cpp new file mode 100644 index 0000000..ed192d7 --- /dev/null +++ b/src/machine_ports.cpp @@ -0,0 +1,71 @@ +#include "FluidSim/Machine.hpp" +#include +#include + +namespace fluidsim::port { +namespace { +void require(bool value,const std::string& message){if(!value)throw std::invalid_argument(message);} +bool key_valid(const std::string& key){if(key.empty()||key.size()>64)return false;for(const auto c:key)if(!((c>='a'&&c<='z')||(c>='A'&&c<='Z')||(c>='0'&&c<='9')||c=='_'||c=='-'||c=='.'))return false;return true;} +} +SignalValue SignalValue::boolean(bool value,double time){return {value,std::isfinite(time)?SignalValidity::Valid:SignalValidity::Fault,time,{}};} +SignalValue SignalValue::number(double value,double time){return {value,std::isfinite(value)&&std::isfinite(time)?SignalValidity::Valid:SignalValidity::Fault,time,{}};} +const char* port_domain_key(PortDomain domain){switch(domain){case PortDomain::Gas:return "gas";case PortDomain::Rotational:return "rotational";case PortDomain::Translational:return "translational";case PortDomain::Boolean:return "boolean";case PortDomain::Number:return "number";case PortDomain::EcuAssignment:return "ecu_assignment";}throw std::invalid_argument("Unknown port domain");} +const char* signal_quantity_key(SignalQuantity q){switch(q){case SignalQuantity::None:return "none";case SignalQuantity::Scalar:return "scalar";case SignalQuantity::Fraction:return "fraction";case SignalQuantity::Pressure:return "pressure";case SignalQuantity::Temperature:return "temperature";case SignalQuantity::MassFlow:return "mass_flow";case SignalQuantity::FreshAirMassFlow:return "fresh_air_mass_flow";case SignalQuantity::AngularSpeed:return "angular_speed";case SignalQuantity::Angle:return "angle";case SignalQuantity::CyclePhase:return "cycle_phase";case SignalQuantity::Torque:return "torque";case SignalQuantity::Power:return "power";case SignalQuantity::Position:return "position";case SignalQuantity::Velocity:return "velocity";case SignalQuantity::Force:return "force";case SignalQuantity::Time:return "time";case SignalQuantity::Inertia:return "inertia";}throw std::invalid_argument("Unknown signal quantity");} +const char* signal_quantity_unit(SignalQuantity q){switch(q){case SignalQuantity::None:case SignalQuantity::Scalar:case SignalQuantity::Fraction:return "1";case SignalQuantity::Pressure:return "Pa";case SignalQuantity::Temperature:return "K";case SignalQuantity::MassFlow:case SignalQuantity::FreshAirMassFlow:return "kg/s";case SignalQuantity::AngularSpeed:return "rad/s";case SignalQuantity::Angle:case SignalQuantity::CyclePhase:return "rad";case SignalQuantity::Torque:return "N m";case SignalQuantity::Power:return "W";case SignalQuantity::Position:return "m";case SignalQuantity::Velocity:return "m/s";case SignalQuantity::Force:return "N";case SignalQuantity::Time:return "s";case SignalQuantity::Inertia:return "kg m2";}throw std::invalid_argument("Unknown signal quantity");} +bool signal_domain(PortDomain domain){return domain==PortDomain::Boolean||domain==PortDomain::Number;} +void validate_port_descriptor(const MachinePortDescriptor& port){ + require(key_valid(port.key),"Invalid stable port key");require(!port.name.empty()&&port.name.size()<=120,"Invalid port display name"); + port_domain_key(port.domain);signal_quantity_key(port.quantity); + require(port.direction==PortDirection::Input||port.direction==PortDirection::Output||port.direction==PortDirection::Bidirectional,"Invalid port direction"); + require(port.cardinality==PortCardinality::Single||port.cardinality==PortCardinality::Multiple,"Invalid port cardinality"); + const bool directed=signal_domain(port.domain)||port.domain==PortDomain::EcuAssignment; + require(directed?port.direction!=PortDirection::Bidirectional:port.direction==PortDirection::Bidirectional,"Port direction is incompatible with its domain"); + if(directed&&port.direction==PortDirection::Input)require(port.cardinality==PortCardinality::Single,"A signal input must have a single driver"); + require((port.domain==PortDomain::Number)==(port.quantity!=SignalQuantity::None),"Numeric ports need an explicit quantity; other ports cannot declare one"); + if(port.quantity==SignalQuantity::Pressure)require(port.pressure_reference==PressureReference::Absolute||port.pressure_reference==PressureReference::Gauge||port.pressure_reference==PressureReference::Differential,"Pressure signal needs an explicit reference"); + else require(port.pressure_reference==PressureReference::None,"Only pressure signals declare a pressure reference"); + require(!port.required||port.direction==PortDirection::Input,"Only inputs declare required wiring"); + require(!port.required||std::holds_alternative(port.default_value),"A required input cannot also declare a default"); + require(!port.minimum||std::isfinite(*port.minimum),"Signal minimum must be finite");require(!port.maximum||std::isfinite(*port.maximum),"Signal maximum must be finite"); + require(!port.minimum||!port.maximum||*port.minimum<=*port.maximum,"Signal range is reversed"); + require(port.domain==PortDomain::Number||(!port.minimum&&!port.maximum),"Only numbers declare numeric ranges"); + if(!std::holds_alternative(port.default_value)){ + require(port.direction==PortDirection::Input&&signal_domain(port.domain),"Only signal inputs declare default values"); + const auto value=std::holds_alternative(port.default_value)?SignalValue::boolean(std::get(port.default_value)):SignalValue::number(std::get(port.default_value)); + require(signal_value_valid_for(value,port),"Input default has the wrong type or is outside its range"); + } +} +PortCompatibility port_compatibility(const MachinePortDescriptor& a,const MachinePortDescriptor& b){ + try{validate_port_descriptor(a);validate_port_descriptor(b);}catch(const std::exception& error){return {false,error.what()};} + if(a.domain!=b.domain)return {false,std::string("Cannot connect ")+port_domain_key(a.domain)+" to "+port_domain_key(b.domain)}; + if(a.direction!=PortDirection::Bidirectional&&!(a.direction==PortDirection::Output&&b.direction==PortDirection::Input)&&!(b.direction==PortDirection::Output&&a.direction==PortDirection::Input))return {false,"Connect an output to an input"}; + if(a.domain==PortDomain::Number&&(a.quantity!=b.quantity||a.pressure_reference!=b.pressure_reference))return {false,"Number quantities or pressure references do not match; use an explicit conversion"}; + return {true,"Compatible ports"}; +} +bool signal_value_valid_for(const SignalValue& value,const MachinePortDescriptor& port){ + if(value.validity!=SignalValidity::Valid||!std::isfinite(value.timestamp))return false; + if(port.domain==PortDomain::Boolean)return std::holds_alternative(value.value); + if(port.domain!=PortDomain::Number||!std::holds_alternative(value.value))return false; + const auto number=std::get(value.value);return std::isfinite(number)&&(!port.minimum||number>=*port.minimum)&&(!port.maximum||number<=*port.maximum); +} +MachinePortDescriptor machine_port_descriptor(const MachinePart& part,unsigned index){ + require(index +#include +#include +#include +#include +namespace fluidsim::port::desktop { +using namespace machine_math; +namespace { +void vertex(Vec3 p){glVertex3f(p.x,p.y,p.z);} +void tint(Vec3 p,float alpha=1){glColor4f(p.x,p.y,p.z,alpha);} +void line(Vec3 a,Vec3 b){vertex(a);vertex(b);} +struct MeshVertex {Vec3 position,normal;}; +struct Mesh { + std::vector vertices; + std::vector indices; + GLenum mode{GL_TRIANGLES}; + void draw(const std::vector* colors=nullptr)const{ + glDisableClientState(GL_TEXTURE_COORD_ARRAY);glEnableClientState(GL_VERTEX_ARRAY);glEnableClientState(GL_NORMAL_ARRAY); + glVertexPointer(3,GL_FLOAT,sizeof(MeshVertex),vertices.data());glNormalPointer(GL_FLOAT,sizeof(MeshVertex),reinterpret_cast(vertices.data())+offsetof(MeshVertex,normal)); + if(colors){glEnableClientState(GL_COLOR_ARRAY);glColorPointer(3,GL_FLOAT,sizeof(Vec3),colors->data());}else glDisableClientState(GL_COLOR_ARRAY); + glDrawElements(mode,GLsizei(indices.size()),GL_UNSIGNED_INT,indices.data()); + glDisableClientState(GL_COLOR_ARRAY);glDisableClientState(GL_NORMAL_ARRAY);glDisableClientState(GL_VERTEX_ARRAY); + } +}; +void box(Vec3 s){ + static const Mesh mesh=[] {Mesh m;m.mode=GL_QUADS; + const std::array v{{{-.5f,-.5f,-.5f},{.5f,-.5f,-.5f},{.5f,.5f,-.5f},{-.5f,.5f,-.5f},{-.5f,-.5f,.5f},{.5f,-.5f,.5f},{.5f,.5f,.5f},{-.5f,.5f,.5f}}}; + const int faces[6][4]{{0,3,2,1},{4,5,6,7},{0,4,7,3},{1,2,6,5},{3,7,6,2},{0,1,5,4}}; + const Vec3 normals[]{{0,0,-1},{0,0,1},{-1,0,0},{1,0,0},{0,1,0},{0,-1,0}}; + for(int f=0;f<6;++f)for(int i:faces[f]){m.indices.push_back(unsigned(m.vertices.size()));m.vertices.push_back({v[i],normals[f]});} + return m;}(); + glPushMatrix();glScalef(s.x,s.y,s.z);mesh.draw();glPopMatrix(); +} +void cylinder(float radius,float bottom,float top){ + static const Mesh mesh=[] {Mesh m;constexpr unsigned n=28; + for(unsigned i=0;i<=n;++i){float a=2*pi*i/n,x=std::cos(a),z=std::sin(a);m.vertices.push_back({{x,0,z},{x,0,z}});m.vertices.push_back({{x,1,z},{x,0,z}});if(i caps; + std::vector points,colors; + std::vector fractions; + std::vector diameters;bool valid{};std::uint64_t frame{}; + bool matches(const MachinePart& p)const{return valid&&points==p.points&&(p.point_diameters.empty()?diameters==machine_pipe_diameters(p):diameters==p.point_diameters);} + void build(const MachinePart& p){ + mesh={};caps={};fractions.clear();points=p.points;diameters=machine_pipe_diameters(p); + float total=machine_pipe_length(p),distance=0;std::vector anchors; + for(std::size_t i=0;i samples; + for(std::size_t i=1;i sealed){ + if(state&&!state->pipe_pressures.empty()){for(std::size_t i=0;ipipe_pressures.size()-1,std::size_t(fractions[i]*state->pipe_pressures.size()));std::fill_n(colors.begin()+i*24,24,pressure_tint(state->pipe_pressures[index]));}mesh.draw(&colors);} + else{tint(base);mesh.draw();} + tint(base);for(unsigned end=0;end<2;++end)if(sealed[end])caps[end].draw(); + } +}; +Vec3 part_color(MachinePartKind k){ + if(machine_rotational_part(k))return k==MachinePartKind::DriveMotor?Vec3{.26f,.46f,.70f}:k==MachinePartKind::Brake?Vec3{.65f,.33f,.28f}:Vec3{.72f,.64f,.42f}; + switch(k){case MachinePartKind::Pipe:return {.52f,.65f,.70f};case MachinePartKind::Chamber:return {.4f,.55f,.64f};case MachinePartKind::Throttle:return {.8f,.58f,.25f};case MachinePartKind::CheckValve:return {.55f,.7f,.4f};case MachinePartKind::Cylinder:return {.62f,.66f,.72f};case MachinePartKind::PistonPump:return {.55f,.45f,.72f};case MachinePartKind::Reservoir:return {.4f,.78f,.67f};case MachinePartKind::Carburetor:return {.82f,.63f,.3f};case MachinePartKind::Ecu:return {.25f,.64f,.72f};default:return {.6f,.65f,.7f};} +} +void draw_part(const MachinePart& p,const MachinePartState* s,bool selected,bool ghost=false,std::array sealed={false,false},PipeVisual* visual=nullptr){ + glPushMatrix();glTranslatef(p.position.x,p.position.y,p.position.z);glRotatef(p.rotation.z,0,0,1);glRotatef(p.rotation.y,0,1,0);glRotatef(p.rotation.x,1,0,0); + const Vec3 base=selected?Vec3{1,.74f,.3f}:part_color(p.kind);tint(ghost?Vec3{.35f,.9f,.8f}:s&&p.kind!=MachinePartKind::Ecu&&!machine_rotational_part(p.kind)?pressure_tint(s->pressure):base); + if(ghost){glPolygonMode(GL_FRONT_AND_BACK,GL_LINE);glDisable(GL_LIGHTING);} + if(machine_rotational_part(p.kind)){ + const float radius=std::min(p.size.y,p.size.z)*.5f; + glPushMatrix();glRotatef(-90,0,0,1);cylinder(radius,-p.size.x*.5f,p.size.x*.5f);tint({.75f,.76f,.78f});cylinder(radius*.25f,-p.size.x*.58f,p.size.x*.58f);glPopMatrix(); + const float angle=s?float(std::fmod(s->shaft_angle,2*pi)):0; + glDisable(GL_LIGHTING);tint({.95f,.82f,.48f});glBegin(GL_LINES); + for(int i=0;i<(p.kind==MachinePartKind::Flywheel?6:1);++i){const float a=angle+i*2*pi/6;line({p.size.x*.51f,0,0},{p.size.x*.51f,radius*.85f*std::cos(a),radius*.85f*std::sin(a)});}glEnd();glEnable(GL_LIGHTING); + if(p.kind==MachinePartKind::Brake){tint({.25f,.27f,.31f});glPushMatrix();glTranslatef(0,radius*.72f,0);box({p.size.x*.9f,p.size.y*.25f,p.size.z*.3f});glPopMatrix();} + } + else if(p.kind==MachinePartKind::Pipe){if(visual)visual->draw(s,base,sealed);} + else if(p.kind==MachinePartKind::Cylinder||p.kind==MachinePartKind::PistonPump){ + const auto size=machine_part_size(p);const float top=size.y*.5f,bottom=-top,r=p.cylinder.bore*.5f; + const float piston=top-p.cylinder.stroke/(p.cylinder.compression-1)-p.cylinder.stroke*(s?std::clamp(s->piston_fraction,0.f,1.f):.6f); + cylinder(r,piston,top);tint({.78f,.8f,.85f});cylinder(r,piston-.002f,piston); + glPolygonMode(GL_FRONT_AND_BACK,GL_LINE);tint(base);cylinder(r,bottom,top);glPolygonMode(GL_FRONT_AND_BACK,ghost?GL_LINE:GL_FILL); + for(unsigned port=0;port<2;++port){const auto pos=machine_local_port(p,port);glPushMatrix();glTranslatef(pos.x,0,pos.z);tint(port?Vec3{.9f,.5f,.32f}:Vec3{.4f,.7f,.9f});const float lift=s?(port?s->exhaust_lift:s->intake_lift):0;cylinder((port?p.cylinder.exhaust_valve_diameter:p.cylinder.intake_valve_diameter)*.5f,top-lift-.001f,top-lift);glPopMatrix();} + }else if(p.kind==MachinePartKind::Carburetor){ + // The visible throat is the same elliptical cylinder used for gas storage. + glPushMatrix();glRotatef(-90,0,0,1);glScalef(1,1,p.size.z/p.size.y);cylinder(p.size.y*.5f,-p.size.x*.5f,p.size.x*.5f);glPopMatrix(); + const float bowl=std::cbrt(p.carburetor.bowl_capacity/p.carburetor.fuel_density);glPushMatrix();glTranslatef(0,-p.size.y*.5f-bowl*.5f,0);tint({.68f,.51f,.2f});box({bowl,bowl,bowl});glPopMatrix(); + glDisable(GL_LIGHTING);tint({.95f,.72f,.25f});glBegin(GL_LINES);line({0,-p.size.y*.5f,0},{0,0,0});glEnd();glEnable(GL_LIGHTING); + }else if(p.kind==MachinePartKind::Ecu){ + box(p.size);tint({.12f,.22f,.26f});glPushMatrix();glTranslatef(0,p.size.y*.55f,0);box({p.size.x*.5f,p.size.y*.15f,p.size.z*.55f});glPopMatrix(); + tint({.8f,.8f,.7f});for(int i=0;i<6;++i)for(float side:{-1.f,1.f}){glPushMatrix();glTranslatef((i-2.5f)*p.size.x*.13f,0,side*p.size.z*.55f);box({p.size.x*.07f,p.size.y*.3f,p.size.z*.12f});glPopMatrix();} + }else if(p.kind==MachinePartKind::Reservoir||p.kind==MachinePartKind::Cap||(p.kind==MachinePartKind::Junction&&p.junction_method)){ + glDisable(GL_LIGHTING);glBegin(GL_LINES); + if(p.kind==MachinePartKind::Reservoir){for(int plane=0;plane<3;++plane)for(int i=0;i<32;++i){const float a=2*pi*i/32,b=2*pi*(i+1)/32,r=.018f;const auto point=[&](float t){return plane==0?Vec3{0,r*std::cos(t),r*std::sin(t)}:plane==1?Vec3{r*std::cos(t),0,r*std::sin(t)}:Vec3{r*std::cos(t),r*std::sin(t),0};};line(point(a),point(b));}} + else if(p.kind==MachinePartKind::Cap){line({-.015f,-.015f,0},{.015f,.015f,0});line({-.015f,.015f,0},{.015f,-.015f,0});} + else{line({-.02f,0,0},{.02f,0,0});line({0,-.02f,0},{0,.02f,0});line({0,0,-.02f},{0,0,.02f});}glEnd();glEnable(GL_LIGHTING); + }else{ + const bool valve=p.kind==MachinePartKind::Throttle||p.kind==MachinePartKind::CheckValve; + if(valve)glPolygonMode(GL_FRONT_AND_BACK,GL_LINE); + if(p.kind==MachinePartKind::Throttle){glPushMatrix();glRotatef(-90,0,0,1);glScalef(1,1,p.size.z/p.size.y);cylinder(p.size.y*.5f,-p.size.x*.5f,p.size.x*.5f);glPopMatrix();}else box(p.size); + glPolygonMode(GL_FRONT_AND_BACK,ghost?GL_LINE:GL_FILL); + if(valve){glPushMatrix();if(p.kind==MachinePartKind::Throttle)glRotatef((s?s->opening:p.opening)*90,0,0,1);else glTranslatef(s?s->opening*.005f:0,0,0); + tint({.85f,.67f,.3f});glBegin(GL_TRIANGLE_FAN);glNormal3f(1,0,0);glVertex3f(0,0,0);for(int i=0;i<=32;++i){float a=2*pi*i/32;glVertex3f(0,p.diameter*.5f*std::cos(a),p.diameter*.5f*std::sin(a));}glEnd();glPopMatrix();} + if(p.kind==MachinePartKind::Throttle||p.kind==MachinePartKind::CheckValve){glDisable(GL_LIGHTING);tint({.9f,.75f,.35f});glBegin(GL_LINES);line({-p.size.x*.5f,p.size.y*.51f,0},{p.size.x*.5f,p.size.y*.51f,0});line({p.size.x*.5f,p.size.y*.51f,0},{p.size.x*.2f,p.size.y*.51f,.012f});line({p.size.x*.5f,p.size.y*.51f,0},{p.size.x*.2f,p.size.y*.51f,-.012f});glEnd();glEnable(GL_LIGHTING);} + } + if(p.injector){tint({.8f,.62f,.26f});cylinder(.006f,p.size.y*.45f,p.size.y*.5f+.025f);glPushMatrix();glTranslatef(0,p.size.y*.5f+.025f,0);tint({.2f,.25f,.3f});box({.022f,.016f,.016f});glPopMatrix();} + if(selected&&p.kind!=MachinePartKind::Pipe){glDisable(GL_LIGHTING);glPolygonMode(GL_FRONT_AND_BACK,GL_LINE);tint({1,.78f,.3f});box(machine_part_size(p));glPolygonMode(GL_FRONT_AND_BACK,GL_FILL);glEnable(GL_LIGHTING);} + glPolygonMode(GL_FRONT_AND_BACK,GL_FILL);glEnable(GL_LIGHTING);glPopMatrix(); +} +} +struct MachineRenderer3D::Impl { + std::unordered_map pipes; + PipeVisual ghost,icon_pipe; + std::array icon_parts; + std::unique_ptr icons; + std::uint64_t frame{},mesh_builds{},atlas_builds{}; + Impl(){MachineDocument d;for(int i=0;i()){} +MachineRenderer3D::~MachineRenderer3D()=default; +MachineRenderStats MachineRenderer3D::stats()const{return {impl_->mesh_builds,impl_->atlas_builds,impl_->pipes.size()};} +void MachineRenderer3D::draw(sf::RenderWindow& window,const MachineDocument& document,const MachineSnapshot* state,sf::FloatRect viewport,const OrbitCamera& camera,std::uint32_t selected,const MachinePart* ghost,const std::vector& selection){ + const auto frame=++impl_->frame; + window.pushGLStates();const auto size=window.getSize();const int x=int(viewport.position.x),y=int(size.y-viewport.position.y-viewport.size.y),w=int(viewport.size.x),h=int(viewport.size.y); + glViewport(x,y,w,h);glEnable(GL_SCISSOR_TEST);glScissor(x,y,w,h);glClearColor(.035f,.047f,.061f,1);glClear(GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT);glEnable(GL_DEPTH_TEST);glDepthFunc(GL_LEQUAL);glDisable(GL_TEXTURE_2D);glDisable(GL_BLEND);glDisable(GL_CULL_FACE); + glEnable(GL_NORMALIZE);glEnable(GL_LIGHTING);glEnable(GL_LIGHT0);glEnable(GL_COLOR_MATERIAL);glColorMaterial(GL_FRONT_AND_BACK,GL_AMBIENT_AND_DIFFUSE); + glMatrixMode(GL_PROJECTION);glLoadIdentity();const double near_clip=.03,half=near_clip*.41421356;glFrustum(-half*w/h,half*w/h,-half,half,near_clip,std::max(250.f,camera.distance*4)); + glMatrixMode(GL_MODELVIEW);glLoadIdentity();const GLfloat light[]{-.3f,1,1,0},ambient[]{.35f,.35f,.35f,1};glLightfv(GL_LIGHT0,GL_POSITION,light);glLightModelfv(GL_LIGHT_MODEL_AMBIENT,ambient); + glTranslatef(0,0,-camera.distance);glRotatef(camera.pitch,1,0,0);glRotatef(camera.yaw,0,1,0);glTranslatef(-camera.target.x,-camera.target.y,-camera.target.z); + glDisable(GL_LIGHTING);glDepthMask(GL_FALSE);glBegin(GL_LINES);const float extent=std::max(2.f,std::ceil(camera.distance/2));const float step=extent>5?.5f:.1f; + for(float i=-extent;i<=extent+.001f;i+=step){tint({.1f,.14f,.18f});line({i,0,-extent},{i,0,extent});line({-extent,0,i},{extent,0,i});}tint({.35f,.12f,.12f});line({-extent,0,0},{extent,0,0});tint({.12f,.2f,.4f});line({0,0,-extent},{0,0,extent});glEnd();glDepthMask(GL_TRUE);glEnable(GL_LIGHTING); + glDisable(GL_LIGHTING);glBegin(GL_LINES);for(const auto& link:document.links){const auto domain=machine_link_domain(document,link);if(domain==PortDomain::Gas)continue;const auto color=port_color(domain);glColor3ub(color.r,color.g,color.b);line(machine_port_position(*document.find(link.a.part),link.a.port),machine_port_position(*document.find(link.b.part),link.b.port));}glEnd();glEnable(GL_LIGHTING); + for(auto& p:document.parts){const MachinePartState* s=nullptr;if(state)for(auto& v:state->parts)if(v.id==p.id){s=&v;break;} + std::array sealed{true,true};if(p.kind==MachinePartKind::Pipe)for(auto& link:document.links){if(link.a.part==p.id)sealed[link.a.port]=document.find(link.b.part)->kind==MachinePartKind::Cap;else if(link.b.part==p.id)sealed[link.b.port]=document.find(link.a.part)->kind==MachinePartKind::Cap;} + PipeVisual* visual=nullptr;if(p.kind==MachinePartKind::Pipe){auto& cache=impl_->pipes[p.id];cache.frame=frame;if(!cache.matches(p)){cache.build(p);++impl_->mesh_builds;}visual=&cache;} + draw_part(p,s,p.id==selected||std::find(selection.begin(),selection.end(),p.id)!=selection.end(),false,sealed,visual);} + std::erase_if(impl_->pipes,[&](const auto& entry){return entry.second.frame!=frame;}); + if(ghost){PipeVisual* visual=nullptr;if(ghost->kind==MachinePartKind::Pipe){if(!impl_->ghost.matches(*ghost)){impl_->ghost.build(*ghost);++impl_->mesh_builds;}visual=&impl_->ghost;}draw_part(*ghost,nullptr,false,true,{false,false},visual);} + glDisable(GL_SCISSOR_TEST);window.popGLStates(); + // popGLStates restores the pre-call GL state, while SFML's state cache was + // changed by pushGLStates. Reconcile them even if this was the first draw. + window.resetGLStates(); +} +namespace { +void render_icon(sf::RenderTarget& target,const MachinePart& part,PipeVisual* pipe,sf::FloatRect viewport){ + const auto kind=part.kind;auto size=machine_part_size(part);float extent=std::max({size.x,size.y,size.z})*.8f; + if(kind==MachinePartKind::Pipe)extent=.19f; + if(kind==MachinePartKind::Reservoir||kind==MachinePartKind::Cap||kind==MachinePartKind::Junction)extent=.028f; + if(kind==MachinePartKind::Carburetor)extent=.065f; + target.pushGLStates();const auto window_size=target.getSize();const int x=int(viewport.position.x),y=int(window_size.y-viewport.position.y-viewport.size.y),w=int(viewport.size.x),h=int(viewport.size.y); + glViewport(x,y,w,h);glEnable(GL_SCISSOR_TEST);glScissor(x,y,w,h);glClear(GL_DEPTH_BUFFER_BIT);glEnable(GL_DEPTH_TEST);glDisable(GL_TEXTURE_2D);glDisable(GL_BLEND);glDisable(GL_CULL_FACE);glEnable(GL_NORMALIZE);glEnable(GL_LIGHTING);glEnable(GL_LIGHT0);glEnable(GL_COLOR_MATERIAL);glColorMaterial(GL_FRONT_AND_BACK,GL_AMBIENT_AND_DIFFUSE); + glMatrixMode(GL_PROJECTION);glLoadIdentity();glOrtho(-extent*w/h,extent*w/h,-extent,extent,-2,2);glMatrixMode(GL_MODELVIEW);glLoadIdentity();const GLfloat light[]{-.3f,1,1,0},ambient[]{.4f,.4f,.4f,1};glLightfv(GL_LIGHT0,GL_POSITION,light);glLightModelfv(GL_LIGHT_MODEL_AMBIENT,ambient);glRotatef(22,1,0,0);glRotatef(-32,0,1,0); + draw_part(part,nullptr,false,false,{true,true},pipe);glDisable(GL_SCISSOR_TEST);target.popGLStates(); +} +} +void MachineRenderer3D::draw_icon(sf::RenderWindow& window,MachinePartKind kind,sf::FloatRect viewport){ + constexpr unsigned width=64,height=48; + if(!impl_->icons){ + sf::ContextSettings settings;settings.depthBits=24;settings.majorVersion=2;settings.minorVersion=1; + impl_->icons=std::make_unique(sf::Vector2u{width*machine_part_kind_count,height},settings); + impl_->icons->clear(sf::Color::Transparent); + for(int i=0;iicons,impl_->icon_parts[i],i==int(MachinePartKind::Pipe)?&impl_->icon_pipe:nullptr,{{float(i*width),0},{float(width),float(height)}}); + impl_->icons->display();++impl_->atlas_builds;window.resetGLStates(); + } + sf::Sprite sprite(impl_->icons->getTexture(),{{int(kind)*int(width),0},{int(width),int(height)}}); + sprite.setPosition(viewport.position);sprite.setScale({viewport.size.x/width,viewport.size.y/height});window.draw(sprite); +} +} diff --git a/src/machine_sensors.cpp b/src/machine_sensors.cpp new file mode 100644 index 0000000..6f607f1 --- /dev/null +++ b/src/machine_sensors.cpp @@ -0,0 +1,77 @@ +#include "FluidSim/Machine.hpp" +#include +#include +#include +#include + +namespace fluidsim::port { +namespace { +constexpr const char* keys[]{"pressure","differential_pressure","gas_temperature","mass_flow","crank","throttle_position"}; +constexpr const char* names[]{"Pressure / MAP","Differential pressure","Gas temperature / IAT","Total mass flow","Crank speed / position","Throttle position"}; +static_assert(std::size(keys)==machine_sensor_kind_count&&std::size(names)==machine_sensor_kind_count); +void require(bool condition,const std::string& message){if(!condition)throw std::invalid_argument(message);} +} +const char* machine_sensor_key(MachineSensorKind kind){require(int(kind)>=0&&int(kind)=0&&int(kind) machine_sensor_position(const MachineDocument& doc,const MachineSensorTap& tap){ + const auto host=doc.find(tap.part);if(!host)return {}; + if(host->kind!=MachinePartKind::Pipe)return machine_world_point(*host,{0,machine_part_size(*host).y*.5f,0}); + return GeometryPath{machine_pipe_path(*host)}.position(machine_pipe_length(*host)*tap.position); +} +MachineSensor* MachineDocument::find_sensor(std::uint32_t id){for(auto& s:sensors)if(s.id==id)return &s;return nullptr;} +const MachineSensor* MachineDocument::find_sensor(std::uint32_t id)const{for(const auto& s:sensors)if(s.id==id)return &s;return nullptr;} +void MachineDocument::remove_sensor(std::uint32_t id){for(auto& node:logic.nodes)if(node.kind==LogicNodeKind::Sensor&&node.sensor==id){if(const auto sensor=find_sensor(id))node.binding_label=sensor->name;node.sensor=0;}std::erase_if(sensors,[&](const auto& s){return s.id==id;});} +MachineSensor& MachineDocument::add_sensor(MachineSensorKind kind,std::uint32_t host,float position){ + const auto part=find(host);require(part&&machine_sensor_supported(*part,kind),"This part does not support that sensor"); + require(std::isfinite(position)&&position>=0&&position<=1,"Sensor pipe position must be in 0..1"); + MachineSensor sensor;sensor.id=next_sensor_id++;sensor.kind=kind;sensor.target={host,position,part->name};sensor.name=std::string(machine_sensor_name(kind))+" / "+part->name;if(sensor.name.size()>120)sensor.name.resize(120); + sensors.push_back(std::move(sensor));return sensors.back(); +} +void validate_machine_sensors(const MachineDocument& doc){ + require(doc.sensors.size()<=1024,"Machine exceeds 1024 sensor attachments");std::set ids; + for(const auto& s:doc.sensors){ + require(s.id>0&&s.id<1000000000&&ids.insert(s.id).second,"Invalid or duplicate sensor ID");machine_sensor_key(s.kind); + require(!s.name.empty()&&s.name.size()<=120,"Sensor name must have 1..120 characters"); + for(const auto* tap:{&s.target,&s.reference})require(tap->part<1000000000&&tap->label.size()<=120&&std::isfinite(tap->position)&&tap->position>=0&&tap->position<=1,"Invalid sensor host or pipe position"); + require(std::isfinite(s.angle_offset)&&std::abs(s.angle_offset)<=1000000,"Invalid crank pickup offset"); + for(unsigned i=0;i0&&doc.next_sensor_id<1000000000&&(ids.empty()||doc.next_sensor_id>*ids.rbegin()),"Invalid next sensor ID"); +} +} diff --git a/src/machine_simulation.cpp b/src/machine_simulation.cpp new file mode 100644 index 0000000..5c0192f --- /dev/null +++ b/src/machine_simulation.cpp @@ -0,0 +1,382 @@ +#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);} +} diff --git a/src/mechanics.cpp b/src/mechanics.cpp new file mode 100644 index 0000000..8616601 --- /dev/null +++ b/src/mechanics.cpp @@ -0,0 +1,101 @@ +#include "FluidSim/Mechanics.hpp" +#include +#include +#include +#include +#include + +namespace fluidsim::port { +namespace { +void require(bool condition,const char* message){if(!condition)throw std::invalid_argument(message);} +void drag_parameters(double constant,double viscous,double quadratic){require(std::isfinite(constant)&&constant>=0&&std::isfinite(viscous)&&viscous>=0&&std::isfinite(quadratic)&&quadratic>=0,"Mechanical drag coefficients must be finite and nonnegative");} +void drive_parameters(const DriveParameters& p){require(p.mode==DriveMode::Torque||p.mode==DriveMode::Speed,"Unknown drive mode");require(std::isfinite(p.command),"Drive command must be finite");} +double sign(double value){return value>0?1.:value<0?-1.:0.;} +} +struct MechanicalSystem::Impl { + struct Shaft {ShaftParameters parameters;ShaftState state;double pending_torque{};std::vector drives,brakes;}; + struct Drive {DriveParameters parameters;DriveState state;}; + struct Brake {BrakeParameters parameters;BrakeState state;double step_start_loss{};}; + std::vector shafts;std::vector drives;std::vector brakes;std::set ids;bool started{}; + Shaft& shaft(std::uint32_t id){for(auto& s:shafts)if(s.state.id==id)return s;throw std::invalid_argument("Unknown mechanical shaft");} + const Shaft& shaft(std::uint32_t id)const{for(const auto& s:shafts)if(s.state.id==id)return s;throw std::invalid_argument("Unknown mechanical shaft");} + Drive& drive(std::uint32_t id){for(auto& d:drives)if(d.state.id==id)return d;throw std::invalid_argument("Unknown mechanical drive");} + Brake& brake(std::uint32_t id){for(auto& b:brakes)if(b.state.id==id)return b;throw std::invalid_argument("Unknown mechanical brake");} + void claim(std::uint32_t id){require(!started,"Mechanical topology changes require a fresh simulation");require(id&&ids.insert(id).second,"Duplicate or invalid mechanical part ID");} + void validate_drive(const Shaft& s,const DriveParameters& parameters,std::uint32_t replacing=0)const{ + drive_parameters(parameters);if(!parameters.enabled||parameters.mode!=DriveMode::Speed)return; + require(!s.parameters.forward_only||parameters.command>=0,"A forward-only shaft cannot use a negative speed target"); + for(auto i:s.drives){const auto& other=drives[i];if(other.state.id!=replacing)require(!(other.parameters.enabled&&other.parameters.mode==DriveMode::Speed),"A shaft supports only one active ideal speed drive; reaction sharing would be undefined");} + } + void losses(Shaft& s,double speed,double duration){ + const double magnitude=std::abs(speed),direction=sign(speed),distance=magnitude*duration;const auto& p=s.parameters; + const double friction=p.friction_constant+p.friction_viscous*magnitude+p.friction_quadratic*magnitude*magnitude; + s.state.friction_loss+=friction*distance;s.state.friction_torque-=direction*friction*duration; + for(auto index:s.brakes){auto& b=brakes[index];if(!b.parameters.enabled)continue;const auto& c=b.parameters;const double torque=c.constant+c.viscous*magnitude+c.quadratic*magnitude*magnitude; + b.state.torque-=direction*torque*duration;b.state.loss+=torque*distance;s.state.brake_loss+=torque*distance;s.state.brake_torque-=direction*torque*duration; + } + } + void static_reaction(Shaft& s,double torque,double constant,double duration){ + // At rest only the needed fraction of Coulomb capacity reacts; no work. + const double fraction=constant>0?std::min(1.,std::abs(torque)/constant):0,direction=sign(torque); + s.state.friction_torque-=direction*s.parameters.friction_constant*fraction*duration; + for(auto index:s.brakes){auto& b=brakes[index];if(!b.parameters.enabled)continue;const double reaction=-direction*b.parameters.constant*fraction*duration;b.state.torque+=reaction;s.state.brake_torque+=reaction;} + if(s.parameters.forward_only&&torque<0) s.state.constraint_torque+=(-torque+direction*constant*fraction)*duration; + } + void advance(Shaft& s,double dt){ + auto& state=s.state;const double before=state.speed,energy=state.kinetic_energy;state.previous_angle=state.angle; + state.friction_torque=state.brake_torque=state.constraint_torque=0;double torque=s.pending_torque,constant=s.parameters.friction_constant,viscous=s.parameters.friction_viscous,quadratic=s.parameters.friction_quadratic;Drive* speed_drive=nullptr; + for(auto index:s.drives){auto& d=drives[index];d.state.torque=d.state.power=0;if(!d.parameters.enabled)continue;if(d.parameters.mode==DriveMode::Speed)speed_drive=&d;else torque+=d.parameters.command;} + for(auto index:s.brakes){auto& b=brakes[index];b.state.torque=b.state.power=0;b.step_start_loss=b.state.loss;if(b.parameters.enabled){constant+=b.parameters.constant;viscous+=b.parameters.viscous;quadratic+=b.parameters.quadratic;}} + const double old_losses=state.friction_loss+state.brake_loss; + double displacement=0; + if(speed_drive){ + state.speed=speed_drive->parameters.command;displacement=state.speed*dt;losses(s,state.speed,dt); + }else{ + double remaining=dt; + // A step can decelerate to rest, then accelerate in the other direction. + // Each segment uses midpoint work, so its kinetic-energy change equals + // source work minus drag work even when a stop splits the interval. + for(int segment=0;segment<2&&remaining>0;++segment){ + const double w0=std::abs(state.speed),direction=w0>0?sign(state.speed):sign(torque); + if(w0==0&&(std::abs(torque)<=constant||(s.parameters.forward_only&&torque<=0))){static_reaction(s,torque,constant,remaining);break;} + const double source=torque*direction; + const double a=quadratic*.25,b=state.inertia/remaining+viscous*.5+quadratic*w0*.5; + const double c=quadratic*w0*w0*.25-state.inertia*w0/remaining-source+constant+viscous*w0*.5; + double duration=remaining,w1=0; + if(c>0&&w0>0){duration=state.inertia*w0/(constant+viscous*w0*.5+quadratic*w0*w0*.25-source);duration=std::clamp(duration,0.,remaining);} + else if(c<=0){w1=a>0?-2*c/(b+std::sqrt(b*b-4*a*c)):-c/b;} + const double midpoint=direction*(w0+w1)*.5;displacement+=midpoint*duration;losses(s,midpoint,duration);state.speed=direction*w1;remaining-=duration; + if(w1!=0)break; + } + } + state.angle+=displacement;state.kinetic_energy=.5*state.inertia*state.speed*state.speed; + state.external_work+=s.pending_torque*displacement; + for(auto index:s.drives){auto& d=drives[index];if(!d.parameters.enabled||d.parameters.mode!=DriveMode::Torque)continue;const double work=d.parameters.command*displacement;d.state.torque=d.parameters.command;d.state.work+=work;d.state.power=work/dt;state.drive_work+=work;} + state.friction_torque/=dt;state.brake_torque/=dt;state.constraint_torque/=dt; + state.net_torque=state.inertia*(state.speed-before)/dt;state.source_torque=torque;state.external_torque=s.pending_torque; + if(speed_drive){const double work=state.kinetic_energy-energy-torque*displacement+(state.friction_loss+state.brake_loss-old_losses); + speed_drive->state.torque=state.net_torque-torque-state.friction_torque-state.brake_torque;speed_drive->state.work+=work;speed_drive->state.power=work/dt;state.drive_work+=work;state.source_torque+=speed_drive->state.torque;} + for(auto index:s.brakes){auto& b=brakes[index];b.state.torque/=dt;b.state.power=(b.state.loss-b.step_start_loss)/dt;} + s.pending_torque=0; + require(std::isfinite(state.angle)&&std::isfinite(state.speed)&&std::isfinite(state.kinetic_energy)&&std::isfinite(state.drive_work)&&std::isfinite(state.external_work)&&std::isfinite(state.friction_loss)&&std::isfinite(state.brake_loss),"Nonfinite mechanical state"); + } +}; +MechanicalSystem::MechanicalSystem():impl_(std::make_unique()){} +MechanicalSystem::~MechanicalSystem()=default; +void MechanicalSystem::add_shaft(std::uint32_t id,const ShaftParameters& p){ + require(std::isfinite(p.inertia)&&p.inertia>0&&std::isfinite(p.initial_speed)&&std::isfinite(p.initial_angle),"Invalid shaft inertia or initial state");require(!p.forward_only||p.initial_speed>=0,"A forward-only shaft cannot start in reverse");drag_parameters(p.friction_constant,p.friction_viscous,p.friction_quadratic);impl_->claim(id); + Impl::Shaft s;s.parameters=p;s.state.id=id;s.state.inertia=p.inertia;s.state.angle=s.state.previous_angle=p.initial_angle;s.state.speed=p.initial_speed;s.state.initial_energy=s.state.kinetic_energy=.5*p.inertia*p.initial_speed*p.initial_speed;impl_->shafts.push_back(std::move(s)); +} +void MechanicalSystem::add_flywheel(std::uint32_t id,std::uint32_t shaft,double inertia){require(std::isfinite(inertia)&&inertia>0,"Flywheel inertia must be positive and finite");auto& s=impl_->shaft(shaft);impl_->claim(id);s.state.inertia+=inertia;s.state.initial_energy=s.state.kinetic_energy=.5*s.state.inertia*s.state.speed*s.state.speed;} +void MechanicalSystem::add_drive(std::uint32_t id,std::uint32_t shaft,const DriveParameters& p){auto& s=impl_->shaft(shaft);impl_->validate_drive(s,p);impl_->claim(id);s.drives.push_back(impl_->drives.size());Impl::Drive d;d.parameters=p;d.state.id=id;d.state.shaft=shaft;impl_->drives.push_back(d);} +void MechanicalSystem::add_brake(std::uint32_t id,std::uint32_t shaft,const BrakeParameters& p){drag_parameters(p.constant,p.viscous,p.quadratic);auto& s=impl_->shaft(shaft);impl_->claim(id);s.brakes.push_back(impl_->brakes.size());Impl::Brake b;b.parameters=p;b.state.id=id;b.state.shaft=shaft;impl_->brakes.push_back(b);} +void MechanicalSystem::set_drive(std::uint32_t id,const DriveParameters& p){auto& d=impl_->drive(id);impl_->validate_drive(impl_->shaft(d.state.shaft),p,id);d.parameters=p;} +void MechanicalSystem::set_brake(std::uint32_t id,const BrakeParameters& p){drag_parameters(p.constant,p.viscous,p.quadratic);impl_->brake(id).parameters=p;} +void MechanicalSystem::set_friction(std::uint32_t id,double constant,double viscous,double quadratic){drag_parameters(constant,viscous,quadratic);auto& p=impl_->shaft(id).parameters;p.friction_constant=constant;p.friction_viscous=viscous;p.friction_quadratic=quadratic;} +void MechanicalSystem::add_torque(std::uint32_t shaft,double torque){require(std::isfinite(torque),"Shaft torque must be finite");auto& s=impl_->shaft(shaft);require(std::isfinite(s.pending_torque+torque),"Accumulated shaft torque overflow");s.pending_torque+=torque;} +void MechanicalSystem::step(double dt){require(std::isfinite(dt)&&dt>0,"Mechanical timestep must be positive and finite");impl_->started=true;for(auto& shaft:impl_->shafts)impl_->advance(shaft,dt);} +const ShaftState& MechanicalSystem::shaft(std::uint32_t id)const{return impl_->shaft(id).state;} +const DriveState& MechanicalSystem::drive(std::uint32_t id)const{return impl_->drive(id).state;} +const BrakeState& MechanicalSystem::brake(std::uint32_t id)const{return impl_->brake(id).state;} +} diff --git a/src/scenario_menu.cpp b/src/scenario_menu.cpp index 847b18a..21faa14 100644 --- a/src/scenario_menu.cpp +++ b/src/scenario_menu.cpp @@ -1,83 +1,187 @@ #include "FluidSim/Desktop/ScenarioMenu.hpp" +#include "FluidSim/Desktop/MachineRenderer3D.hpp" +#include "FluidSim/Desktop/WindowPresentation.hpp" +#include "FluidSim/Machine.hpp" #include -namespace fluidsim::port::desktop { -std::optional choose_scenario(sf::RenderWindow& window,const sf::Font* font,const std::vector& examples) { - if(examples.empty())return std::nullopt; - int selected=0,first=0; - std::vector categories; - for(const auto& e:examples) if(std::find(categories.begin(),categories.end(),e.category)==categories.end())categories.push_back(e.category); - std::string category; - struct Item { std::string title,description;std::size_t value; }; +#include +#include +#include +#include - auto label=[&](const std::string& str,float x,float y,unsigned size,sf::Color color=sf::Color::White) { - if(!font)return; - sf::Text text(*font,str,size);text.setPosition({x,y});text.setFillColor(color);window.draw(text); - }; - while(window.isOpen()) { - const auto size=window.getSize();const float w=float(size.x),h=float(size.y); - std::vector items; - if(category.empty()) for(std::size_t i=0;istd::optional { - if(category.empty()) {category=categories[items[selected].value];selected=0;first=0;navigating=true;return std::nullopt;} - return items[selected].value; - }; - auto back=[&] {category.clear();selected=0;first=0;navigating=true;}; - auto reveal=[&] {if(selected=first+rows)first=selected-rows+1;}; - while(auto event=window.pollEvent()) { - if(event->is())return std::nullopt; - if(auto key=event->getIf()) { - switch(key->code) { - case sf::Keyboard::Key::Escape:if(category.empty())return std::nullopt;back();break; - case sf::Keyboard::Key::Backspace:case sf::Keyboard::Key::Left:if(!category.empty())back();break; - case sf::Keyboard::Key::Enter:if(auto choice=activate())return choice;break; - case sf::Keyboard::Key::Up:selected=std::max(0,selected-1);reveal();break; - case sf::Keyboard::Key::Down:case sf::Keyboard::Key::Tab:selected=std::min(count-1,selected+1);reveal();break; - case sf::Keyboard::Key::PageUp:selected=std::max(0,selected-rows);reveal();break; - case sf::Keyboard::Key::PageDown:selected=std::min(count-1,selected+rows);reveal();break; - case sf::Keyboard::Key::Home:selected=0;reveal();break; - case sf::Keyboard::Key::End:selected=count-1;reveal();break; - default:break; - } - } - if(auto wheel=event->getIf()) { - first=std::clamp(first+(wheel->delta<0?1:-1),0,std::max(0,count-rows)); - selected=std::clamp(selected,first,std::min(count-1,first+rows-1)); - } - if(auto click=event->getIf();click && click->button==sf::Mouse::Button::Left) { - sf::Vector2f point{float(click->position.x),float(click->position.y)}; - if(!category.empty() && sf::FloatRect({24,54},{150,36}).contains(point))back(); - if(start.contains(point)) {if(auto choice=activate())return choice;} - for(int row=0;rowrows) { - const float track=rows*86.f, thumb=track*std::min(1.f,float(rows)/count); - sf::RectangleShape bar({4,thumb});bar.setPosition({w-21,105+(track-thumb)*first/std::max(1,count-rows)});bar.setFillColor({170,170,170});window.draw(bar); - } - sf::RectangleShape button(start.size);button.setPosition(start.position);button.setFillColor({55,55,55});window.draw(button); - label(category.empty()?"Browse variants [Enter]":"Start selected [Enter]",38,h-76,16); - label("Wheel / Up / Down / Page keys | "+std::to_string(selected+1)+" / "+std::to_string(count),24,h-28,12,{170,170,170}); - window.display(); +namespace fluidsim::port::desktop { +namespace { +const sf::Color ink{232,238,246},muted{152,168,190},accent{116,170,235},edge{53,67,86},surface{23,31,43}; +std::string lower(std::string text){for(auto& c:text)c=char(std::tolower(static_cast(c)));return text;} +std::string fit(const sf::Font* font,std::string text,float width,unsigned size){ + if(!font)return {}; + sf::Text measured(*font,text,size);if(measured.getLocalBounds().size.x<=width)return text; + while(!text.empty()){text.pop_back();measured.setString(text+"...");if(measured.getLocalBounds().size.x<=width)return text+"...";}return {}; +} +void text(sf::RenderWindow& w,const sf::Font* font,const std::string& value,float x,float y,float width,unsigned size=14,sf::Color color=ink){ + if(!font)return; + sf::Text label(*font,fit(font,value,width,size),size);label.setPosition({x,y});label.setFillColor(color);w.draw(label); +} +void wrap(sf::RenderWindow& w,const sf::Font* font,const std::string& value,float x,float y,float width,unsigned size,int lines,sf::Color color=muted){ + if(!font)return; + std::istringstream words(value);std::string word,line;int row=0;sf::Text measured(*font,"",size); + while(words>>word){const auto candidate=line.empty()?word:line+" "+word;measured.setString(candidate); + if(!line.empty()&&measured.getLocalBounds().size.x>width){text(w,font,line,x,y+row*(size+7),width,size,color);if(++row>=lines)return;line=word;}else line=candidate; + }if(row example;std::filesystem::path machine;}; + struct Action{sf::FloatRect rect;std::string title,subtitle;std::function run;bool selected{},enabled{true},primary{};int kind{};}; + std::vector examples;std::vector machines;std::vector categories{"All engines"}; + std::vector items,saved_items;MenuScreen page{MenuScreen::Home};int selected{},first{},category{},category_first{},home_selection{}; + bool quit{},search_focus{};std::string query;sf::Vector2f mouse{-1,-1};std::optional choice; + MachineRenderer3D renderer;std::optional preview;OrbitCamera camera;std::string preview_key,preview_note; + Impl(std::vector e,std::vector m):examples(std::move(e)),machines(std::move(m)){ + for(const auto& item:examples)if(std::find(categories.begin(),categories.end(),item.category)==categories.end())categories.push_back(item.category); + for(const auto& file:machines){std::string title=file.stem().stem().string();try{title=load_machine(file).name;}catch(const std::exception&){}saved_items.push_back({title,file.filename().string()+" / edit or play",{},file});} + } + void refresh(){ + items.clear();const auto search=lower(query); + const auto add=[&](Item item){if(search.empty()||lower(item.title+" "+item.description).find(search)!=std::string::npos)items.push_back(std::move(item));}; + if(page==MenuScreen::Workshop){add({"New machine","Start an empty workshop and build your own machine",{}, {}});for(const auto& item:saved_items)add(item);} + if(page==MenuScreen::Scenarios)for(std::size_t i=0;i=first+rows)first=selected-rows+1;} + void activate(bool play=false){ + if(page==MenuScreen::Home){if(home_selection==0)navigate(MenuScreen::Workshop);else if(home_selection==1)navigate(MenuScreen::Scenarios);else quit=true;return;} + if(items.empty())return; + const auto& item=items[selected];choice=ScenarioMenuChoice{item.example,item.machine,play&&!item.machine.empty()}; + } + std::vector actions(sf::Vector2u size){ + const auto l=MenuLayout::calculate(size,page==MenuScreen::Scenarios);std::vector result; + const auto add=[&](sf::FloatRect r,std::string title,std::function run,bool active=false,bool enabled=true,bool primary=false,std::string subtitle={},int kind=0){result.push_back({r,std::move(title),std::move(subtitle),std::move(run),active,enabled,primary,kind});}; + if(page==MenuScreen::Home){ + const float top=std::max(254.f,l.height*.36f); + add({{64,top},{416,92}},"Machine Workshop",[this]{navigate(MenuScreen::Workshop);},home_selection==0,true,false,"Build, connect and tune your own machines",2); + add({{64,top+108},{416,92}},"Engine Scenarios",[this]{navigate(MenuScreen::Scenarios);},home_selection==1,true,false,"Explore the engine collection",2); + add({{64,top+224},{196,44}},"Quit",[this]{quit=true;},home_selection==2); + return result; + } + add(l.back,"< Back",[this]{navigate(MenuScreen::Home);}); + first=std::clamp(first,0,std::max(0,int(items.size())-l.rows)); + for(int row=0;rowexample?"engine:"+std::to_string(*item->example):item->machine.empty()?"new":"machine:"+item->machine.string()):"none"; + if(key==preview_key)return; + preview_key=key;preview.reset();preview_note.clear(); + try{ + if(item&&item->example){const auto& engine=examples[*item->example];if(engine.kind==ScenarioKind::TwoStroke){preview_note="TWO-STROKE / ENGINE SCENARIO";return;}preview=machine_from_engine(engine);} + else if(item&&!item->machine.empty())preview=load_machine(item->machine); + else if(item)preview=MachineDocument{}; + else if(page==MenuScreen::Home){auto found=std::find_if(examples.begin(),examples.end(),[](const auto& e){return e.id=="flat6";});if(found!=examples.end())preview=machine_from_engine(*found);else preview=machine_engine_example();} + if(!preview)return; + if(preview->parts.empty()){camera.target={0,.22f,0};camera.distance=1.8f;camera.pitch=24;camera.yaw=-32;return;} + Vec3 lo{1e6f,1e6f,1e6f},hi{-1e6f,-1e6f,-1e6f}; + const auto include=[&](Vec3 p){lo={std::min(lo.x,p.x),std::min(lo.y,p.y),std::min(lo.z,p.z)};hi={std::max(hi.x,p.x),std::max(hi.y,p.y),std::max(hi.z,p.z)};}; + for(const auto& part:preview->parts){if(part.kind==MachinePartKind::Pipe)for(auto p:machine_pipe_path(part))include(p);else{const auto half=machine_math::multiply(machine_part_size(part),.5f);include(machine_math::subtract(part.position,half));include(machine_math::add(part.position,half));}} + camera.target=machine_math::multiply(machine_math::add(lo,hi),.5f);camera.distance=std::clamp(machine_math::length(machine_math::subtract(hi,lo))*1.3f+.2f,.8f,100.f);camera.pitch=24;camera.yaw=-32; + }catch(const std::exception&){preview.reset();preview_note="No 3D preview available";} + } +}; +ScenarioMenu::ScenarioMenu(std::vector e,std::vector m):impl_(std::make_unique(std::move(e),std::move(m))){} +ScenarioMenu::~ScenarioMenu()=default; +bool ScenarioMenu::finished()const{return impl_->quit;} +MenuScreen ScenarioMenu::screen()const{return impl_->page;} +std::optional ScenarioMenu::take_choice(){auto result=impl_->choice;impl_->choice.reset();return result;} +bool ScenarioMenu::handle_event(const sf::Event& event,sf::Vector2u size){ + auto& m=*impl_;const auto layout=MenuLayout::calculate(size,m.page==MenuScreen::Scenarios); + if(event.is()){m.quit=true;return true;} + if(auto move=event.getIf()){m.mouse=sf::Vector2f(move->position);return true;} + if(auto press=event.getIf();press&&press->button==sf::Mouse::Button::Left){m.search_focus=m.page!=MenuScreen::Home&&layout.search.contains(sf::Vector2f(press->position));return true;} + if(auto typed=event.getIf();typed&&m.search_focus){if(typed->unicode>=32&&typed->unicode<127&&m.query.size()<80){m.query+=char(typed->unicode);m.selected=m.first=0;m.refresh();}return true;} + if(auto key=event.getIf()){ + using K=sf::Keyboard::Key;const auto k=key->code; + if(m.search_focus){if(k==K::Escape||k==K::Enter)m.search_focus=false;else if(k==K::Backspace&&!m.query.empty()){m.query.pop_back();m.selected=m.first=0;m.refresh();}return true;} + if(key->control&&k==K::F&&m.page!=MenuScreen::Home){m.search_focus=true;return true;} + if(k==K::Escape||k==K::Backspace){if(m.page==MenuScreen::Home){if(k==K::Escape)m.quit=true;}else m.navigate(MenuScreen::Home);return true;} + if(k==K::B){m.choice=ScenarioMenuChoice{};return true;} + if(k==K::Enter||k==K::Space){m.activate(k==K::Space);return true;} + int& selected=m.page==MenuScreen::Home?m.home_selection:m.selected;const int count=m.page==MenuScreen::Home?3:int(m.items.size()); + if(k==K::Up)selected=std::max(0,selected-1);else if(k==K::Down||k==K::Tab)selected=std::min(std::max(0,count-1),selected+1);else if(k==K::Home)selected=0;else if(k==K::End)selected=std::max(0,count-1);else if(k==K::PageDown)selected=std::min(std::max(0,count-1),selected+layout.rows);else if(k==K::PageUp)selected=std::max(0,selected-layout.rows);else return false; + m.reveal(layout.rows);return true; + } + if(auto wheel=event.getIf();wheel&&m.page!=MenuScreen::Home){ + if(m.page==MenuScreen::Scenarios&&wheel->position.x<232){const int rows=std::max(1,int((layout.height-230)/44));m.category_first=std::clamp(m.category_first+(wheel->delta<0?1:-1),0,std::max(0,int(m.categories.size())-rows));} + else{m.first=std::clamp(m.first+(wheel->delta<0?1:-1),0,std::max(0,int(m.items.size())-layout.rows));if(!m.items.empty())m.selected=std::clamp(m.selected,m.first,std::min(int(m.items.size())-1,m.first+layout.rows-1));}return true; + } + if(auto click=event.getIf();click&&click->button==sf::Mouse::Button::Left){m.mouse=sf::Vector2f(click->position);auto actions=m.actions(size);for(auto& action:actions)if(action.rect.contains(m.mouse)){if(action.enabled)action.run();return true;}return true;} + return false; +} +void ScenarioMenu::draw(sf::RenderWindow& w,const sf::Font* font){ + auto& m=*impl_;const auto size=w.getSize();const auto l=MenuLayout::calculate(size,m.page==MenuScreen::Scenarios);const auto full=sf::View(sf::FloatRect({0,0},sf::Vector2f(size))); + w.setView(full);w.clear({11,16,24}); + m.update_preview();const bool home=m.page==MenuScreen::Home; + const sf::FloatRect scene=home?sf::FloatRect{{528,96},{l.width-560,l.height-200}}:sf::FloatRect{l.detail.position,{l.detail.size.x,l.detail.size.y*.43f}}; + if(!home)box(w,l.detail); + if(m.preview)m.renderer.draw(w,*m.preview,nullptr,scene,m.camera,0);else{ + box(w,scene,{15,22,32});for(int i=0;i<6;++i){sf::CircleShape ring(30.f+i*13);ring.setFillColor(sf::Color::Transparent);ring.setOutlineColor({38,55,76});ring.setOutlineThickness(1);ring.setPosition({scene.position.x+scene.size.x*.5f-ring.getRadius(),scene.position.y+scene.size.y*.5f-ring.getRadius()});w.draw(ring);} + text(w,font,m.preview_note,scene.position.x+20,scene.position.y+scene.size.y*.5f-8,scene.size.x-40,12,accent); + } + w.setView(full); + if(home){ + text(w,font,"FLUIDSIM",64,96,440,54);text(w,font,"THE MACHINE SANDBOX",68,170,412,14,accent); + wrap(w,font,"Explore engine sound. Build machines. Put your ideas in motion.",68,204,390,14,2); + text(w,font,"BUILD. CONNECT. SIMULATE.",552,l.height-92,l.width-600,18,ink); + }else{ + text(w,font,m.page==MenuScreen::Workshop?"MACHINE WORKSHOP":"ENGINE SCENARIOS",174,31,l.width-220,26); + text(w,font,m.page==MenuScreen::Workshop?"Create a machine, or continue one of your saved designs.":"Choose a category, select an engine, then start your session.",32,80,l.width-64,13,muted); + box(w,l.search,{14,21,31},m.search_focus?accent:edge);text(w,font,m.query.empty()&&!m.search_focus?"Search... [Ctrl+F]":m.query+(m.search_focus?"|":""),l.search.position.x+10,111,l.search.size.x-20,13,muted); + if(m.page==MenuScreen::Scenarios){text(w,font,"COLLECTION",32,112,196,12,accent);if(int(m.categories.size())>std::max(1,int((l.height-230)/44)))text(w,font,"Scroll for more categories",32,l.height-64,196,11,muted);} + const auto d=l.detail;const float info_y=scene.position.y+scene.size.y+18; + if(!m.items.empty()){ + const auto& item=m.items[m.selected];wrap(w,font,item.title,d.position.x+20,info_y,d.size.x-40,18,2,ink); + const auto subtitle=item.example?m.examples[*item.example].category:item.machine.empty()?"NEW DESIGN":"SAVED MACHINE"; + text(w,font,subtitle,d.position.x+20,info_y+58,d.size.x-40,11,accent); + wrap(w,font,item.description,d.position.x+20,info_y+86,d.size.x-40,13,3); + }else{ + text(w,font,"No matches",l.list_x,174,l.list_width,18);wrap(w,font,"Clear your search or choose another category.",l.list_x,210,l.list_width,13,2); + } + if(int(m.items.size())>l.rows){const float height=l.rows*78.f;box(w,{{l.detail.position.x-14,154},{3,height}},{32,43,58});box(w,{{l.detail.position.x-14,154+height*m.first/m.items.size()},{3,height*l.rows/m.items.size()}},accent);} + } + std::string hover_text; + for(const auto& action:m.actions(size)){ + const bool hover=action.rect.contains(m.mouse);box(w,action.rect,action.primary?sf::Color(42,77,115):action.selected||hover?sf::Color(32,48,68):surface,action.selected||hover||action.primary?accent:edge); + const float x=action.rect.position.x+16,y=action.rect.position.y,width=action.rect.size.x-32; + text(w,font,action.title,x,y+(action.kind==2?14:action.kind==1?10:9),width,action.kind==2?21:action.kind==1?15:14,action.enabled?ink:sf::Color(91,107,128)); + if(!action.subtitle.empty())text(w,font,action.subtitle,x,y+(action.kind==2?54:42),width,12,muted); + if(hover&&(fit(font,action.title,width,action.kind==2?21:action.kind==1?15:14)!=action.title||(!action.subtitle.empty()&&fit(font,action.subtitle,width,12)!=action.subtitle)))hover_text=action.title+(action.subtitle.empty()?"":" / "+action.subtitle); + } + text(w,font,home?"A sandbox for engines, compressed air and mechanical systems.":"Up / Down: select Enter: open Escape: main menu B: new machine",32,l.height-30,l.width-64,12,muted); + if(!hover_text.empty()){const float width=std::min(520.f,l.width-48),x=std::clamp(m.mouse.x+12,24.f,l.width-width-24),y=std::clamp(m.mouse.y+32,24.f,l.height-112);box(w,{{x,y},{width,70}},{13,20,30},accent);wrap(w,font,hover_text,x+12,y+9,width-24,12,3,ink);} +} +std::optional choose_scenario(sf::RenderWindow& window,const sf::Font* font,const std::vector& examples){ + maximize_window(window);ScenarioMenu menu(examples,machine_files()); + while(window.isOpen()&&!menu.finished()){ + while(auto event=window.pollEvent()){menu.handle_event(*event,window.getSize());if(auto choice=menu.take_choice())return choice;if(menu.finished())return {};} + menu.draw(window,font);window.display(); + }return {}; } } diff --git a/src/simulation_ring_buffer.cpp b/src/simulation_ring_buffer.cpp index 35707ee..4df7b36 100644 --- a/src/simulation_ring_buffer.cpp +++ b/src/simulation_ring_buffer.cpp @@ -49,6 +49,7 @@ bool SimulationRingBuffer::wait_for_space(std::size_t target_fill, std::stop_tok void SimulationRingBuffer::reset_consumer() { consumer_initialized_ = false; } +void SimulationRingBuffer::discard(){read_head_.store(write_head_.load(std::memory_order_acquire),std::memory_order_release);consumer_initialized_=false;} std::size_t SimulationRingBuffer::read_interpolated(std::span dst_left, std::span dst_right, diff --git a/src/solver.cpp b/src/solver.cpp index 2ef6c7b..d765516 100644 --- a/src/solver.cpp +++ b/src/solver.cpp @@ -26,6 +26,7 @@ void Solver::step(float dt,bool profile_sample) { if (!(sub_dt > 0) || ++count > 1000000) throw std::runtime_error("solver timestep failed to progress"); ++substep_count_; + for (Component *component : components_) component->prepare_step(sub_dt); { ProfileScope measured(profiler_,profile_sample,ProfileStage::Boundary);boundaries_.resolve(sub_dt); } { ProfileScope measured(profiler_,profile_sample,ProfileStage::Pipe); for (auto &pipe : pipes_) pipe.advance(sub_dt); } diff --git a/src/speed_density_example.cpp b/src/speed_density_example.cpp new file mode 100644 index 0000000..924f64d --- /dev/null +++ b/src/speed_density_example.cpp @@ -0,0 +1,30 @@ +#include "FluidSim/Machine.hpp" +#include + +namespace fluidsim::port { +MachineDocument machine_speed_density_example(){ + auto doc=machine_sensor_engine_example(true);doc.name="Sensor speed-density EFI bench"; + std::uint32_t ecu=0,host=0; + for(auto& part:doc.parts){ + if(part.kind==MachinePartKind::Ecu){ecu=part.id;part.name="Speed-density ECU";part.sensor_ecu.strategy=SensorEcuConfig::Strategy::SpeedDensity;} + if(part.injector)host=part.id; + } + // This is a declared calibration set when constructing the example, not + // runtime access to measured/trapped cylinder charge. + for(const auto& part:doc.parts)if(part.kind==MachinePartKind::Cylinder)doc.find(ecu)->sensor_ecu.displacement=pi*.25f*part.cylinder.bore*part.cylinder.bore*part.cylinder.stroke; + doc.find(host)->injector->ecu_label=doc.find(ecu)->name; + std::vector removed; + for(const auto& node:doc.logic.nodes)if(node.kind==LogicNodeKind::Sensor&&node.sensor_kind==MachineSensorKind::ThrottlePosition)removed.push_back(node.id); + for(const auto id:removed)doc.logic.remove(id); + removed.clear();for(const auto& sensor:doc.sensors)if(sensor.kind==MachineSensorKind::ThrottlePosition)removed.push_back(sensor.id); + for(const auto id:removed)doc.remove_sensor(id); + std::uint32_t connector=0; + for(auto& node:doc.logic.nodes)if(node.kind==LogicNodeKind::Ecu){connector=node.id;node.ecu_strategy=SensorEcuConfig::Strategy::SpeedDensity;node.binding_label=doc.find(ecu)->name;node.position={340,80};} + const auto map=doc.add_sensor(MachineSensorKind::Pressure,host).id,iat=doc.add_sensor(MachineSensorKind::GasTemperature,host).id; + doc.find_sensor(map)->name="Intake MAP / absolute";doc.find_sensor(iat)->name="Intake gas temperature / IAT"; + const auto pressure=doc.logic.add(LogicNodeKind::Sensor,{0,240}).id;doc.logic.find(pressure)->sensor=map;doc.logic.find(pressure)->sensor_kind=MachineSensorKind::Pressure;doc.logic.find(pressure)->name="Measured manifold pressure"; + const auto temperature=doc.logic.add(LogicNodeKind::Sensor,{0,460}).id;doc.logic.find(temperature)->sensor=iat;doc.logic.find(temperature)->sensor_kind=MachineSensorKind::GasTemperature;doc.logic.find(temperature)->name="Measured intake temperature"; + doc.logic.connect({pressure,"value"},{connector,"map"});doc.logic.connect({temperature,"value"},{connector,"iat"}); + validate_machine(doc);return doc; +} +} diff --git a/src/window_presentation.cpp b/src/window_presentation.cpp new file mode 100644 index 0000000..3692626 --- /dev/null +++ b/src/window_presentation.cpp @@ -0,0 +1,28 @@ +#include "FluidSim/Desktop/WindowPresentation.hpp" +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + +namespace fluidsim::port::desktop { +void maximize_window(sf::RenderWindow& window){ + if(!window.isOpen())return; +#ifdef _WIN32 + ShowWindow(window.getNativeHandle(),SW_MAXIMIZE); +#else + // SFML has no portable maximize operation. Keep other desktop builds usable. + window.setPosition({0,0});window.setSize(sf::VideoMode::getDesktopMode().size); +#endif +} +bool window_is_maximized(const sf::RenderWindow& window){ + if(!window.isOpen())return false; +#ifdef _WIN32 + return IsZoomed(window.getNativeHandle())!=FALSE; +#else + return window.getSize()==sf::VideoMode::getDesktopMode().size; +#endif +} +} diff --git a/tests/MachineTestControls.hpp b/tests/MachineTestControls.hpp new file mode 100644 index 0000000..12f2158 --- /dev/null +++ b/tests/MachineTestControls.hpp @@ -0,0 +1,13 @@ +#pragma once +#include "FluidSim/Machine.hpp" +#include +namespace machine_test { +inline void target_speed(fluidsim::port::MachineDocument& d,float rpm){using namespace fluidsim::port;for(auto& part:d.parts)if(part.kind==MachinePartKind::DriveMotor&&machine_shaft_for(d,part.id)==machine_primary_shaft(d)){part.drive.target_speed=rpm*2*pi/60;return;}throw std::runtime_error("Test bench has no speed drive");} +inline void free(fluidsim::port::MachineDocument& d,float rpm){using namespace fluidsim::port;const auto shaft=machine_primary_shaft(d);d.find(shaft)->shaft.initial_speed=rpm*2*pi/60;for(auto& p:d.parts)if(p.kind==MachinePartKind::DriveMotor&&machine_shaft_for(d,p.id)==shaft)p.drive.enabled=false;} +inline void drive(fluidsim::port::MachineDocument& d,float rpm){ + using namespace fluidsim::port;const auto shaft=machine_primary_shaft(d);if(!shaft)throw std::runtime_error("Test bench needs a primary shaft"); + d.find(shaft)->shaft.initial_speed=rpm*2*pi/60; + for(auto& part:d.parts)if(part.kind==MachinePartKind::DriveMotor&&machine_shaft_for(d,part.id)==shaft){part.drive.enabled=true;part.drive.speed_control=true;part.drive.target_speed=rpm*2*pi/60;return;} + auto& motor=d.add(MachinePartKind::DriveMotor,machine_math::add(d.find(shaft)->position,{-.3f,0,0}));const auto id=motor.id;motor.name="Test speed drive";motor.drive.target_speed=rpm*2*pi/60;d.connect({shaft,0},{id,0}); +} +} diff --git a/tests/junction_tests.cpp b/tests/junction_tests.cpp index 4c1cd73..1f4ffdb 100644 --- a/tests/junction_tests.cpp +++ b/tests/junction_tests.cpp @@ -7,7 +7,7 @@ using namespace fluidsim::port; namespace { void check(bool condition,const char* message) { if(!condition) throw std::runtime_error(message); } struct Outcome { std::array mass{}, pressure{}; std::uint64_t fallbacks{}; }; -Outcome flow(bool reverse,bool rotate,float loss,bool mixed=false,bool swap_donors=false) { +Outcome flow(bool reverse,bool rotate,float loss,bool mixed=false,bool swap_donors=false,bool legacy=false) { std::vector pipes; for(int j=0;j<3;++j) { pipes.emplace_back(.001f*(mixed?j+1:1),.2f,8); @@ -16,19 +16,21 @@ Outcome flow(bool reverse,bool rotate,float loss,bool mixed=false,bool swap_dono const double p=mixed?(j==0?120000:101325):101325; const double u=(swap_donors?(j==0?0:-50):(j==0?70:0))*(reverse?-1:1); cell={float(rho),float(rho*u),float(p/pipe_gm1+.5*rho*u*u),j==0?.7f:1.f,j==0?.1f:0.f}; + cell.fuel_equivalent=j==0?.15f:0.f; } } BoundarySystem boundaries(pipes); std::array,3> axes{{{-1,0,0},{1,0,0},{0,1,0}}}; if(rotate) for(auto& axis:axes) axis={-axis[1],axis[0],axis[2]}; - boundaries.add_junction({{0,reverse,axes[0]},{1,!reverse,axes[1]},{2,!reverse,axes[2]}},BoundarySystem::JunctionMethod::Characteristic,loss); + boundaries.add_junction({{0,reverse,axes[0]},{1,!reverse,axes[1]},{2,!reverse,axes[2]}},legacy?BoundarySystem::JunctionMethod::LegacyRusanov:BoundarySystem::JunctionMethod::Characteristic,loss); boundaries.add_closed_end(0,!reverse);boundaries.add_closed_end(1,reverse);boundaries.add_closed_end(2,reverse); const auto totals=[&] { - std::array t{}; + std::array t{}; for(auto& p:pipes) for(int i=0;i pipes;for(int j=0;j<2;++j) pipes.emplace_back(.001f,.1f,4); for(auto& cell:pipes[0].cells) { const float u=500;cell.momentum=cell.rho*u;cell.energy+=.5f*cell.rho*u*u; } diff --git a/tests/logic_editor_tests.cpp b/tests/logic_editor_tests.cpp new file mode 100644 index 0000000..c6faa08 --- /dev/null +++ b/tests/logic_editor_tests.cpp @@ -0,0 +1,86 @@ +#include "FluidSim/Desktop/LogicEditor.hpp" +#include "FluidSim/Desktop/PortAppearance.hpp" +#include +#include +#include +#include +using namespace fluidsim::port; +using namespace fluidsim::port::desktop; +namespace { +void check(bool condition,const char* why){if(!condition)throw std::runtime_error(why);} +bool close(double a,double b,double tolerance=1e-5){return std::abs(a-b) simulation;MachineSnapshot live; + sf::Vector2u size{1920,1080};WorkshopLayout ui=WorkshopLayout::calculate(size);sf::FloatRect canvas=ui.viewport; + std::vector history;std::string status;std::uint32_t physical{},focused{};bool shift{}; + LogicEditorHooks hooks(){return {doc,simulation?&live:nullptr,shift, + [this](const std::function& action){const auto before=doc;try{action();validate_machine(doc);if(simulation)simulation->apply_tuning(doc);history.push_back(before);return true;}catch(const std::exception& e){doc=before;status=e.what();return false;}}, + [this](std::uint32_t id,bool value){simulation->set_logic_control(id,value);},[this](std::uint32_t id,double value){simulation->set_logic_control(id,value);},[this]{if(simulation)simulation->release_logic_buttons();}, + [this](std::uint32_t id){physical=id;},[this](std::uint32_t id){focused=id;},[this](const std::string& text){status=text;},[this]{load(machine_logic_example());}};} + void load(MachineDocument d){simulation.reset();doc=std::move(d);editor.reset_document();editor.fit(doc.logic,canvas);history.clear();} + void send(sf::Event e){auto h=hooks();editor.event(e,h,ui,canvas,true);} + void key(sf::Keyboard::Key code,bool control=false){send(sf::Event::KeyPressed{.code=code,.control=control});} + void text(const std::string& value){for(const auto c:value)send(sf::Event::TextEntered{std::uint32_t(c)});} + void press(sf::Vector2f point,sf::Mouse::Button button=sf::Mouse::Button::Left){send(sf::Event::MouseButtonPressed{button,{int(std::round(point.x)),int(std::round(point.y))}});} + void release(sf::Vector2f point,sf::Mouse::Button button=sf::Mouse::Button::Left){send(sf::Event::MouseButtonReleased{button,{int(std::round(point.x)),int(std::round(point.y))}});} + void click(sf::Vector2f point,sf::Mouse::Button button=sf::Mouse::Button::Left){press(point,button);release(point,button);} + void drag(sf::Vector2f from,sf::Vector2f to){press(from);send(sf::Event::MouseMoved{{int(std::round(to.x)),int(std::round(to.y))}});release(to);} + sf::Vector2f pin(std::uint32_t id,const char* key){return editor.pin_position(*doc.logic.find(id),key,canvas);} + sf::Vector2f control(std::uint32_t id,float fraction=.5f){const auto r=editor.node_rect(*doc.logic.find(id),canvas);const float zoom=r.size.x/240;return {r.position.x+(12+216*fraction)*zoom,r.position.y+r.size.y-26*zoom};} + void start(){simulation=std::make_unique(doc);step();} + void step(int count=1){for(int i=0;iprocess();live=simulation->snapshot();} + SignalValue value(std::uint32_t id,const char* key){const auto ports=logic_node_ports(*doc.logic.find(id));for(const auto& n:live.logic)if(n.id==id)for(std::size_t i=0;ikind==LogicNodeKind::Ecu&&f.doc.logic.find(pasted)->part==0,"Copied ECU connector stole existing ownership"); + } + { + f.load({});f.key(sf::Keyboard::Key::F,true);f.text("Hysteresis");f.key(sf::Keyboard::Key::Enter);f.click({80,244});const auto point=f.canvas.position+sf::Vector2f{120,140};f.click(point);check(f.doc.logic.nodes.size()==1&&f.doc.logic.nodes[0].kind==LogicNodeKind::Hysteresis,"Searchable node library did not create the chosen kind");check(f.history.size()==1,"Node creation did not enter undo history"); + } + MachineDocument wired;const auto a=wired.logic.add(LogicNodeKind::Switch,{0,0}).id,b=wired.logic.add(LogicNodeKind::Switch,{0,250}).id,gate=wired.logic.add(LogicNodeKind::And,{420,80}).id,number=wired.logic.add(LogicNodeKind::NumberConstant,{420,360}).id;f.load(wired); + f.click(f.pin(a,"out"));f.click(f.pin(gate,"in1"));check(f.doc.logic.links.size()==1,"Click-to-connect failed");f.drag(f.pin(b,"out"),f.pin(gate,"in2"));check(f.doc.logic.links.size()==2,"Drag-to-connect failed"); + const auto before=f.doc.logic.links;f.click(f.pin(number,"out"));f.click(f.pin(gate,"in1"));check(f.doc.logic.links==before&&!f.status.empty(),"Incompatible preview/edit changed existing wiring");f.key(sf::Keyboard::Key::Escape); + f.drag(f.pin(gate,"in2"),f.pin(a,"out"));check(f.doc.logic.links.size()==2&&f.doc.logic.links.back().from.node==a,"Rewiring a connected input retained its old driver or rejected fan-out"); + const auto rewired=f.doc.logic.links;f.drag(f.pin(gate,"in2"),f.pin(number,"out"));check(f.doc.logic.links==rewired,"Rejected rewire disconnected the original signal");f.key(sf::Keyboard::Key::Escape);f.click(f.pin(gate,"in1"),sf::Mouse::Button::Right);check(f.doc.logic.links.size()==1,"Right-click input did not disconnect its driver"); + { + const auto old=f.doc.logic.find(a)->position;const auto r=f.editor.node_rect(*f.doc.logic.find(a),f.canvas);const auto from=r.position+sf::Vector2f{30,12},to=from+sf::Vector2f{80,25};const auto p=f.editor.unproject(from,f.canvas),q=f.editor.unproject(to,f.canvas);const auto count=f.history.size();f.drag(from,to);check(close(f.doc.logic.find(a)->position.x-old.x,q.x-p.x,2)&&close(f.doc.logic.find(a)->position.y-old.y,q.y-p.y,2),"Header drag did not move the selected graph node");check(f.history.size()==count+1,"A node drag created more than one undo entry"); + const auto anchor=f.canvas.position+f.canvas.size*.4f;const auto original=f.editor.unproject(anchor,f.canvas);f.send(sf::Event::MouseWheelScrolled{sf::Mouse::Wheel::Vertical,2,{int(anchor.x),int(anchor.y)}});const auto after=f.editor.unproject(anchor,f.canvas);check(close(original.x,after.x,1)&&close(original.y,after.y,1),"Wheel zoom did not preserve the point beneath the pointer"); + } + { + MachineDocument doc;const auto button=doc.logic.add(LogicNodeKind::Button,{0,0}).id,lamp=doc.logic.add(LogicNodeKind::Lamp,{420,0}).id;doc.logic.connect({button,"out"},{lamp,"in"});f.load(doc);f.start();f.press(f.control(button));f.step();check(std::get(f.value(lamp,"in").value),"Live momentary control did not reach the graph");f.send(sf::Event::FocusLost{});f.step();check(!std::get(f.value(lamp,"in").value),"Focus loss left a momentary button true");f.press(f.control(button));auto h=f.hooks();f.editor.deactivate(h);f.step();check(!std::get(f.value(lamp,"in").value),"Closing the graph left a held button active"); + const auto nodes=f.doc.logic.nodes.size();f.key(sf::Keyboard::Key::Delete);check(f.doc.logic.nodes.size()==nodes,"Test allowed node deletion"); + } + { + MachineDocument doc;const auto toggle=doc.logic.add(LogicNodeKind::Switch,{0,0}).id,lamp=doc.logic.add(LogicNodeKind::Lamp,{420,0}).id;doc.logic.connect({toggle,"out"},{lamp,"in"});f.load(doc);f.start();const auto time=f.live.time;f.click(f.control(toggle));f.click(f.control(toggle));check(f.live.time==time,"Editing controls advanced a paused simulation");f.step();check(!std::get(f.value(lamp,"in").value)&&!f.doc.logic.find(toggle)->boolean,"Two queued toggles collapsed or changed configured initial state"); + } + { + MachineDocument doc;const auto slider=doc.logic.add(LogicNodeKind::Slider,{0,0}).id,readout=doc.logic.add(LogicNodeKind::Readout,{420,0}).id;doc.logic.connect({slider,"out"},{readout,"in"});f.load(doc);f.start();f.click(f.control(slider,.75f));f.step();check(close(std::get(f.value(readout,"in").value),.75,.01)&&f.doc.logic.find(slider)->value==0,"Live slider failed or changed its initial value"); + } + { + MachineDocument doc;const auto constant=doc.logic.add(LogicNodeKind::NumberConstant,{0,0}).id,readout=doc.logic.add(LogicNodeKind::Readout,{420,0}).id;doc.logic.connect({constant,"out"},{readout,"in"});f.load(doc);f.start();f.editor.select_node(constant,f.doc);const auto time=f.live.time;f.click({f.ui.inspector_x+40,342});f.text("-3.25");f.key(sf::Keyboard::Key::Enter);check(f.doc.logic.find(constant)->value==-3.25&&f.live.time==time,"Live constant property edit failed or restarted physics");f.step();check(std::get(f.value(readout,"in").value)==-3.25,"Live constant edit did not reach the next sample");f.click({f.ui.inspector_x+40,342});f.text("1e999");f.key(sf::Keyboard::Key::Enter);check(f.editor.editing_text()&&f.doc.logic.find(constant)->value==-3.25,"Invalid numeric input was lost or applied");f.key(sf::Keyboard::Key::Escape); + } + { + MachineDocument doc;const auto host=doc.add(MachinePartKind::Chamber).id,sensor=doc.add_sensor(MachineSensorKind::Pressure,host).id,reference=doc.logic.add(LogicNodeKind::Sensor,{0,0}).id;f.load(doc);f.editor.select_node(reference,f.doc);f.click({f.ui.inspector_x+60,208});check(f.doc.logic.find(reference)->sensor==sensor&&f.doc.sensors.size()==1&&f.doc.parts.size()==1,"Sensor reference created a duplicate physical sensor or failed to bind");f.click({f.ui.inspector_x+60,f.ui.height-92});check(f.focused==host,"Focus in 3D did not resolve the shared physical host"); + f.key(sf::Keyboard::Key::C,true);f.key(sf::Keyboard::Key::V,true);check(f.doc.logic.nodes.size()==2&&f.doc.logic.nodes.back().sensor==sensor,"Copied sensor node did not reference the same sensor entity"); + } + { + MachineDocument destination;const auto part=destination.add(MachinePartKind::Chamber).id;destination.add_sensor(MachineSensorKind::GasTemperature,part);f.load(destination);f.key(sf::Keyboard::Key::V,true);check(f.doc.logic.nodes.size()==1&&f.doc.logic.nodes[0].sensor==0&&!f.doc.logic.nodes[0].binding_label.empty(),"Cross-document graph paste silently matched a coincidental sensor ID or lost its intended binding"); + } + if(argc==3&&std::string(argv[1])=="--ui-preview"){ + const std::filesystem::path output=argv[2];std::filesystem::create_directories(output);sf::ContextSettings settings;settings.depthBits=24;settings.majorVersion=2;settings.minorVersion=1;sf::RenderWindow window(sf::VideoMode({1920,1080}),"Logic editor review",sf::Style::Default,sf::State::Windowed,settings);window.setVisible(false);sf::Font font("C:/Windows/Fonts/consola.ttf"); + {MachineDocument shapes;const auto source=shapes.logic.add(LogicNodeKind::Switch,{0,0}).id,input=shapes.logic.add(LogicNodeKind::Lamp,{420,0}).id,number=shapes.logic.add(LogicNodeKind::NumberConstant,{0,250}).id,readout=shapes.logic.add(LogicNodeKind::Readout,{420,250}).id;shapes.logic.connect({source,"out"},{input,"in"});shapes.logic.connect({number,"out"},{readout,"in"});f.load(shapes);window.clear();auto h=f.hooks();f.editor.draw(window,&font,h,f.ui,f.canvas,true);sf::Texture pixels(f.size);pixels.update(window);const auto image=pixels.copyToImage();const auto sample=[&](std::uint32_t id,const char* key){const auto at=f.pin(id,key);return image.getPixel({unsigned(std::round(at.x)),unsigned(std::round(at.y))});};const auto red=sample(source,"out"),green=sample(number,"out"),ring=sample(input,"in");check(red.r>150&&red.g<150&&green.g>150&&green.r<150,"Rendered signal output circles do not use the declared Boolean/Number colors");check(ring.r<50&&ring.g<50&&ring.b<50,"Rendered input pin has a filled center instead of a ring");} + for(const auto resolution:{sf::Vector2u{1920,1080},sf::Vector2u{1280,720}}){f.size=resolution;f.ui=WorkshopLayout::calculate(resolution);f.canvas=f.ui.viewport;window.setSize(resolution);f.load(machine_logic_example());auto capture=[&](const std::string& name){window.clear();auto h=f.hooks();f.editor.draw(window,&font,h,f.ui,f.canvas,true);sf::Texture pixels(resolution);pixels.update(window);check(pixels.copyToImage().saveToFile(output/(std::to_string(resolution.x)+"-"+name+".png")),"Logic screenshot save failed");};capture("graph-build");f.start();for(int frame=0;frame<10;++frame){f.step(1200);auto h=f.hooks();f.editor.draw(window,&font,h,f.ui,f.canvas,true);}capture("graph-live");for(const auto& n:f.doc.logic.nodes)if(n.kind==LogicNodeKind::Hysteresis){f.editor.reveal(n.id,f.doc,f.canvas);capture("threshold-inspector");break;}} + } + std::cout<<"Logic canvas placement, typed wiring/rewiring, undo, controls, focus release, binding and live inspector passed\n";return 0; +}catch(const std::exception& e){std::cerr< +#include +#include +#include +#include +#include +#include +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-9*std::max({1.,std::abs(a),std::abs(b)});} +templatevoid rejects(F&& f,const char* why){bool rejected=false;try{f();}catch(const std::exception&){rejected=true;}check(rejected,why);} +double number(const MachineLogicRuntime& r,std::uint32_t n,const char* port="out"){const auto value=r.value({n,port});check(value.validity==SignalValidity::Valid,"Numeric output unexpectedly invalid");return std::get(value.value);} +bool bit(const MachineLogicRuntime& r,std::uint32_t n,const char* port="out"){const auto value=r.value({n,port});check(value.validity==SignalValidity::Valid,"Boolean output unexpectedly invalid");return std::get(value.value);} +std::uint32_t add(LogicGraph& g,LogicNodeKind kind){return g.add(kind).id;} +} +int main()try{ + for(int i=0;iboolean=(mask&1)!=0;g.find(b)->boolean=(mask&2)!=0;g.connect({gate,"in1"},{a,"out"});g.connect({b,"out"},{gate,"in2"});MachineLogicRuntime runtime(doc);runtime.evaluate(0,.001); + bool expected=false;if(kind==LogicNodeKind::And)expected=mask==3;else if(kind==LogicNodeKind::Or)expected=mask!=0;else if(kind==LogicNodeKind::Xor)expected=mask==1||mask==2;else if(kind==LogicNodeKind::Nand)expected=mask!=3;else expected=mask==0; + check(bit(runtime,gate)==expected,"Boolean gate truth table is wrong");check(runtime.value({gate,"out"}).timestamp==0,"Logic output has the wrong sample timestamp"); + } + { + MachineDocument doc;auto& g=doc.logic;const auto gate=add(g,LogicNodeKind::And),source=add(g,LogicNodeKind::Switch),negated=add(g,LogicNodeKind::Not);g.find(gate)->input_count=3; + g.connect({source,"out"},{gate,"in1"});g.connect({source,"out"},{gate,"in2"});g.connect({source,"out"},{gate,"in3"});g.connect({gate,"out"},{negated,"in"});MachineLogicRuntime runtime(doc);runtime.evaluate(0,.01);check(bit(runtime,negated),"NOT / fan-out failed");runtime.set_manual(source,true);runtime.evaluate(.01,.01);check(bit(runtime,gate)&&!bit(runtime,negated),"Three-input AND did not use all inputs"); + const auto before=g.links.size();check(!logic_connection_preview(g,{source,"out"},{gate,"in1"}).allowed&&g.links.size()==before,"Connection preview mutated the graph or accepted two drivers");rejects([&]{g.connect({source,"out"},{gate,"out"});},"Output-to-output connection was accepted");rejects([&]{g.connect({source,"missing"},{negated,"in"});},"Unknown stable pin was accepted"); + auto broken=g;broken.disconnect({gate,"in3"});validate_logic_graph(broken);auto missing=doc;missing.logic=broken;rejects([&]{MachineLogicRuntime invalid(missing);},"Required unwired input did not block compilation"); + rejects([&]{runtime.set_manual(source,1.);},"Boolean control accepted an implicit number cast"); + } + { + MachineDocument doc;auto& g=doc.logic;const auto a=add(g,LogicNodeKind::Not),b=add(g,LogicNodeKind::Not);g.find(a)->name="First NOT";g.find(b)->name="Second NOT";g.connect({a,"out"},{b,"in"});std::string error;try{g.connect({b,"out"},{a,"in"});}catch(const std::exception& e){error=e.what();}check(g.links.size()==1&&error.find("First NOT")!=std::string::npos&&error.find("Second NOT")!=std::string::npos,"Cycle rejection omitted its path or changed the draft"); + const auto delay=add(g,LogicNodeKind::BooleanDelay);g.connect({b,"out"},{delay,"in"});g.connect({delay,"out"},{a,"in"});MachineLogicRuntime feedback(doc);feedback.evaluate(0,.001);check(!bit(feedback,delay)&&bit(feedback,a)&&!bit(feedback,b),"Memory output was not initialized before combinational evaluation");feedback.evaluate(.001,.001);check(!bit(feedback,delay),"Delay did not commit stored state simultaneously"); + auto cyclic=g;cyclic.disconnect({a,"in"});cyclic.links.push_back({{b,"out"},{a,"in"}});std::vector path;rejects([&]{logic_evaluation_order(cyclic,&path);},"Cycle path query accepted a combinational cycle");check(path.size()==3&&path.front()==path.back()&&std::find(path.begin(),path.end(),a)!=path.end()&&std::find(path.begin(),path.end(),b)!=path.end(),"Cycle feedback did not identify the actual nodes");logic_evaluation_order(g,&path);check(path.empty(),"A valid graph retained a stale cycle highlight"); + } + { + MachineDocument doc;auto& g=doc.logic;const auto delay=add(g,LogicNodeKind::NumberDelay),sum=add(g,LogicNodeKind::Add),one=add(g,LogicNodeKind::NumberConstant);g.find(one)->value=1;g.connect({delay,"out"},{sum,"a"});g.connect({one,"out"},{sum,"b"});g.connect({sum,"out"},{delay,"in"});MachineLogicRuntime feedback(doc); + for(int i=0;i<8;++i){feedback.evaluate(i*.01,.01);check(number(feedback,delay)==i&&number(feedback,sum)==i+1,"Delayed numeric recurrence depends on document order or leaks next state");} + } + { + MachineDocument doc;auto& g=doc.logic;const auto button=add(g,LogicNodeKind::Button),lamp=add(g,LogicNodeKind::Lamp),slider=add(g,LogicNodeKind::Slider);g.connect({button,"out"},{lamp,"in"});MachineLogicRuntime controls(doc);controls.set_manual(button,true);controls.set_manual(button,false);controls.set_manual(button,true);controls.set_manual(button,false); + for(int i=0;i<4;++i){controls.evaluate(i*.001,.001);check(bit(controls,lamp,"in")==!(i%2),"Short button events collapsed or remained pressed");} + controls.set_manual(button,true);controls.release_buttons();controls.evaluate(.004,.001);check(!bit(controls,button),"Focus-loss release left a queued button press");controls.set_manual(slider,.75);controls.evaluate(.005,.001);check(number(controls,slider)==.75,"Numeric manual control event was lost");rejects([&]{controls.set_manual(slider,1.5);},"Out-of-range slider control was accepted");rejects([&]{controls.set_manual(slider,true);},"Numeric control accepted Boolean coercion"); + for(int i=0;i<10000;++i)controls.set_manual(slider,double(i%1000)/1000);controls.evaluate(.006,.001);check(close(number(controls,slider),.999),"Paused slider motion overflowed its event queue or lost the latest value"); + MachineLogicRuntime fresh(doc);fresh.evaluate(0,.001);check(!bit(fresh,button)&&number(fresh,slider)==0,"Fresh Test inherited transient controls"); + } + { + MachineDocument doc;auto& g=doc.logic;const auto a=add(g,LogicNodeKind::NumberConstant),b=add(g,LogicNodeKind::NumberConstant);g.find(a)->value=7;g.find(b)->value=3; + std::vector> expected; + for(const auto [kind,value]:{std::pair{LogicNodeKind::Add,10.},std::pair{LogicNodeKind::Subtract,4.},std::pair{LogicNodeKind::Multiply,21.},std::pair{LogicNodeKind::Divide,7./3},std::pair{LogicNodeKind::Minimum,3.},std::pair{LogicNodeKind::Maximum,7.}}){const auto n=add(g,kind);g.connect({a,"out"},{n,"a"});g.connect({b,"out"},{n,"b"});expected.emplace_back(n,value);} + const auto absolute=add(g,LogicNodeKind::Absolute),clamp=add(g,LogicNodeKind::Clamp),gain=add(g,LogicNodeKind::GainOffset),map=add(g,LogicNodeKind::MapRange);g.find(clamp)->low=2;g.find(clamp)->high=5;g.find(gain)->gain=2;g.find(gain)->offset=1;g.find(map)->input_low=0;g.find(map)->input_high=10;g.find(map)->output_low=0;g.find(map)->output_high=100; + for(auto id:{absolute,clamp,gain,map})g.connect({a,"out"},{id,"in"});expected.insert(expected.end(),{{absolute,7},{clamp,5},{gain,15},{map,70}}); + const auto greater=add(g,LogicNodeKind::Greater),less=add(g,LogicNodeKind::Less),equal=add(g,LogicNodeKind::Equal),window=add(g,LogicNodeKind::Window);for(auto id:{greater,less,equal}){g.connect({a,"out"},{id,"a"});g.connect({b,"out"},{id,"b"});}g.find(window)->low=2;g.find(window)->high=8;g.connect({a,"out"},{window,"in"}); + MachineLogicRuntime runtime(doc);runtime.evaluate(0,.001);for(const auto& [n,value]:expected)check(close(number(runtime,n),value),"Numeric arithmetic or mapping is incorrect");check(bit(runtime,greater)&&!bit(runtime,less)&&!bit(runtime,equal)&&bit(runtime,window),"Numeric comparisons are incorrect"); + g.find(b)->value=0;runtime.apply_tuning(doc);runtime.evaluate(.001,.001);const auto divider=expected[3].first;check(runtime.value({divider,"out"}).validity==SignalValidity::Fault,"Division by zero silently produced an ordinary number");bool diagnosed=false;for(const auto& n:runtime.snapshot())if(n.id==divider)diagnosed=n.problem=="Division by zero";check(diagnosed,"Invalid math omitted its runtime diagnostic");g.find(b)->value=7+1e-7;runtime.apply_tuning(doc);runtime.evaluate(.002,.001);check(bit(runtime,equal),"Equal tolerance was ignored"); + g.find(a)->value=std::numeric_limits::max();g.find(gain)->gain=2;runtime.apply_tuning(doc);runtime.evaluate(.003,.001);check(runtime.value({gain,"out"}).validity==SignalValidity::Fault,"Arithmetic overflow was valid"); + } + { + MachineDocument doc;auto& g=doc.logic;const auto n=add(g,LogicNodeKind::NumberConstant),threshold=add(g,LogicNodeKind::Hysteresis);g.find(threshold)->low=3;g.find(threshold)->high=7;g.connect({n,"out"},{threshold,"in"});MachineLogicRuntime runtime(doc);int tick=0; + for(const auto [input,expected]:{std::pair{5.,false},std::pair{8.,true},std::pair{5.,true},std::pair{2.,false}}){g.find(n)->value=input;runtime.apply_tuning(doc);runtime.evaluate(tick++*.001,.001);check(bit(runtime,threshold)==expected,"Hysteresis did not preserve its state in the deadband");} + } + { + MachineDocument doc;auto& g=doc.logic;const auto good=add(g,LogicNodeKind::NumberConstant),zero=add(g,LogicNodeKind::NumberConstant),bad=add(g,LogicNodeKind::Divide),select=add(g,LogicNodeKind::Select),condition=add(g,LogicNodeKind::BooleanConstant);g.find(good)->value=42; + g.connect({good,"out"},{bad,"a"});g.connect({zero,"out"},{bad,"b"});g.connect({condition,"out"},{select,"select"});g.connect({good,"out"},{select,"a"});g.connect({bad,"out"},{select,"b"});MachineLogicRuntime runtime(doc);runtime.evaluate(0,.001);check(number(runtime,select)==42,"Invalid unselected branch poisoned a valid selected value");for(const auto& state:runtime.snapshot())if(state.id==select)check(state.problem.empty(),"Selector reported an inactive-branch fault as an output fault"); + } + { + MachineDocument doc;auto& g=doc.logic;const auto p=add(g,LogicNodeKind::NumberConstant),q=add(g,LogicNodeKind::NumberConstant),difference=add(g,LogicNodeKind::Subtract),target=add(g,LogicNodeKind::Readout);for(auto id:{p,q,difference}){g.find(id)->quantity=SignalQuantity::Pressure;g.find(id)->pressure_reference=PressureReference::Absolute;}g.find(p)->value=150000;g.find(q)->value=101325;g.find(target)->quantity=SignalQuantity::Pressure;g.find(target)->pressure_reference=PressureReference::Differential; + g.connect({p,"out"},{difference,"a"});g.connect({q,"out"},{difference,"b"});g.connect({difference,"out"},{target,"in"});check(!logic_connection_preview(g,{p,"out"},{target,"in"}).allowed,"Absolute and differential pressure implicitly mixed");MachineLogicRuntime runtime(doc);runtime.evaluate(0,.01);check(number(runtime,target,"in")==48675,"Pressure subtraction did not produce a differential quantity"); + const auto speed=add(g,LogicNodeKind::Readout);g.find(speed)->quantity=SignalQuantity::AngularSpeed;check(!logic_connection_preview(g,{p,"out"},{speed,"in"}).allowed,"Pressure connected to angular speed"); + } + { + MachineDocument doc;const auto host=doc.add(MachinePartKind::Chamber).id,sensor=doc.add_sensor(MachineSensorKind::Pressure,host).id;auto& g=doc.logic;const auto source=add(g,LogicNodeKind::Sensor),threshold=add(g,LogicNodeKind::Hysteresis),choose=add(g,LogicNodeKind::Select),convert=add(g,LogicNodeKind::BooleanToNumber),a=add(g,LogicNodeKind::NumberConstant),b=add(g,LogicNodeKind::NumberConstant); + g.find(source)->sensor=sensor;g.find(threshold)->quantity=SignalQuantity::Pressure;g.find(threshold)->pressure_reference=PressureReference::Absolute;g.find(threshold)->low=100000;g.find(threshold)->high=130000;g.find(a)->value=1;g.find(b)->value=9;g.find(convert)->false_value=-1;g.find(convert)->true_value=2; + g.connect({source,"value"},{threshold,"in"});g.connect({threshold,"out"},{choose,"select"});g.connect({a,"out"},{choose,"a"});g.connect({b,"out"},{choose,"b"});g.connect({threshold,"out"},{convert,"in"}); + double time=0;SignalValue reading=SignalValue::number(140000);MachineLogicRuntime runtime(doc,[&](std::uint32_t id){MachineSensorState state;state.id=id;state.count=1;state.outputs[0]=reading;return state;});runtime.evaluate(time,.01);check(number(runtime,choose)==9&&number(runtime,convert)==2,"Typed number selection/conversion failed"); + time=.01;reading=SignalValue::number(0,time);reading.validity=SignalValidity::Missing;runtime.evaluate(time,.01);check(runtime.value({threshold,"out"}).validity==SignalValidity::Missing&&runtime.value({choose,"out"}).validity==SignalValidity::Missing,"Missing sensor was converted to ordinary false or zero"); + time=.02;reading=SignalValue::boolean(true,time);runtime.evaluate(time,.01);check(runtime.value({source,"value"}).validity==SignalValidity::Fault,"Sensor supplied the wrong runtime variant without a fault"); + const auto file=std::filesystem::temp_directory_path()/("fluidsim-logic-"+std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())+".machine.toml");save_machine(doc,file);const auto loaded=load_machine(file);check(loaded.logic==doc.logic,"Logic types, keys, parameters or layout did not round-trip"); + {std::ofstream out(file);out<<"format=8\n[[logic.nodes]]\nid=1\nkind='unknown_future_gate'\n";}rejects([&]{load_machine(file);},"Unknown saved logic kind was silently accepted"); + {std::ofstream out(file);out<<"format=8\n[[logic.nodes]]\nid=1\nkind='switch'\nboolean=1\n";}rejects([&]{load_machine(file);},"Saved Boolean control silently accepted a number");std::filesystem::remove(file); + } + std::cout<<"Typed graph ordering, gates, arithmetic, validity, manual events, pressure references and explicit delayed feedback passed\n";return 0; +}catch(const std::exception& e){std::cerr< +#include +#include +#include +#include +#include +#include +using namespace fluidsim::port; +using namespace fluidsim::port::desktop; +using namespace fluidsim::port::machine_math; +void check(bool condition,const char* why){if(!condition)throw std::runtime_error(why);} +void machine_test_directory(const std::string& value){ +#ifdef _WIN32 + _putenv_s("FLUIDSIM_MACHINE_DIR",value.c_str()); +#else + if(value.empty())unsetenv("FLUIDSIM_MACHINE_DIR");else setenv("FLUIDSIM_MACHINE_DIR",value.c_str(),1); +#endif +} +struct UiPreferenceFixture { + std::filesystem::path path=std::filesystem::temp_directory_path()/("fluidsim-ui-"+std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())+".txt"); + std::string previous; + static void set(const std::string& value){ +#ifdef _WIN32 + _putenv_s("FLUIDSIM_UI_SETTINGS",value.c_str()); +#else + if(value.empty())unsetenv("FLUIDSIM_UI_SETTINGS");else setenv("FLUIDSIM_UI_SETTINGS",value.c_str(),1); +#endif + } + UiPreferenceFixture(){if(const char* old=std::getenv("FLUIDSIM_UI_SETTINGS"))previous=old;set(path.string());} + ~UiPreferenceFixture(){set(previous);std::error_code error;std::filesystem::remove(path,error);} +}; +int main(int argc,char** argv)try{ + UiPreferenceFixture preferences; + check(port_color(PortDomain::Boolean).r>port_color(PortDomain::Boolean).g,"Boolean ports are not red"); + check(port_color(PortDomain::Number).g>port_color(PortDomain::Number).r,"Number ports are not green"); + MachinePortDescriptor input_pin;input_pin.direction=PortDirection::Input;auto output_pin=input_pin;output_pin.direction=PortDirection::Output; + check(!filled_port(input_pin)&&filled_port(output_pin),"Input/output silhouettes do not match ring/filled convention"); + const sf::Vector2u size{1440,900};const auto viewport=MachineBuilder::viewport(size); + if(argc==3&&std::string(argv[1])=="--ui-preview"){ + const std::filesystem::path output=argv[2];std::filesystem::create_directories(output); + sf::ContextSettings context;context.depthBits=24;context.majorVersion=2;context.minorVersion=1; + sf::RenderWindow window(sf::VideoMode({1920,1080}),"Workshop UI review",sf::Style::Default,sf::State::Windowed,context);window.setVisible(false); + sf::Font font("C:/Windows/Fonts/consola.ttf");MachineBuilder preview; + auto click_preview=[&](sf::Vector2f at){const sf::Vector2i pos{int(at.x),int(at.y)};preview.handle_event(sf::Event::MouseButtonPressed{sf::Mouse::Button::Left,pos},window.getSize());preview.handle_event(sf::Event::MouseButtonReleased{sf::Mouse::Button::Left,pos},window.getSize());}; + auto key_preview=[&](sf::Keyboard::Key code){preview.handle_event(sf::Event::KeyPressed{.code=code},window.getSize());}; + auto capture=[&](const std::string& name){preview.draw(window,&font);sf::Texture image(window.getSize());image.update(window);check(image.copyToImage().saveToFile(output/name),"UI screenshot save failed");}; + for(const sf::Vector2u resolution:{sf::Vector2u{1920,1080},sf::Vector2u{1280,720}}){ + window.setSize(resolution);const auto ui=WorkshopLayout::calculate(resolution);const auto prefix=std::to_string(resolution.x)+"-"; + preview.set_document({});capture(prefix+"empty.png"); + auto design=machine_from_engine(scenario_example("flat6"));preview.set_document(design); + click_preview({150,120}); // objects list + const auto ecu=std::find_if(design.parts.begin(),design.parts.end(),[](const auto& p){return p.kind==MachinePartKind::Ecu;}); + if(ecu!=design.parts.end()){ + click_preview({40,165});for(char c:std::string("ECU"))preview.handle_event(sf::Event::TextEntered{std::uint32_t(c)},resolution);key_preview(sf::Keyboard::Key::Enter); + click_preview({85,242});capture(prefix+"ecu.png"); + click_preview({40,165});for(int i=0;i<3;++i)key_preview(sf::Keyboard::Key::Backspace);key_preview(sf::Keyboard::Key::Enter); + } + key_preview(sf::Keyboard::Key::H);capture(prefix+"checks.png"); + key_preview(sf::Keyboard::Key::M);capture(prefix+"meters.png"); + auto pump=machine_pump_example();pump.probes={{1,pump.parts.front().id,"Tank pressure",MachineQuantity::Pressure},{2,0,"Shaft speed",MachineQuantity::Rpm}}; + preview.set_document(pump);preview.start_test();check(preview.testing(),"Preview test did not start");preview.update();capture(prefix+"running.png");preview.pause_test();preview.step_test();key_preview(sf::Keyboard::Key::M);capture(prefix+"paused.png");preview.stop_test(); + click_preview({160,68});capture(prefix+"collapsed.png");click_preview({160,68}); + preview.set_document(load_machine(machine_directory()/"shaft-drive-bench.machine.toml"));click_preview({160,120}); + for(const auto& entry:std::vector>{{"Bench shaft","mechanical-shaft"},{"Flywheel","mechanical-flywheel"},{"Torque motor","mechanical-motor"},{"Adjustable brake","mechanical-brake"}}){ + preview.handle_event(sf::Event::KeyPressed{.code=sf::Keyboard::Key::F,.control=true},resolution);for(char c:entry.first)preview.handle_event(sf::Event::TextEntered{std::uint32_t(c)},resolution);key_preview(sf::Keyboard::Key::Enter);click_preview({85,242});capture(prefix+entry.second+".png"); + } + preview.start_test();preview.pause_test();for(int i=0;i<10;++i)preview.step_test();key_preview(sf::Keyboard::Key::M);capture(prefix+"mechanical-running.png");preview.stop_test(); + auto tapped=machine_pump_example();const auto tank=tapped.parts.front().id;tapped.add_sensor(MachineSensorKind::Pressure,tank);tapped.add_sensor(MachineSensorKind::GasTemperature,tank);tapped.add_sensor(MachineSensorKind::Crank,machine_primary_shaft(tapped)); + const auto delta=tapped.add_sensor(MachineSensorKind::DifferentialPressure,tank).id;for(const auto& p:tapped.parts)if(p.kind==MachinePartKind::Reservoir){tapped.find_sensor(delta)->reference={p.id,.5f,p.name};break;} + preview.set_document(tapped);preview.handle_event(sf::Event::KeyPressed{.code=sf::Keyboard::Key::F,.control=true},resolution);key_preview(sf::Keyboard::Key::Backspace);key_preview(sf::Keyboard::Key::Enter); + const auto marker=builder_project(preview.camera(),preview.workspace_viewport(resolution),*machine_sensor_position(tapped,tapped.sensors[0].target));check(bool(marker),"Sensor preview marker is outside the viewport");click_preview(*marker+sf::Vector2f{0,22});capture(prefix+"sensors-build.png"); + preview.start_test();preview.pause_test();preview.step_test();capture(prefix+"sensors-live.png"); + click_preview({ui.inspector_x+ui.inspector_width-32,286});click_preview({ui.inspector_x+ui.inspector_width-32,286});capture(prefix+"sensors-crank.png"); + click_preview({ui.inspector_x+ui.inspector_width-32,286});capture(prefix+"sensors-differential.png");preview.stop_test(); + preview.set_document(machine_logic_example());preview.handle_event(sf::Event::KeyPressed{.code=sf::Keyboard::Key::F,.control=true},resolution);for(char c:std::string("Bench speed drive"))preview.handle_event(sf::Event::TextEntered{std::uint32_t(c)},resolution);key_preview(sf::Keyboard::Key::Enter);click_preview({85,242});capture(prefix+"logic-owned-drive.png"); + preview.start_test();preview.pause_test();for(int i=0;i<50;++i)preview.step_test();key_preview(sf::Keyboard::Key::M);capture(prefix+"logic-pressure-stop.png");preview.stop_test(); + preview.set_document(machine_logic_example());preview.set_workspace_view(WorkshopView::Logic);capture(prefix+"logic-canvas-build.png");preview.start_test();preview.pause_test();for(int i=0;i<20;++i)preview.step_test();capture(prefix+"logic-canvas-live.png");preview.set_workspace_view(WorkshopView::Split);capture(prefix+"logic-split.png");preview.stop_test();preview.set_workspace_view(WorkshopView::ThreeD); + for(const int preset:{0,1,2}){ + const bool injection=preset!=0;const auto design=preset==2?machine_speed_density_example():machine_sensor_engine_example(injection);const auto tag=preset==2?"speed-density-":injection?"alpha-n-":"ignition-";preview.set_document(design);preview.set_workspace_view(WorkshopView::Logic);key_preview(sf::Keyboard::Key::Home);std::uint32_t connector=0;std::string host;for(const auto& n:design.logic.nodes)if(n.kind==LogicNodeKind::Ecu)connector=n.id;for(const auto& p:design.parts)if(p.injector)host=p.name; + click_preview(preview.logic_node_rectangle(connector,resolution).position+sf::Vector2f{24,12});capture(prefix+tag+"connector-build.png");preview.start_test();check(preview.testing(),"Sensor engine preview failed to start");preview.pause_test();for(int i=0;i<50;++i)preview.step_test();capture(prefix+tag+"connector-live.png");preview.set_workspace_view(WorkshopView::ThreeD);click_preview({ui.inspector_x+30,148});capture(prefix+tag+"ecu-live.png"); + if(injection){click_preview({160,120});preview.handle_event(sf::Event::KeyPressed{.code=sf::Keyboard::Key::F,.control=true},resolution);for(char c:host)preview.handle_event(sf::Event::TextEntered{std::uint32_t(c)},resolution);key_preview(sf::Keyboard::Key::Enter);click_preview({85,242});capture(prefix+tag+"injector-live.png");} + preview.stop_test(); + } + const auto viewport=preview.workspace_viewport(resolution);check(viewport.position.x>ui.left.position.x+ui.left.size.x,"Viewport overlaps library"); + } + std::cout<<"Workshop UI previews rendered\n";return 0; + } + { + MachineBuilder ecu_ui;auto design=machine_engine_example();std::uint32_t ecu=0,host=0;for(const auto& p:design.parts){if(p.kind==MachinePartKind::Ecu)ecu=p.id;if(p.kind==MachinePartKind::Chamber&&p.position.x<0)host=p.id;}ecu_ui.set_document(design);const auto ui=WorkshopLayout::calculate(size); + const auto click_ecu=[&](sf::Vector2f at){const sf::Vector2i point{int(at.x),int(at.y)};ecu_ui.handle_event(sf::Event::MouseButtonPressed{sf::Mouse::Button::Left,point},size);ecu_ui.handle_event(sf::Event::MouseButtonReleased{sf::Mouse::Button::Left,point},size);}; + const auto choose=[&](std::uint32_t id){ecu_ui.set_workspace_view(WorkshopView::ThreeD);click_ecu({160,120});ecu_ui.handle_event(sf::Event::KeyPressed{.code=sf::Keyboard::Key::F,.control=true},size);for(char c:ecu_ui.document().find(id)->name)ecu_ui.handle_event(sf::Event::TextEntered{std::uint32_t(c)},size);ecu_ui.handle_event(sf::Event::KeyPressed{.code=sf::Keyboard::Key::Enter},size);click_ecu({85,242});check(ecu_ui.selected_parts()==std::vector{id},"ECU workflow object selection failed");}; + choose(ecu);click_ecu({ui.inspector_x+90,222});check(ecu_ui.document().find(ecu)->sensor_ecu.enabled,"ECU preset button did not select sensor ignition");click_ecu({ui.inspector_x+100,258});check(ecu_ui.workspace_view()==WorkshopView::Logic&&ecu_ui.document().logic.nodes.size()==1,"Physical ECU did not create grouped Logic connector"); + choose(ecu);click_ecu({ui.inspector_x+90,222});check(ecu_ui.document().find(ecu)->sensor_ecu.strategy==SensorEcuConfig::Strategy::AlphaN&&logic_node_ports(ecu_ui.document().logic.nodes[0]).size()==3,"Alpha-N preset did not require TPS on its existing connector"); + choose(ecu);click_ecu({ui.inspector_x+90,222});check(ecu_ui.document().find(ecu)->sensor_ecu.strategy==SensorEcuConfig::Strategy::SpeedDensity&&logic_node_ports(ecu_ui.document().logic.nodes[0]).size()==4,"Speed-density preset did not replace TPS with MAP/IAT"); + choose(host);click_ecu({ui.inspector_x+90,222});check(bool(ecu_ui.document().find(host)->injector),"Chamber inspector did not attach an injector");click_ecu({ui.inspector_x+100,258});check(ecu_ui.document().find(host)->injector->ecu==ecu,"Injector binding picker did not choose fuel ECU"); + ecu_ui.set_document(machine_sensor_engine_example(true));host=0;for(const auto& p:ecu_ui.document().parts)if(p.injector)host=p.id;ecu_ui.start_test();check(ecu_ui.testing(),"Alpha-N workshop Test failed to start");ecu_ui.pause_test();choose(host);click_ecu({ui.inspector_x+90,222});check(bool(ecu_ui.document().find(host)->injector),"Test allowed injector removal");ecu_ui.stop_test(); + } + for(float yaw:{-110.f,-28.f,0.f,90.f})for(float pitch:{-50.f,0.f,60.f}){ + OrbitCamera camera;camera.yaw=yaw;camera.pitch=pitch; + for(Vec3 point:{Vec3{0,.2f,0},Vec3{.2f,.3f,-.1f}}){auto projected=builder_project(camera,viewport,point);check(bool(projected),"Point in front of camera was not projected");auto ray=builder_ray(camera,viewport,*projected);check(length(cross(subtract(point,ray.origin),ray.direction))<1e-5f,"Mouse ray and render projection differ");} + } + MachinePart box;box.kind=MachinePartKind::Chamber;box.position={};box.size={.4f,.1f,.1f};box.rotation={0,0,90}; + check(bool(builder_pick_part(box,{{0,0,1},{0,0,-1}})),"Picking missed a rotated part");check(!builder_pick_part(box,{{.15f,0,1},{0,0,-1}}),"Picking ignored the part rotation"); + auto euler=builder_rotate_world({15,35,20},0,45);const auto transformed=rotate({0,1,0},euler),expected=rotate(rotate({0,1,0},{15,35,20}),{45,0,0});check(length(subtract(transformed,expected))<1e-5f,"World rotation composition is incorrect"); + MachinePart bulge;bulge.kind=MachinePartKind::Pipe;bulge.position={};bulge.points={{-.15f,0,0},{0,0,0},{.15f,0,0}};bulge.point_diameters={.02f,.08f,.02f}; + check(bool(builder_pick_part(bulge,{{0,.035f,1},{0,0,-1}})),"Picking missed the widened interior point"); + check(!builder_pick_part(bulge,{{-.15f,.035f,1},{0,0,-1}}),"Picking used an interior diameter at the narrow endpoint"); + MachineBuilder builder;auto doc=machine_pump_example();builder.set_document(doc); + const auto tank_id=doc.parts.front().id;const auto original=doc.find(tank_id)->position; + const auto click=[&](sf::Vector2f p){sf::Vector2i pos{int(std::round(p.x)),int(std::round(p.y))};builder.handle_event(sf::Event::MouseButtonPressed{sf::Mouse::Button::Left,pos},size);builder.handle_event(sf::Event::MouseButtonReleased{sf::Mouse::Button::Left,pos},size);}; + const auto key=[&](sf::Keyboard::Key code,bool control=false){builder.handle_event(sf::Event::KeyPressed{.code=code,.control=control},size);}; + key(sf::Keyboard::Key::Escape);check(!builder.finished(),"Escape exited the workshop"); + MachineBuilder exit_menu;exit_menu.handle_event(sf::Event::MouseButtonPressed{sf::Mouse::Button::Left,{24,24}},size);check(exit_menu.finished()&&!exit_menu.wants_close_window(),"Menu did not explicitly leave the workshop"); + MachineBuilder close_window;close_window.handle_event(sf::Event::Closed{},size);check(close_window.finished()&&close_window.wants_close_window(),"Window close did not explicitly exit the application"); + auto center=builder_project(builder.camera(),builder.workspace_viewport(size),original);check(bool(center),"Tank is outside the builder camera");click(*center);key(sf::Keyboard::Key::G); + const float gizmo=builder.camera().distance*.828427f/viewport.size.y*76;auto end=builder_project(builder.camera(),builder.workspace_viewport(size),add(original,{gizmo,0,0}));check(bool(end),"Gizmo projection failed"); + const sf::Vector2i start{int(end->x),int(end->y)},finish{int(end->x+50),int(end->y)}; + builder.handle_event(sf::Event::MouseButtonPressed{sf::Mouse::Button::Left,start},size);builder.handle_event(sf::Event::MouseMoved{finish},size);builder.handle_event(sf::Event::MouseButtonReleased{sf::Mouse::Button::Left,finish},size); + check(builder.document().find(tank_id)->position.x>original.x+.01f,"Mouse move gizmo did not translate the selected part");validate_machine(builder.document()); + key(sf::Keyboard::Key::Z,true);check(length(subtract(builder.document().find(tank_id)->position,original))<1e-6f,"Undo did not restore the transform"); + key(sf::Keyboard::Key::D,true);check(builder.document().parts.size()==doc.parts.size()+1,"Duplicate did not add a part");key(sf::Keyboard::Key::Delete);check(builder.document().parts.size()==doc.parts.size(),"Delete did not remove a part"); + builder.set_document(doc);click(*builder_project(builder.camera(),builder.workspace_viewport(size),doc.parts[0].position));key(sf::Keyboard::Key::LShift);click(*builder_project(builder.camera(),builder.workspace_viewport(size),doc.parts[1].position));builder.handle_event(sf::Event::KeyReleased{.code=sf::Keyboard::Key::LShift},size); + check(builder.selected_parts().size()==2,"Shift-click did not select two parts");key(sf::Keyboard::Key::D,true);check(builder.document().parts.size()==doc.parts.size()+2,"Group duplicate did not copy both parts");key(sf::Keyboard::Key::Delete);check(builder.document().parts.size()==doc.parts.size(),"Group delete did not remove both copies"); + builder.set_document(doc);key(sf::Keyboard::Key::A,true);check(builder.selected_parts().size()==doc.parts.size(),"Select all missed parts");key(sf::Keyboard::Key::D,true);check(builder.document().parts.size()==doc.parts.size()*2&&builder.document().links.size()==doc.links.size()*2,"Group duplication lost internal connections");validate_machine(builder.document()); + builder.set_document(doc);key(sf::Keyboard::Key::A,true);key(sf::Keyboard::Key::C,true);key(sf::Keyboard::Key::V,true); + check(builder.document().parts.size()==doc.parts.size()*2&&builder.document().links.size()==doc.links.size()*2,"Copy/paste lost group parts or internal connections");validate_machine(builder.document()); + key(sf::Keyboard::Key::Escape);key(sf::Keyboard::Key::Escape);check(!builder.finished(),"Repeated Escape exited the workshop"); + builder.set_document(doc);key(sf::Keyboard::Key::V,true);check(builder.document().parts.size()==doc.parts.size()*2,"Loading a document discarded the copied group"); + builder.set_document(doc);key(sf::Keyboard::Key::A,true); + const auto group_screen=builder_project(builder.camera(),builder.workspace_viewport(size),doc.parts.front().position);check(bool(group_screen),"Group is outside camera"); + builder.handle_event(sf::Event::MouseButtonPressed{sf::Mouse::Button::Right,{int(group_screen->x),int(group_screen->y)}},size); + check(builder.selected_parts().size()==doc.parts.size(),"Right-click collapsed the selected group");key(sf::Keyboard::Key::Escape); + { + const auto root=std::filesystem::temp_directory_path()/("fluidsim-assembly-ui-"+std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + const auto* old_directory=std::getenv("FLUIDSIM_MACHINE_DIR");const std::string previous_directory=old_directory?old_directory:"";machine_test_directory(root.string()); + const float mx=float(size.x)*.5f-280,my=float(size.y)*.5f-185; + click({450,26});click({mx+450,my+30});for(char c:std::string("pump-module"))builder.handle_event(sf::Event::TextEntered{std::uint32_t(c)},size);key(sf::Keyboard::Key::Enter); + const auto saved=machine_assembly_directory()/"pump-module.assembly.toml";check(std::filesystem::exists(saved),"Save selected assembly did not create a file"); + check(load_machine(saved).links.size()==doc.links.size(),"Saved assembly lost internal connections"); + MachineDocument marker;marker.name="Marker";marker.add(MachinePartKind::Cap); + for(int i=0;i<5;++i)save_machine(marker,machine_assembly_directory()/("0"+std::to_string(i)+".assembly.toml")); + check(machine_assembly_files().size()==6,"Assembly library did not discover saved files"); + builder.set_document({});click({450,26});builder.handle_event(sf::Event::MouseWheelScrolled{.delta=-1,.position={800,400}},size);click({mx+480,my+76+4*47+17}); + check(builder.document().parts.size()==doc.parts.size()&&builder.document().links.size()==doc.links.size(),"Library could not browse and insert a saved connected assembly");validate_machine(builder.document()); + key(sf::Keyboard::Key::Z,true);check(builder.document().parts.empty(),"Assembly insertion did not undo as one action");key(sf::Keyboard::Key::Y,true);check(builder.document().parts.size()==doc.parts.size(),"Assembly insertion redo failed"); + for(const auto& path:machine_assembly_files())std::filesystem::remove(path);std::filesystem::remove(machine_assembly_directory());std::filesystem::remove(root);machine_test_directory(previous_directory); + } + // Atmosphere symbols share a point with their pipe end, and must remain + // selectable/movable after Connect without implicitly starting another wire. + MachineDocument ports;const auto pipe_id=ports.add(MachinePartKind::Pipe,{0,.25f,0}).id; + ports.find(pipe_id)->points={{-.28f,0,0},{0,0,0}};ports.find(pipe_id)->diameter=ports.find(pipe_id)->outlet_diameter=.12f; + const auto atmosphere_id=ports.add(MachinePartKind::Reservoir,{0,.25f,0}).id; + const auto free_atmosphere_id=ports.add(MachinePartKind::Reservoir,{.25f,.25f,.1f}).id; + ports.connect({pipe_id,1},{atmosphere_id,0});validate_machine(ports); + const auto screen_part=[&](unsigned id){auto point=builder_project(builder.camera(),builder.workspace_viewport(size),builder.document().find(id)->position);check(bool(point),"Atmosphere is outside the builder camera");return *point;}; + const auto drag_body=[&](sf::Vector2f point){const sf::Vector2i from{int(std::round(point.x)),int(std::round(point.y))},to{from.x+45,from.y-30}; + builder.handle_event(sf::Event::MouseButtonPressed{sf::Mouse::Button::Left,from},size);builder.handle_event(sf::Event::MouseMoved{to},size);builder.handle_event(sf::Event::MouseButtonReleased{sf::Mouse::Button::Left,to},size);}; + builder.set_document(ports);key(sf::Keyboard::Key::Q);click(screen_part(atmosphere_id)); + check(builder.selected_parts()==std::vector{atmosphere_id},"Attached pipe captured atmosphere selection"); + key(sf::Keyboard::Key::C);click(screen_part(atmosphere_id));check(builder.selected_parts()==std::vector{atmosphere_id},"Connect selected the pipe instead of the atmosphere symbol"); + key(sf::Keyboard::Key::Escape);drag_body(screen_part(atmosphere_id)); + check(length(subtract(builder.document().find(atmosphere_id)->position,ports.find(atmosphere_id)->position))>.01f,"Escape did not return Connect to Move"); + check(length(subtract(machine_port_position(*builder.document().find(pipe_id),1),builder.document().find(atmosphere_id)->position))<1e-6f,"Moving atmosphere detached its pipe endpoint");validate_machine(builder.document()); + key(sf::Keyboard::Key::Z,true);check(length(subtract(builder.document().find(atmosphere_id)->position,ports.find(atmosphere_id)->position))<1e-6f,"Undo did not restore atmosphere and attached pipe");validate_machine(builder.document()); + for(auto exit_key:{sf::Keyboard::Key::G,sf::Keyboard::Key::Escape}){ + builder.set_document(ports);key(sf::Keyboard::Key::C);click(screen_part(free_atmosphere_id));key(exit_key); + click(screen_part(free_atmosphere_id)); + // A selected port now starts a direct wire. Use the move gizmo to move + // a boundary whose sole port occupies its entire symbol. + const float handle=builder.camera().distance*.828427f/viewport.size.y*76; + drag_body(*builder_project(builder.camera(),builder.workspace_viewport(size),add(builder.document().find(free_atmosphere_id)->position,{handle,0,0}))); + check(length(subtract(builder.document().find(free_atmosphere_id)->position,ports.find(free_atmosphere_id)->position))>.01f,"Move gizmo could not move a directly connectable boundary"); + check(builder.document().links.size()==ports.links.size(),"Moving atmosphere changed connections");validate_machine(builder.document()); + } + builder.set_document(ports);drag_body(screen_part(free_atmosphere_id)); + check(length(subtract(builder.document().find(free_atmosphere_id)->position,ports.find(free_atmosphere_id)->position))>.01f,"Move required a separate selection click before dragging atmosphere"); + // Explicit connection still works after the selection/drag changes. + builder.set_document(ports);key(sf::Keyboard::Key::C);click(screen_part(free_atmosphere_id));click(*builder_project(builder.camera(),builder.workspace_viewport(size),machine_port_position(*ports.find(pipe_id),0))); + check(builder.document().links.size()==ports.links.size()+1,"Explicit atmosphere connection failed");validate_machine(builder.document()); + // Route insertion uses the clicked segment in perspective and preserves the + // authored length/volume until its new bend is actually moved. + MachineDocument route;const auto route_id=route.add(MachinePartKind::Pipe,{0,.3f,0}).id; + route.find(route_id)->points={{-.4f,0,-.25f},{.4f,.15f,.3f}};route.find(route_id)->rotation={15,30,10}; + route.find(route_id)->point_diameters={.02f,.08f}; + builder.set_document(route);const auto& route_part=*route.find(route_id);const auto local=add(route_part.points[0],multiply(subtract(route_part.points[1],route_part.points[0]),.3f)); + const auto on_route=builder_project(builder.camera(),builder.workspace_viewport(size),machine_world_point(route_part,local));check(bool(on_route),"Route point is outside camera"); + const auto location=builder_pipe_location(builder.camera(),viewport,route_part,*on_route);check(location&&length(subtract(location->local_point,local))<1e-5f,"Bend insertion ignored perspective or part rotation"); + key(sf::Keyboard::Key::Q);click(*on_route);key(sf::Keyboard::Key::P);key(sf::Keyboard::Key::LControl);click(*on_route);builder.handle_event(sf::Event::KeyReleased{.code=sf::Keyboard::Key::LControl},size); + const auto& edited=*builder.document().find(route_id);check(edited.points.size()==3,"Ctrl-click did not insert a pipe bend"); + check(std::abs(machine_pipe_length(edited)-machine_pipe_length(route_part))<1e-6f&&std::abs(machine_part_volume(edited)-machine_part_volume(route_part))<1e-8f,"Inserting a bend changed pipe length or volume"); + check(edited.point_diameters.size()==3&&std::abs(edited.point_diameters[1]-.038f)<.0002f,"Inserted bend lost the original interpolated diameter"); + // The selected point's diameter field changes only that point, including undo. + click(WorkshopLayout::calculate(size).field_value(6).position+sf::Vector2f{16,10});for(char c:std::string("90"))builder.handle_event(sf::Event::TextEntered{std::uint32_t(c)},size);key(sf::Keyboard::Key::Enter); + check(std::abs(builder.document().find(route_id)->point_diameters[1]-.09f)<1e-6f&&builder.document().find(route_id)->point_diameters.front()==.02f&&builder.document().find(route_id)->point_diameters.back()==.08f,"Point diameter edit changed the wrong points"); + key(sf::Keyboard::Key::Z,true);check(std::abs(builder.document().find(route_id)->point_diameters[1]-.038f)<.0002f,"Undo did not restore the point diameter"); + key(sf::Keyboard::Key::Z,true);check(builder.document().find(route_id)->points==route_part.points,"Undo did not restore the routed pipe"); + key(sf::Keyboard::Key::Y,true);check(builder.document().find(route_id)->points.size()==3,"Redo did not restore the inserted bend"); + // Checks select and focus the problem part without changing the document. + MachineDocument dry;const auto dry_cylinder=dry.add(MachinePartKind::Cylinder,{.6f,.3f,0}).id;dry.add(MachinePartKind::Pipe,{-.6f,.3f,0}); + builder.set_document(dry);key(sf::Keyboard::Key::H);click({float(size.x)-280,235}); + check(builder.selected_parts()==std::vector{dry_cylinder},"Clicking a check did not select the affected cylinder"); + check(length(subtract(builder.camera().target,dry.find(dry_cylinder)->position))<1e-5f,"Clicking a check did not focus the affected part"); + check(builder.document().links.empty()&&builder.document().parts.size()==dry.parts.size(),"Checks changed the machine"); + key(sf::Keyboard::Key::Home);const float whole_distance=builder.camera().distance;key(sf::Keyboard::Key::F); + check(builder.camera().distance.003&&slow_elapsed<.1,"Slow playback was stalled by old queued audio or ran at full speed");builder.pause_test(); + const auto ui=WorkshopLayout::calculate(size);const auto motor_id=std::find_if(instrumented.parts.begin(),instrumented.parts.end(),[](const auto& p){return p.kind==MachinePartKind::DriveMotor;})->id; + click({160,120});key(sf::Keyboard::Key::F,true);for(char c:std::string("Bench speed drive"))builder.handle_event(sf::Event::TextEntered{std::uint32_t(c)},size);key(sf::Keyboard::Key::Enter);click({90,242}); + check(builder.selected_parts()==std::vector{motor_id},"Objects list did not select the drive motor");click(ui.field_value(0).position+sf::Vector2f{16,10});for(char c:std::string("1200"))builder.handle_event(sf::Event::TextEntered{std::uint32_t(c)},size);key(sf::Keyboard::Key::Enter); + check(std::abs(builder.document().find(motor_id)->drive.target_speed*60/(2*pi)-1200)<.01f,"Drive RPM could not be tuned live");const auto before_tuning_step=builder.playback_stats().simulation_seconds;builder.step_test();check(std::abs(builder.playback_stats().simulation_seconds-before_tuning_step-.01)<1e-7&&std::abs(builder.measurements().back().values[1]-1200)<.01f,"Live tuning reset time or failed to reach the running shaft"); + const auto cells=builder.document().cells_per_meter;click({ui.inspector_x+12+2.5f*(ui.inspector_width-34)/6+4,148});click(ui.field_value(0).position+sf::Vector2f{16,10});for(char c:std::string("80"))builder.handle_event(sf::Event::TextEntered{std::uint32_t(c)},size);key(sf::Keyboard::Key::Enter);check(builder.document().cells_per_meter==cells,"A rebuild-only property was editable during Test"); + key(sf::Keyboard::Key::Z,true);check(builder.document().find(motor_id)->drive.target_speed==instrumented.find(motor_id)->drive.target_speed,"Live tuning undo failed");key(sf::Keyboard::Key::Y,true);check(std::abs(builder.document().find(motor_id)->drive.target_speed*60/(2*pi)-1200)<.01f,"Live tuning redo failed"); + click(*builder_project(builder.camera(),builder.workspace_viewport(size),builder.document().find(tank_id)->position));const auto before_probe=builder.playback_stats().simulation_seconds;click({ui.inspector_x+ui.inspector_width*.5f,210});check(builder.document().probes.size()==3&&builder.playback_stats().simulation_seconds==before_probe,"Adding a temperature probe live reloaded physics");builder.step_test();check(builder.measurements().back().count==3,"Live instrument setup mixed trace schemas"); + click(*builder_project(builder.camera(),builder.workspace_viewport(size),builder.document().find(tank_id)->position)+sf::Vector2f{18,-18});click({ui.inspector_x+ui.inspector_width*.75f,484}); + check(builder.document().probes.size()==2&&std::none_of(builder.document().probes.begin(),builder.document().probes.end(),[](const auto& p){return p.quantity==MachineQuantity::Temperature;}),"Overlapping probe markers could not be selected independently"); + const auto csv=std::filesystem::temp_directory_path()/("fluidsim-probe-"+std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())+".csv");check(builder.export_measurements(csv),"Instrument CSV export failed");{std::ifstream input(csv);std::string header;std::getline(input,header);check(header.find("time_s")!=std::string::npos&&header.find("Tank pressure [kPa abs]")!=std::string::npos,"CSV lost time or channel units");}std::filesystem::remove(csv); + builder.stop_test();check(std::abs(builder.document().find(motor_id)->drive.target_speed*60/(2*pi)-1200)<.01f&&!builder.measurements().empty(),"Returning to Build lost live calibration or captured data"); + { + MachineBuilder sensing;MachineDocument tapped;const auto pipe=tapped.add(MachinePartKind::Pipe,{}).id;tapped.add(MachinePartKind::Shaft,{.5f,0,0});sensing.set_document(tapped); + const auto click_sensor=[&](sf::Vector2f at){const sf::Vector2i point{int(std::round(at.x)),int(std::round(at.y))};sensing.handle_event(sf::Event::MouseButtonPressed{sf::Mouse::Button::Left,point},size);sensing.handle_event(sf::Event::MouseButtonReleased{sf::Mouse::Button::Left,point},size);}; + const auto sensor_key=[&](sf::Keyboard::Key code,bool control=false){sensing.handle_event(sf::Event::KeyPressed{.code=code,.control=control},size);}; + const auto at=builder_project(sensing.camera(),sensing.workspace_viewport(size),machine_world_point(*tapped.find(pipe),{.09f,0,0}));check(bool(at),"Sensor tap is outside the camera"); + sensing.handle_event(sf::Event::MouseButtonPressed{sf::Mouse::Button::Right,{int(std::round(at->x)),int(std::round(at->y))}},size); + const auto menu_x=std::clamp(at->x,sensing.workspace_viewport(size).position.x,float(size.x)-210),menu_y=std::clamp(at->y,92.f,float(size.y)-420); + click_sensor({menu_x+90,menu_y+311});click_sensor({ui.inspector_x+35,210});check(sensing.document().sensors.size()==1,"Add sensor from the pipe context menu failed"); + const auto sensor_id=sensing.document().sensors[0].id;check(sensing.document().sensors[0].target.part==pipe&&std::abs(sensing.document().sensors[0].target.position-.8f)<.02f,"Context sensor did not use the clicked pipe arclength"); + sensor_key(sf::Keyboard::Key::Z,true);check(sensing.document().sensors.empty(),"Sensor creation did not enter undo history");sensor_key(sf::Keyboard::Key::Y,true);check(sensing.document().sensors[0].id==sensor_id,"Sensor redo changed stable identity"); + sensing.start_test();check(sensing.testing(),"Instrumented physical machine did not start");sensing.pause_test();const auto time=sensing.playback_stats().simulation_seconds; + click_sensor({ui.inspector_x+35,210});click_sensor({ui.inspector_x+ui.inspector_width-48,480});check(sensing.document().sensors.size()==1&&sensing.playback_stats().simulation_seconds==time,"Test permitted sensor topology edits or advanced time");sensing.step_test();sensing.stop_test(); + sensor_key(sf::Keyboard::Key::Delete);check(sensing.document().find(pipe)==nullptr&&sensing.document().find_sensor(sensor_id)&&sensing.document().find_sensor(sensor_id)->target.part==0,"Deleting a host removed its sensor instead of exposing an unresolved binding"); + sensor_key(sf::Keyboard::Key::H);click_sensor({ui.inspector_x+80,240});click_sensor({ui.inspector_x+ui.inspector_width-48,480});check(sensing.document().sensors.empty(),"Unresolved-sensor diagnostic did not navigate to its editable attachment"); + } + { + MachineBuilder logic;auto design=machine_logic_example();const auto master=std::find_if(design.logic.nodes.begin(),design.logic.nodes.end(),[](const auto& n){return n.kind==LogicNodeKind::Switch;})->id; + const auto button=design.logic.add(LogicNodeKind::Button,{600,600}).id,lamp=design.logic.add(LogicNodeKind::Lamp,{900,600}).id;design.logic.find(button)->name="Held test button";design.logic.connect({button,"out"},{lamp,"in"});logic.set_document(design); + const auto logic_click=[&](sf::Vector2f at){const sf::Vector2i point{int(std::round(at.x)),int(std::round(at.y))};logic.handle_event(sf::Event::MouseButtonPressed{sf::Mouse::Button::Left,point},size);logic.handle_event(sf::Event::MouseButtonReleased{sf::Mouse::Button::Left,point},size);}; + const auto logic_key=[&](sf::Keyboard::Key k,bool control=false){logic.handle_event(sf::Event::KeyPressed{.code=k,.control=control},size);}; + const auto output=[&](std::uint32_t id){for(const auto& n:logic.logic_values())if(n.id==id)return n.values.back();throw std::runtime_error("Missing shared live graph values");}; + logic_click({float(size.x)-121,68});check(logic.workspace_view()==WorkshopView::Logic,"Logic toolbar tab did not open the graph workspace");const auto original_camera=logic.camera();logic_key(sf::Keyboard::Key::Home); + const auto master_rect=logic.logic_node_rectangle(master,size);logic_click(master_rect.position+sf::Vector2f{20,10});logic_click({ui.inspector_x+40,148});for(char c:std::string("Changed master"))logic.handle_event(sf::Event::TextEntered{std::uint32_t(c)},size);logic_key(sf::Keyboard::Key::Enter);check(logic.document().logic.find(master)->name=="Changed master","Logic name field did not apply");logic_key(sf::Keyboard::Key::Z,true); + const auto speed_node=std::find_if(design.logic.nodes.begin(),design.logic.nodes.end(),[](const auto& n){return n.kind==LogicNodeKind::Slider;})->id,gate_node=std::find_if(design.logic.nodes.begin(),design.logic.nodes.end(),[](const auto& n){return n.kind==LogicNodeKind::And;})->id;const auto old_links=logic.document().logic.links;logic_click(logic.logic_pin_position(speed_node,"out",size));logic_click(logic.logic_pin_position(gate_node,"in1",size));check(logic.document().logic.links==old_links,"Rejected graph edit changed connections");logic_key(sf::Keyboard::Key::Escape);logic_key(sf::Keyboard::Key::Y,true);check(logic.document().logic.find(master)->name=="Changed master","A rejected graph edit destroyed redo history");for(const auto& p:design.parts)if(p.kind==MachinePartKind::Pipe)check(logic.document().find(p.id)->points==p.points,"Logic edits changed physical pipe geometry"); + const auto body=logic.logic_node_rectangle(design.logic.nodes.front().id,size);logic_click(body.position+sf::Vector2f{20,40});check(logic.selected_parts()==std::vector{design.sensors[0].target.part},"Graph sensor selection did not share its physical host selection"); + logic.start_test();logic.pause_test();const auto paused=logic.playback_stats().simulation_seconds;const auto control=logic.logic_node_rectangle(master,size);const float scale=control.size.x/240;logic_click({control.position.x+control.size.x*.5f,control.position.y+control.size.y-26*scale});logic.step_test();check(!std::get(output(master).value)&&logic.document().logic.find(master)->boolean,"Live graph control changed its default or did not reach physics"); + const auto after_step=logic.playback_stats().simulation_seconds;logic.set_workspace_view(WorkshopView::Split);check(logic.playback_stats().simulation_seconds==after_step&&logic.testing(),"Switching to split view restarted or advanced paused physics");const auto physical=logic.workspace_viewport(size),graph=logic.logic_viewport(size);check(physical.position.x+physical.size.x(output(button).value),"Holding a graph button did not survive physics stepping");logic.set_workspace_view(WorkshopView::ThreeD);logic.step_test();check(!std::get(output(button).value),"Closing Logic left its button pressed");check(logic.playback_stats().simulation_seconds>paused,"Graph/physics steps did not share simulation time");logic.stop_test(); + } + // Layout rows, footer actions and physical viewport have disjoint bounds. + for(const sf::Vector2u resolution:{sf::Vector2u{1280,720},sf::Vector2u{1920,1080}})for(float left:{220.f,420.f})for(float right:{280.f,520.f}){ + const auto layout=WorkshopLayout::calculate(resolution,left,right); + check(layout.viewport.position.x>=layout.left.position.x+layout.left.size.x+8,"Library overlaps physical viewport"); + check(layout.viewport.position.x+layout.viewport.size.x<=layout.right.position.x-8,"Inspector overlaps physical viewport"); + check(layout.field_row(layout.field_rows()-1).position.y+48<=float(resolution.y)-164,"Fields overlap inspector footer"); + check(layout.library.position.y+layout.library_rows()*36<=float(resolution.y)-238,"Library rows overlap the preview"); + } + { + MachineBuilder responsive;responsive.set_document(doc);const auto before=responsive.workspace_viewport(size);const auto layout=WorkshopLayout::calculate(size); + const sf::Vector2i from{int(layout.inspector_x-4),420},to{from.x-100,420}; + responsive.handle_event(sf::Event::MouseButtonPressed{sf::Mouse::Button::Left,from},size);responsive.handle_event(sf::Event::MouseMoved{to},size);responsive.handle_event(sf::Event::MouseButtonReleased{sf::Mouse::Button::Left,to},size); + check(responsive.workspace_viewport(size).size.xbefore.size.x,"Collapsing inspector did not return viewport space"); + } + // Search captures typing and routes a filtered library selection to placement. + builder.set_document({});click({60,120});key(sf::Keyboard::Key::F,true);for(char c:std::string("Throttle"))builder.handle_event(sf::Event::TextEntered{std::uint32_t(c)},size);key(sf::Keyboard::Key::Enter);click({90,242});click({650,450}); + check(builder.document().parts.size()==1&&builder.document().parts.front().kind==MachinePartKind::Throttle,"Filtered library did not place the searched part"); + // Ports can be dragged directly without entering the optional Ports tool. + builder.set_document(ports);key(sf::Keyboard::Key::Q);click(screen_part(free_atmosphere_id)); + const auto source=screen_part(free_atmosphere_id),target=*builder_project(builder.camera(),builder.workspace_viewport(size),machine_port_position(*ports.find(pipe_id),0)); + builder.handle_event(sf::Event::MouseButtonPressed{sf::Mouse::Button::Left,{int(source.x),int(source.y)}},size);builder.handle_event(sf::Event::MouseMoved{{int(target.x),int(target.y)}},size);builder.handle_event(sf::Event::MouseButtonReleased{sf::Mouse::Button::Left,{int(target.x),int(target.y)}},size); + check(builder.document().links.size()==ports.links.size()+1,"Direct port dragging failed");validate_machine(builder.document()); + builder.set_document(doc); + if(argc==3&&std::string(argv[1])=="--preview"){ + const std::filesystem::path output=argv[2];std::filesystem::create_directories(output);sf::ContextSettings context;context.depthBits=24;context.majorVersion=2;context.minorVersion=1; + sf::RenderWindow window(sf::VideoMode(size),"Machine builder preview",sf::Style::Default,sf::State::Windowed,context);window.setVisible(false);sf::Font font("C:/Windows/Fonts/consola.ttf"); + const auto capture=[&](const char* file){builder.draw(window,&font);sf::Texture image(size);image.update(window);check(image.copyToImage().saveToFile(output/file),"Screenshot save failed");}; + { + const auto* old_directory=std::getenv("FLUIDSIM_MACHINE_DIR");const std::string previous_directory=old_directory?old_directory:"";machine_test_directory(std::filesystem::absolute(output/"assembly-library").string()); + builder.set_document(machine_pump_example());key(sf::Keyboard::Key::A,true);capture("assembly-selection.png"); + const auto point=builder_project(builder.camera(),builder.workspace_viewport(size),builder.document().parts.front().position);check(bool(point),"Assembly preview is outside camera"); + builder.handle_event(sf::Event::MouseButtonPressed{sf::Mouse::Button::Right,{int(point->x),int(point->y)}},size);capture("assembly-context.png");key(sf::Keyboard::Key::Escape); + click({450,26});const float mx=float(size.x)*.5f-280,my=float(size.y)*.5f-185;click({mx+450,my+30}); + for(char c:std::string("Vacuum pump module"))builder.handle_event(sf::Event::TextEntered{std::uint32_t(c)},size);capture("assembly-save.png");key(sf::Keyboard::Key::Enter); + click({450,26});capture("assembly-library.png");click({mx+480,my+93});capture("assembly-inserted.png"); + key(sf::Keyboard::Key::C,true);key(sf::Keyboard::Key::V,true);capture("assembly-pasted.png");machine_test_directory(previous_directory); + } + MachineRenderer3D cached;MachineDocument visual;auto& visual_pipe=visual.add(MachinePartKind::Pipe);const auto visual_id=visual_pipe.id;visual_pipe.points=bulge.points;visual_pipe.point_diameters=bulge.point_diameters;OrbitCamera view; + cached.draw(window,visual,nullptr,viewport,view,0);const auto initial_builds=cached.stats().pipe_mesh_builds;check(initial_builds==1,"First pipe mesh was not cached"); + const auto check_overlay=[&]{sf::RectangleShape marker({24,24});marker.setPosition({8,8});marker.setFillColor(sf::Color::Magenta);window.draw(marker);sf::Texture pixels(size);pixels.update(window);check(pixels.copyToImage().getPixel({16,16})==sf::Color::Magenta,"Raw 3D rendering left SFML overlays invisible");};check_overlay(); + view.orbit({12,8});visual.parts[0].position.x+=.03f;visual.parts[0].rotation.z=15; + cached.draw(window,visual,nullptr,viewport,view,visual_id);check(cached.stats().pipe_mesh_builds==initial_builds,"Camera, transform or selection rebuilt local pipe geometry"); + MachineSnapshot colored;MachinePartState ps;ps.id=visual_id;ps.pipe_pressures={95000,105000};colored.parts.push_back(ps); + cached.draw(window,visual,&colored,viewport,view,0);check(cached.stats().pipe_mesh_builds==initial_builds,"Pressure colors rebuilt the pipe mesh"); + visual.parts[0].points.back().y+=.05f;cached.draw(window,visual,nullptr,viewport,view,0);check(cached.stats().pipe_mesh_builds==initial_builds+1,"Path edit failed to invalidate the mesh"); + visual.parts[0].point_diameters[1]+=.005f;cached.draw(window,visual,nullptr,viewport,view,0);check(cached.stats().pipe_mesh_builds==initial_builds+2,"Interior diameter edit failed to invalidate the mesh"); + cached.draw_icon(window,MachinePartKind::Pipe,{{0,0},{35,26}});cached.draw_icon(window,MachinePartKind::Cylinder,{{40,0},{35,26}});check(cached.stats().icon_atlas_builds==1,"Part icons rebuilt their atlas"); + check_overlay(); + visual.parts.clear();cached.draw(window,visual,nullptr,viewport,view,0);check(cached.stats().cached_pipes==0,"Deleted geometry remained cached"); + visual.next_id=visual_id;visual.add(MachinePartKind::Pipe).points={{0,0,0},{0,.2f,0}};cached.draw(window,visual,nullptr,viewport,view,0);check(cached.stats().pipe_mesh_builds==initial_builds+3,"Reused document IDs retained stale geometry"); + builder.set_document({});capture("empty.png");builder.set_document(machine_pump_example());click(*builder_project(builder.camera(),builder.workspace_viewport(size),builder.document().parts.front().position));capture("pump-build.png"); + key(sf::Keyboard::Key::C);capture("connections.png");builder.start_test();builder.update();capture("pump-test.png");builder.stop_test();builder.set_document(machine_engine_example());capture("engine-build.png"); + key(sf::Keyboard::Key::I);capture("import-menu.png");key(sf::Keyboard::Key::Escape); + builder.set_document(machine_from_engine(scenario_example("v8-h")));key(sf::Keyboard::Key::A,true);capture("v8-import.png"); + builder.set_document(machine_from_engine(scenario_example("single-450")));capture("single-import.png"); + for(auto& p:builder.document().parts)if(p.kind==MachinePartKind::Ecu){key(sf::Keyboard::Key::Q);click(*builder_project(builder.camera(),builder.workspace_viewport(size),p.position));break;}capture("ecu-settings.png"); + builder.set_document(dry);key(sf::Keyboard::Key::H);capture("checks-missing-fuel.png"); + auto feedback=ports;const auto feedback_ecu=feedback.add(MachinePartKind::Ecu,{.3f,.5f,0}).id;builder.set_document(feedback); + key(sf::Keyboard::Key::C);click(screen_part(free_atmosphere_id)); + const auto hover=[&](MachinePort port){const auto point=builder_project(builder.camera(),builder.workspace_viewport(size),machine_port_position(*builder.document().find(port.part),port.port));check(bool(point),"Preview target is outside camera");builder.handle_event(sf::Event::MouseMoved{{int(std::round(point->x)),int(std::round(point->y))}},size);}; + hover({pipe_id,0});capture("connect-air-ready.png");hover({feedback_ecu,0});capture("connect-incompatible.png"); + builder.set_document(route);key(sf::Keyboard::Key::Q);click(*builder_project(builder.camera(),builder.workspace_viewport(size),machine_world_point(route_part,local)));key(sf::Keyboard::Key::P); + key(sf::Keyboard::Key::LControl);click(*builder_project(builder.camera(),builder.workspace_viewport(size),machine_world_point(route_part,local)));builder.handle_event(sf::Event::KeyReleased{.code=sf::Keyboard::Key::LControl},size);capture("inserted-bend.png"); + MachineDocument sculpt;sculpt.add(MachinePartKind::Pipe)=bulge;sculpt.parts[0].id=1;sculpt.parts[0].name="Per-point diameter pipe";builder.set_document(sculpt);key(sf::Keyboard::Key::Q);click(*builder_project(builder.camera(),builder.workspace_viewport(size),Vec3{}));key(sf::Keyboard::Key::P);click(*builder_project(builder.camera(),builder.workspace_viewport(size),Vec3{}));capture("point-diameters.png"); + auto gauges=machine_from_engine(scenario_example("single-450"));machine_test::drive(gauges,3000);unsigned cylinder=0,downpipe=0,carb=0; + for(const auto& p:gauges.parts){if(p.kind==MachinePartKind::Cylinder)cylinder=p.id;if(p.kind==MachinePartKind::Carburetor)carb=p.id;if(p.name=="Exhaust atmosphere")for(const auto& link:gauges.links){const auto other=link.a.part==p.id?link.b.part:link.b.part==p.id?link.a.part:0;if(const auto part=gauges.find(other);part&&part->kind==MachinePartKind::Pipe)downpipe=other;}} + gauges.probes={{1,cylinder,"Cylinder pressure",MachineQuantity::Pressure},{2,downpipe,"Downpipe lambda",MachineQuantity::Lambda},{3,cylinder,"Cylinder fresh charge",MachineQuantity::ChargeAfr}}; + builder.set_document(gauges);builder.set_test_speed(.1);builder.start_test();builder.pause_test();for(int i=0;i<100;++i)builder.step_test();key(sf::Keyboard::Key::M);capture("instruments-pressure.png"); + click({float(size.x)-50,318});capture("instruments-lambda.png"); + const auto center=builder_project(builder.camera(),builder.workspace_viewport(size),gauges.find(carb)->position);check(bool(center),"Carburetor is outside camera");bool selected_carb=false; + for(int dy:{0,-8,8,-16,16}){for(int dx:{0,-8,8,-16,16}){click(*center+sf::Vector2f{float(dx),float(dy)});if(builder.selected_parts()==std::vector{carb}){selected_carb=true;break;}}if(selected_carb)break;} + check(selected_carb,"Could not select the carburetor for live tuning");click({ui.inspector_x+40,148});capture("live-carb-tuning.png");builder.stop_test(); + } + std::cout<<"Builder selection, transforms, connected copy/paste, assembly library, undo/redo, explicit exit and mode switching passed\n";return 0; +}catch(const std::exception& e){std::cerr< +#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& sim,int count){for(int i=0;ishaft.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(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(value(logic_only.snapshot(),pure,lamp,"in").value),"A short momentary press vanished before a physics boundary");run(logic_only,1);check(!std::get(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(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(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 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< +#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< +#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);} +templatevoid rejects(F&& operation,const char* why){bool rejected=false;try{operation();}catch(const std::exception&){rejected=true;}check(rejected,why);} +struct TempDirectory { + std::filesystem::path path=std::filesystem::temp_directory_path()/("fluidsim-port-tests-"+std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + TempDirectory(){std::filesystem::create_directories(path);} + ~TempDirectory(){std::error_code error;std::filesystem::remove_all(path,error);} +}; +void write(const std::filesystem::path& file,const std::string& value){std::ofstream stream(file);stream.exceptions(std::ios::failbit|std::ios::badbit);stream<(stream)),{}};} +MachinePortDescriptor signal(PortDomain domain,PortDirection direction,SignalQuantity quantity=SignalQuantity::None){MachinePortDescriptor port;port.key="value";port.name="Value";port.domain=domain;port.direction=direction;port.quantity=quantity;port.cardinality=direction==PortDirection::Output?PortCardinality::Multiple:PortCardinality::Single;return port;} +} +int main()try{ + MachinePart undeclared;undeclared.kind=MachinePartKind(machine_part_kind_count); + rejects([&]{machine_port_count(undeclared);},"Undeclared part silently acquired gas ports"); + rejects([&]{machine_part_volume(undeclared);},"Undeclared part silently acquired gas storage"); + for(int kind=0;kind keys; + for(unsigned i=0;i::quiet_NaN();check(SignalValue::number(nan).validity==SignalValidity::Fault&&!signal_value_valid_for(SignalValue::number(1,nan),number_in),"Nonfinite signal was treated as valid"); + for(auto domain:{PortDomain::Gas,PortDomain::Rotational,PortDomain::Translational}){MachinePortDescriptor a;a.key="physical";a.name="Physical";a.domain=domain;validate_port_descriptor(a);check(port_compatibility(a,a).allowed,"Physical bidirectional domain was rejected");check(!port_compatibility(a,boolean_in).allowed,"Signal connected to a physical port");} + check(std::string(signal_quantity_unit(SignalQuantity::AngularSpeed))=="rad/s","Internal speed units are not SI"); + + TempDirectory temp;const auto file=temp.path/"ports.machine.toml"; + const std::string legacy=R"(format = 4 +name = 'Legacy control' +[[parts]] +id = 1 +kind = 'ecu' +[[parts]] +id = 2 +kind = 'cylinder' +[[parts]] +id = 3 +kind = 'throttle' +[[connections]] +a_part = 1 +a_port = 0 +b_part = 2 +b_port = 2 +method = 4 +[[connections]] +a_part = 1 +a_port = 0 +b_part = 3 +b_port = 2 +method = 4 +)"; + write(file,legacy);auto d=load_machine(file);check(std::count_if(d.links.begin(),d.links.end(),[](const auto& link){return link.kind==MachineConnectionKind::Control;})==2,"Legacy ECU fan-out was lost"); + check(machine_link_domain(d,d.links[0])==PortDomain::EcuAssignment,"Legacy control was reinterpreted as a signal");save_machine(d,file);const auto saved=read(file); + check(saved.find("format = 10")!=std::string::npos&&saved.find("'assignments'")!=std::string::npos&&saved.find("'ecu_assignment'")!=std::string::npos,"Current format did not serialize stable port/method keys"); + const auto roundtrip=load_machine(file);check(roundtrip.links.size()==d.links.size()&&roundtrip.links[0].a.port==0&&roundtrip.links[0].b.port==2,"Stable key round-trip changed legacy wiring"); + auto multiple=d;multiple.links.push_back(multiple.links.front());rejects([&]{validate_machine(multiple);},"Duplicate driver was accepted"); + auto reversed=d;std::swap(reversed.links[0].a,reversed.links[0].b);rejects([&]{validate_machine(reversed);},"Noncanonical ECU connection passed document validation"); + check(!machine_connection_preview(d,{1,0},{2,0}).allowed,"Preview accepted a gas/control mismatch"); + + const auto custom=[](bool reorder){return std::string(R"(format = 5 +name = 'Stable custom ports' +[[parts]] +id = 1 +kind = 'pipe' +position_m = [0,0,0] +points_m = [[-0.2,0,0],[0,0,0]] +point_diameters_m = [0.02,0.02] +[[parts]] +id = 2 +kind = 'chamber' +position_m = [0,0,0] +)")+(reorder?"port_coordinates = [[1,0,0],[0,0,0]]\nport_keys = ['tap_b','tap_a']\n":"port_coordinates = [[0,0,0],[1,0,0]]\nport_keys = ['tap_a','tap_b']\n")+R"([[connections]] +a_part = 1 +a_port = 'end' +b_part = 2 +b_port = 'tap_a' +method = 'orifice' +)";}; + write(file,custom(false));const auto original=load_machine(file);write(file,custom(true));const auto reordered=load_machine(file); + check(original.links.front().b.port==0&&reordered.links.front().b.port==1,"Stable key did not follow a reordered physical port"); + check(machine_port_position(*original.find(2),0)==machine_port_position(*reordered.find(2),1),"Reordered port changed its physical endpoint"); + auto invalid=custom(false);invalid.replace(invalid.find("'tap_a'",invalid.find("[[connections]]")),7,"'unknown'");write(file,invalid);rejects([&]{load_machine(file);},"Unknown serialized port key silently reconnected"); + invalid=custom(false);invalid.replace(invalid.find("'orifice'"),9,"'unknown'");write(file,invalid);rejects([&]{load_machine(file);},"Unknown serialized method was accepted"); + auto duplicate=original;duplicate.find(2)->port_keys={"tap_a","tap_a"};rejects([&]{validate_machine(duplicate);},"Duplicate custom identities were accepted"); + write(file,R"(format = 4 +[[parts]] +id = 1 +kind = 'pipe' +point_diameters_m = [0.04,0.04] +[[parts]] +id = 2 +kind = 'junction' +junction_method = 1 +position_m = [0.15,0.25,0] +[[connections]] +a_part = 1 +a_port = 1 +b_part = 2 +b_port = 5 +method = 0 +)"); + const auto alias=load_machine(file);check(alias.links.front().b.port==0&&machine_port_count(*alias.find(2))==1,"Legacy junction alias did not migrate to its shared port"); + for(const auto& source:machine_files()){auto machine=load_machine(source);const auto copy=temp.path/source.filename();save_machine(machine,copy);const auto loaded=load_machine(copy);check(loaded.parts.size()==machine.parts.size()&&loaded.links.size()==machine.links.size(),"Existing machine changed topology during migration");} + std::cout<<"Typed port domains, quantities, validation and version 5 migration passed\n";return 0; +}catch(const std::exception& error){std::cerr< +#include +#include +#include +#include +#include +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,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);} +double value(const MachineSensorState& s,unsigned output=0){check(s.status==MachineSensorStatus::Valid&&s.outputs.at(output).validity==SignalValidity::Valid,"Sensor output is unexpectedly invalid");return std::get(s.outputs.at(output).value);} +void run(MachineSimulation& sim,int count){for(int i=0;ipressure=200000;doc.connect({pipe,1},{tank,0});doc.connect({pipe,0},{ambient,0}); + const auto pressure=doc.add_sensor(MachineSensorKind::Pressure,tank).id; + const auto difference=doc.add_sensor(MachineSensorKind::DifferentialPressure,tank).id;doc.find_sensor(difference)->reference={ambient,.5f,"Ambient reference"}; + const auto temperature=doc.add_sensor(MachineSensorKind::GasTemperature,pipe,.9f).id; + const auto flow=doc.add_sensor(MachineSensorKind::MassFlow,pipe,.35f).id; + for(int i=0;i<12;++i)doc.add_sensor(MachineSensorKind::Pressure,pipe,float(i)/11); + doc.probes={{1,pipe,"Pipe flow",MachineQuantity::MassFlow,.35f}}; + validate_machine(doc);MachineSimulation sensed(doc);auto bare=doc;bare.sensors.clear();MachineSimulation baseline(bare); + check(close(value(sensed.sense(pressure)),200000)&&close(value(sensed.sense(difference)),200000-101325),"Pressure taps lost absolute or differential reference"); + check(close(value(sensed.sense(temperature)),300)&&value(sensed.sense(flow))==0,"Initial sensor sample is missing or has the wrong units"); + check(sensed.snapshot().sensors.size()==16&&sensed.measure().count==1,"Sensors consume the instrument recording limit"); + bool reverse_flow=false;for(int i=0;i<2048;++i){const auto a=sensed.process(),b=baseline.process();check(a.left==b.left&&a.right==b.right,"Ideal sensor altered machine audio");reverse_flow|=value(sensed.sense(flow))<-.00001;} + const auto measured=sensed.measure();check(close(value(sensed.sense(flow)),measured.values[0]),"Pipe sensor and targeted instrument disagree at the same location"); + check(reverse_flow,"Total mass flow lost the pipe Start-to-End sign"); + const auto a=sensed.snapshot(),b=baseline.snapshot();check(a.cells==b.cells&&a.substeps==b.substeps&&a.parts.size()==b.parts.size(),"Sensor added a physical volume or solver cells"); + for(std::size_t i=0;itarget.position==.9f,"Remeshing changed normalized sensor location");} + auto moved=doc;const auto original=machine_sensor_position(moved,moved.find_sensor(temperature)->target);moved.transform_selection({pipe,tank,ambient},{},{.1f,.2f,.3f});const auto after=machine_sensor_position(moved,moved.find_sensor(temperature)->target); + check(original&&after&&close(after->x-original->x,.1)&&close(after->y-original->y,.2)&&close(after->z-original->z,.3),"Sensor marker did not follow its physical host"); + auto invalid=doc;invalid.find_sensor(temperature)->target.position=1.1f;rejects([&]{validate_machine(invalid);},"Out-of-pipe sensor position was accepted"); + rejects([&]{doc.add_sensor(MachineSensorKind::Crank,tank);},"Crank sensor attached to a gas tank");rejects([&]{doc.add_sensor(MachineSensorKind::MassFlow,tank);},"A gas volume silently supplied an ambiguous through-flow"); + auto unresolved=doc;unresolved.remove(tank);check(unresolved.find_sensor(pressure)&&unresolved.find_sensor(pressure)->target.part==0&&unresolved.find_sensor(pressure)->target.label==doc.find(tank)->name,"Deleting a host removed its sensor or lost the unresolved label"); + MachineSimulation missing(unresolved);check(missing.sense(pressure).status==MachineSensorStatus::MissingHost&&missing.sense(pressure).outputs[0].validity==SignalValidity::Missing,"Deleted host supplied plausible valid data"); + unresolved.add(MachinePartKind::Chamber);MachineSimulation still_missing(unresolved);check(still_missing.sense(pressure).status==MachineSensorStatus::MissingHost,"New part silently repaired a deleted sensor host"); + auto wrong=doc;wrong.find_sensor(pressure)->target.part=ambient;wrong.find(ambient)->kind=MachinePartKind::Cap;MachineSimulation incompatible(wrong);check(incompatible.sense(pressure).status==MachineSensorStatus::IncompatibleHost,"Incompatible sensor binding supplied a reading"); + + MachineDocument rotation;const auto shaft=rotation.add(MachinePartKind::Shaft).id;rotation.find(shaft)->shaft.initial_angle=-.5f;rotation.find(shaft)->shaft.initial_speed=-2; + const auto crank=rotation.add_sensor(MachineSensorKind::Crank,shaft).id;rotation.find_sensor(crank)->angle_offset=.25f;MachineSimulation rotating(rotation,1000); + auto pickup=rotating.sense(crank);check(value(pickup)==-2&&close(value(pickup,1),2*double(pi)-.25)&&std::get(pickup.outputs[2].value),"Crank pickup lost signed speed or wrapped angle");check(pickup.timing_shaft==shaft&&close(pickup.unwrapped_angle,-.25),"Ideal crank provenance is unavailable"); + run(rotating,4000);pickup=rotating.sense(crank);check(close(pickup.unwrapped_angle,-8.25)&&value(pickup,1)>=0&&value(pickup,1)<2*pi,"Crank pickup discarded unwrapped reverse crossings"); + for(unsigned i=0;i<3;++i){const auto descriptor=machine_sensor_output(*rotation.find_sensor(crank),i);check(descriptor.direction==PortDirection::Output&&descriptor.cardinality==PortCardinality::Multiple,"Sensor outputs cannot fan out");check(signal_value_valid_for(pickup.outputs[i],descriptor),"Crank values do not match typed outputs");check(pickup.outputs[i].timestamp==rotating.snapshot().time,"Crank channels are not synchronized");} + auto changed=rotation;changed.find_sensor(crank)->angle_offset=1;rejects([&]{rotating.apply_tuning(changed);},"Timing attachment changed during Test"); + auto stopped=rotation;stopped.find(shaft)->shaft.initial_speed=0;MachineSimulation stationary(stopped);check(value(stationary.sense(crank))==0&&stationary.sense(crank).status==MachineSensorStatus::Valid,"Stopped crank was confused with a missing pickup"); + + MachineDocument throttle;const auto duct=throttle.add(MachinePartKind::Pipe).id,body=throttle.add(MachinePartKind::Throttle).id;throttle.connect({duct,1},{body,0});throttle.find(body)->pedal_control=false;throttle.find(body)->opening=.35f; + const auto tps=throttle.add_sensor(MachineSensorKind::ThrottlePosition,body).id;MachineSimulation opening(throttle);opening.set_controls(.9f);run(opening,1);check(close(value(opening.sense(tps)),.35),"TPS read pedal demand instead of fixed mechanism opening");throttle.find(body)->opening=.6f;opening.apply_tuning(throttle);run(opening,1);check(close(value(opening.sense(tps)),.6),"TPS missed safe live throttle tuning"); + + const auto assembly=machine_extract_assembly(doc,{tank},"Tapped tank");check(assembly.sensors.size()==2&&assembly.find_sensor(difference)->reference.part==0,"Assembly lost sensors or retained an external reference ID");auto destination=doc;const auto inserted=machine_insert_assembly(destination,assembly,{1,.25f,0});check(destination.sensors.size()==18&&destination.sensors[16].target.part==inserted[0]&&destination.sensors[16].id!=pressure,"Copied sensor retained source identity or host");check(destination.sensors[17].reference.part==0,"Copied differential reference accidentally matched an existing host"); + const auto whole=machine_extract_assembly(doc,{pipe,tank,ambient},"Tapped network");MachineDocument empty;machine_insert_assembly(empty,whole,{});MachineSimulation copied(empty);check(close(value(copied.sense(empty.sensors[1].id)),200000-101325),"Internal sensor reference did not follow assembly remapping"); + + const auto file=std::filesystem::temp_directory_path()/("fluidsim-sensors-"+std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())+".machine.toml"); + save_machine(destination,file);const auto restored=load_machine(file);check(restored.sensors==destination.sensors&&restored.next_sensor_id==destination.next_sensor_id,"Sensor identities, binding labels or locations did not round-trip"); + save_machine(unresolved,file);auto reloaded=load_machine(file);check(reloaded.next_id==unresolved.next_id&&reloaded.find_sensor(pressure)->target.part==0,"Reload reused deleted part IDs or rebound an unresolved sensor"); + reloaded.sensors.erase(reloaded.sensors.begin());const auto reserved=reloaded.next_sensor_id;save_machine(reloaded,file);auto repeat=load_machine(file);check(repeat.next_sensor_id==reserved,"Deleted sensor IDs can be reused after reload"); + {std::ofstream out(file);out<<"format=7\n[[sensors]]\nid=1\nkind='future_unknown_sensor'\n";}rejects([&]{load_machine(file);},"Unknown sensor kind silently loaded");std::filesystem::remove(file); + {const auto example=load_machine(FLUIDSIM_SENSOR_EXAMPLE_FILE);MachineSimulation bench(example);run(bench,2048);check(example.sensors.size()==6&&close(value(bench.sense(5)),188.49556)&&close(value(bench.sense(6)),.45),"Sensor example lost its shaft or mechanism measurement");for(const auto& s:bench.snapshot().sensors)check(s.status==MachineSensorStatus::Valid,"Sensor example contains an unresolved or invalid attachment");} + std::cout<<"Sensor attachments, coherent samples, typed outputs, unresolved bindings and assembly persistence passed\n";return 0; +}catch(const std::exception& error){std::cerr< +#include +#include +#include +#include +#include +using namespace fluidsim::port; +using namespace fluidsim::port::machine_math; +namespace { +void check(bool v,const std::string& why){if(!v)throw std::runtime_error(why);} +bool close(float a,float b,float tolerance=1e-5f){return std::abs(a-b)<=tolerance;} +templatevoid rejects(F f,const char* why){bool rejected=false;try{f();}catch(const std::exception&){rejected=true;}check(rejected,why);} +const MachinePartState& state(const MachineSnapshot& s,unsigned id){for(auto& p:s.parts)if(p.id==id)return p;throw std::runtime_error("Missing part snapshot");} +std::uint32_t duct(MachineDocument& d,MachinePort a,MachinePort b){const auto start=machine_port_position(*d.find(a.part),a.port),end=machine_port_position(*d.find(b.part),b.port);auto& p=d.add(MachinePartKind::Pipe);const auto id=p.id;p.position={};p.points={start,end};d.connect({id,0},a);d.connect({id,1},b);return id;} +void run(MachineSimulation& sim,int frames){for(int i=0;i=2&&std::string(argv[1])=="--mixture-sweep"){ + std::cout<<"case,rpm,charge_median,charge_max,cylinder_bulk_median,pipe_bulk_median,fuel_flow_g_s,fuel_cut_samples,spark_cut_samples,valve_float_samples\n"; + for(const char* id:{"single-450","flat6"}){if(argc==3&&std::string(id)=="flat6")break;auto design=argc==3?load_machine(argv[2]):machine_from_engine(scenario_example(id));machine_test::drive(design,3000);unsigned cylinder=0,pipe=0,carb=0; + for(const auto& p:design.parts){if(!cylinder&&p.kind==MachinePartKind::Cylinder)cylinder=p.id;if(p.kind==MachinePartKind::Carburetor)carb=p.id;if(p.name=="Exhaust atmosphere")for(const auto& l:design.links){const auto other=l.a.part==p.id?l.b.part:l.b.part==p.id?l.a.part:0;if(const auto p=design.find(other);p&&p->kind==MachinePartKind::Pipe)pipe=other;}} + if(!pipe)for(const auto& p:design.parts)if(p.kind==MachinePartKind::Pipe)pipe=p.id; + design.probes={{1,cylinder,"Fresh charge",MachineQuantity::ChargeAfr},{2,cylinder,"Cylinder bulk",MachineQuantity::AirFuelRatio},{3,pipe,"Exhaust bulk",MachineQuantity::AirFuelRatio}};MachineSimulation sim(design);sim.set_controls(1); + for(float rpm:{3000.f,5000.f,6500.f,7000.f,7500.f,10000.f,12000.f,13000.f,14000.f,16000.f}){machine_test::target_speed(design,rpm);sim.apply_tuning(design);std::array,3> samples;unsigned fuel_cut=0,spark_cut=0,floating=0;double fuel_flow=0;unsigned count=0; + for(int i=0;i<22050;++i){sim.process();if(i>=11025&&i%16==0){const auto m=sim.measure();for(int channel=0;channel<3;++channel)if(std::isfinite(m.values[channel]))samples[channel].push_back(m.values[channel]);const auto s=sim.snapshot();const auto& c=state(s,cylinder);fuel_cut+=c.fuel_cut;spark_cut+=c.ignition_cut;floating+=c.valve_float;if(carb)fuel_flow+=state(s,carb).fuel_flow*1000;++count;}} + for(auto& channel:samples)std::sort(channel.begin(),channel.end());const auto median=[&](int channel){const auto& values=samples[channel];return values.empty()?std::numeric_limits::quiet_NaN():values[values.size()/2];}; + std::cout<<(argc==3?"saved":id)<<','<=256,"A real short duct lost its physical length or CFL substeps"); + MachineDocument shaped;auto& sculpted=shaped.add(MachinePartKind::Pipe);const auto shaped_id=sculpted.id; + sculpted.points={{0,0,0},{.1f,0,0},{.105f,0,0},{.3f,0,0}};sculpted.point_diameters={.02f,.1f,.025f,.06f}; + const auto frustum=[](double length,double a,double b){return float(double(pi)/12*length*(a*a+a*b+b*b));}; + const float expected_volume=frustum(.1,.02,.1)+frustum(.005,.1,.025)+frustum(.195,.025,.06); + check(close(machine_part_volume(sculpted),expected_volume,1e-9f),"Per-point pipe volume is not the sum of its frustums"); + check(close(machine_pipe_diameter(sculpted,.1f),.1f,1e-6f)&&close(machine_pipe_diameter(sculpted,.1025f),.0625f,1e-6f),"Per-point diameter interpolation is incorrect"); + for(float density:{10.f,40.f,200.f}){auto refined=shaped;refined.cells_per_meter=density;MachineSimulation simulation(refined);const auto info=simulation.snapshot(); + check(info.cells==std::uint64_t(profile_section_cells(.3f,1/density)),"Profile points forced extra fluid cells"); + check(close(state(info,shaped_id).volume,expected_volume,2e-9f),"Mesh refinement changed the per-point pipe volume");try{run(simulation,256);}catch(const std::exception& e){throw std::runtime_error("Per-point profile at "+std::to_string(density)+" cells/m: "+e.what());} + const auto settled=simulation.snapshot();check(settled.positivity_repairs==0,"Per-point pipe equilibrium needed positivity repairs");for(float pressure:state(settled,shaped_id).pipe_pressures)check(close(pressure,101325,2),"Per-point pipe failed to preserve static equilibrium"); + } + for(auto method:{PipeSolver::Rusanov,PipeSolver::Hlle,PipeSolver::Hllc,PipeSolver::Muscl}){auto flow=shaped;flow.cells_per_meter=200;flow.solver=method; + const auto supply=flow.add(MachinePartKind::Reservoir,machine_port_position(*flow.find(shaped_id),0)).id;flow.find(supply)->pressure=102000; + const auto outlet=flow.add(MachinePartKind::Reservoir,machine_port_position(*flow.find(shaped_id),1)).id; + flow.connect({shaped_id,0},{supply,0});flow.connect({shaped_id,1},{outlet,0});MachineSimulation simulation(flow); + try{run(simulation,2048);}catch(const std::exception& e){throw std::runtime_error(std::string("Per-point pressure-driven ")+pipe_solver_name(method)+": "+e.what());} + const auto result=simulation.snapshot();check(result.positivity_repairs==0&&state(result,shaped_id).flow>0,"Per-point constriction did not sustain stable forward flow"); + } + auto inserted=sculpted;machine_insert_pipe_point(inserted,0,{.035f,0,0});check(inserted.point_diameters.size()==inserted.points.size()&&close(inserted.point_diameters[1],.048f,1e-6f),"Inserted point did not inherit its interpolated diameter"); + check(close(machine_part_volume(inserted),expected_volume,1e-9f),"Inserting a profile point changed gas volume");machine_remove_pipe_point(inserted,1);check(inserted.points==sculpted.points&&inserted.point_diameters==sculpted.point_diameters,"Removing the inserted point lost the diameter profile"); + auto malformed=shaped;malformed.parts[0].point_diameters.pop_back();rejects([&]{validate_machine(malformed);},"Mismatched point diameters were accepted"); + malformed=shaped;malformed.parts[0].point_diameters[1]=0;rejects([&]{validate_machine(malformed);},"Zero point diameter was accepted"); + + auto& chamber=d.add(MachinePartKind::Chamber,{.4f,.4f,.1f});const auto chamber_id=chamber.id;chamber.size={.15f,.1f,.2f};chamber.rotation={30,25,60}; + d.connect({pipe_id,1},{chamber_id,0});validate_machine(d); + d.find(chamber_id)->position.x+=.1f;d.find(chamber_id)->rotation.z+=40;d.sync_connections(chamber_id);validate_machine(d); + check(length(subtract(machine_port_position(*d.find(pipe_id),1),machine_port_position(*d.find(chamber_id),0)))<1e-6f,"Moving a component detached its pipe"); + MachineSimulation with_tank(d);check(close(state(with_tank.snapshot(),chamber_id).volume,.003f,1e-8f),"Rotated tank volume must equal visible dimensions"); + rejects([&]{d.connect({pipe_id,1},{chamber_id,1});},"An occupied pipe endpoint was connected twice"); + auto broken=d;broken.links[0].b.part=999;rejects([&]{validate_machine(broken);},"Dangling references were accepted"); + broken=d;broken.parts.front().points[1]=broken.parts.front().points[0];rejects([&]{validate_machine(broken);},"Collapsed pipe segment was accepted"); + broken=d;broken.find(chamber_id)->size.x=0;rejects([&]{validate_machine(broken);},"Zero volume was accepted"); + broken=d;broken.find(chamber_id)->position.y+=.1f;rejects([&]{validate_machine(broken);},"A connection with a physical gap was accepted"); + + {MachineDocument feedback;const auto pipe=feedback.add(MachinePartKind::Pipe).id; + const auto air=feedback.add(MachinePartKind::Reservoir,{.4f,.3f,0}).id; + const auto ecu=feedback.add(MachinePartKind::Ecu,{0,.6f,0}).id; + const auto cylinder=feedback.add(MachinePartKind::Cylinder).id; + const auto original_points=feedback.find(pipe)->points; + const auto air_preview=machine_connection_preview(feedback,{pipe,1},{air,0}); + check(air_preview.allowed&&air_preview.pipe.has_value(),"Valid connection has no snapped pipe preview"); + check(length(subtract(machine_port_position(*air_preview.pipe,1),machine_port_position(*feedback.find(air),0)))<1e-6f,"Preview did not snap to the target port"); + check(feedback.links.empty()&&feedback.find(pipe)->points==original_points,"Connection preview mutated the design"); + check(!machine_connection_preview(feedback,{pipe,1},{ecu,0}).allowed,"Air/control mismatch was accepted in preview"); + check(!machine_connection_preview(feedback,{air,0},{cylinder,0}).allowed,"Preview connected two air components without a pipe"); + check(machine_connection_preview(feedback,{ecu,0},{cylinder,2}).allowed,"Valid ECU control wire was rejected"); + check(!machine_connection_preview(feedback,{ecu,0},{cylinder,0}).allowed,"ECU preview connected to a cylinder air port"); + feedback.find(air)->position=machine_port_position(*feedback.find(pipe),0); + check(!machine_connection_preview(feedback,{pipe,1},{air,0}).allowed,"Preview allowed a collapsed pipe"); + feedback.find(air)->position={.4f,.3f,0};feedback.connect({pipe,1},{air,0}); + const auto branch=feedback.add(MachinePartKind::Pipe,{-.4f,.25f,0}).id; + const auto occupied=machine_connection_preview(feedback,{branch,0},{air,0});check(!occupied.allowed&&!occupied.message.empty(),"Occupied atmosphere preview lacks an explanation"); + } + {auto bench=machine_engine_example();unsigned cylinder=0,throttle=0,ecu=0; + for(const auto& part:bench.parts){if(part.kind==MachinePartKind::Cylinder)cylinder=part.id;if(part.kind==MachinePartKind::Throttle)throttle=part.id;if(part.kind==MachinePartKind::Ecu)ecu=part.id;} + const auto has_check=[&](unsigned id,const std::string& title){const auto checks=machine_checks(bench);return std::any_of(checks.begin(),checks.end(),[&](const auto& item){return item.part==id&&item.title==title;});}; + check(machine_has_fuel_source(bench,cylinder)&&!has_check(cylinder,"No fuel source"),"Wired injection engine was reported dry"); + bench.disconnect({cylinder,2});check(!machine_has_fuel_source(bench,cylinder)&&has_check(cylinder,"No fuel source"),"Missing injection assignment was not diagnosed"); + bench.find(throttle)->pedal_control=false;bench.find(throttle)->opening=0; + check(!has_check(throttle,"Fixed throttle is closed"),"ECU-controlled throttle was diagnosed from its unused fixed opening"); + bench.disconnect({throttle,2});check(has_check(throttle,"Fixed throttle is closed")&&has_check(ecu,"ECU has no assigned parts"),"Closed throttle / unused ECU checks are missing"); + bench.find(throttle)->kind=MachinePartKind::Carburetor;check(machine_has_fuel_source(bench,cylinder)&&!has_check(cylinder,"No fuel source"),"Connected carburetor was not recognized as a fuel source"); + bench.disconnect({cylinder,0});check(!machine_has_fuel_source(bench,cylinder),"Fuel reachability crossed the cylinder from its exhaust"); + } + + const auto dir=std::filesystem::temp_directory_path()/("fluidsim-machine-tests-"+std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));std::filesystem::create_directories(dir); + const auto file=dir/"test.machine.toml";save_machine(d,file);const auto loaded=load_machine(file); + check(loaded.parts.size()==d.parts.size()&&loaded.links.size()==d.links.size(),"Save/load changed the machine topology"); + check(length(subtract(loaded.find(chamber_id)->rotation,d.find(chamber_id)->rotation))<1e-6f,"Save/load lost 3D rotation"); + check(loaded.find(pipe_id)->points==d.find(pipe_id)->points,"Save/load changed the pipe centerline"); + { + auto source=machine_engine_example();const auto cylinder=std::find_if(source.parts.begin(),source.parts.end(),[](const auto& p){return p.kind==MachinePartKind::Cylinder;})->id; + source.probes.push_back({1,cylinder,"Cylinder pressure",MachineQuantity::Pressure,.5f,14.7f}); + source.probes.push_back({2,0,"Shaft RPM",MachineQuantity::Rpm,.5f,14.7f}); + std::vector selected;for(const auto& part:source.parts)selected.push_back(part.id); + auto assembly=machine_extract_assembly(source,selected,"Intake module"); + check(assembly.parts.size()==source.parts.size()&&assembly.links.size()==source.links.size(),"Assembly extraction lost parts or internal air/control connections"); + check(assembly.probes.size()==1&&assembly.probes.front().name=="Cylinder pressure","Assembly extraction lost a component probe"); + check(length(machine_selection_center(assembly,selected))<1e-6f,"Assembly was not centered for reuse"); + const auto assembly_file=dir/"intake.assembly.toml";save_machine(assembly,assembly_file);assembly=load_machine(assembly_file); + MachineDocument destination=machine_pump_example();const auto prior_parts=destination.parts.size(),prior_links=destination.links.size(); + destination.probes.push_back({1,destination.parts.front().id,"Tank pressure",MachineQuantity::Pressure,.5f,14.7f}); + const auto inserted=machine_insert_assembly(destination,assembly,{1,.35f,.5f}); + check(inserted.size()==assembly.parts.size()&&destination.parts.size()==prior_parts+assembly.parts.size(),"Assembly insertion did not remap and append parts"); + check(destination.links.size()==prior_links+assembly.links.size(),"Assembly insertion did not recreate internal connections"); + const auto remapped=[&](std::uint32_t id){for(std::size_t i=0;iid; + const auto intake=std::find_if(source.links.begin(),source.links.end(),[&](const auto& link){return link.b==MachinePort{cylinder,0};})->a.part; + const auto subset=machine_extract_assembly(source,{ecu,cylinder,intake},"Cylinder module"); + check(subset.parts.size()==3&&subset.links.size()==2,"Subset copy retained external links or lost an internal connection"); + for(const auto& link:subset.links)check(subset.find(link.a.part)&&subset.find(link.b.part),"Assembly connection reaches outside the selected parts"); + rejects([&]{machine_extract_assembly(source,{},"Empty");},"An empty selection was copied"); + MachineDocument empty;rejects([&]{machine_insert_assembly(destination,empty,{});},"An empty assembly was inserted"); + MachineDocument offset;auto& offpipe=offset.add(MachinePartKind::Pipe,{2,1,-3});offpipe.points={{1,0,0},{3,0,0}};offpipe.rotation={0,45,0}; + const auto pipe_assembly=machine_extract_assembly(offset,{offpipe.id},"Offset pipe");const auto centered_path=machine_pipe_path(pipe_assembly.parts.front()); + check(length(add(centered_path.front(),centered_path.back()))<1e-6f&&close(machine_pipe_length(pipe_assembly.parts.front()),2),"Reusable pipe assembly centered its part origin instead of its visible geometry"); + } + const auto shaped_file=dir/"profile.machine.toml";save_machine(shaped,shaped_file);const auto restored_profile=load_machine(shaped_file); + check(restored_profile.parts[0].point_diameters==sculpted.point_diameters&&restored_profile.parts[0].points==sculpted.points,"Save/load lost per-point diameters"); + {std::ifstream input(shaped_file);const std::string text((std::istreambuf_iterator(input)),{});check(text.find("point_diameters_m")!=std::string::npos&&text.find("outlet_diameter_m")==std::string::npos,"Saved pipes still use endpoint diameter fields");} + const auto legacy_file=dir/"legacy.machine.toml"; + const std::string pipe_record="\n[[parts]]\nid = 1\nkind = 'pipe'\npoints_m = [[0,0,0],[0.1,0,0],[0.1,0.2,0]]\n"; + for(int version:{1,2}){{std::ofstream output(legacy_file);output<<"format = "<junction_method=method; + for(int branch=0;branch<3;++branch){auto& pipe=network.add(MachinePartKind::Pipe);pipe.position={};const unsigned port=method==0?unsigned(branch):0;const auto start=machine_port_position(*network.find(node),port);pipe.points={start,add(start,branch==0?Vec3{-.2f,0,0}:branch==1?Vec3{.2f,0,0}:Vec3{0,-.2f,0})};const auto id=pipe.id;network.connect({id,0},{node,port});} + validate_machine(network);MachineSimulation simulation(network);run(simulation,1024);check(simulation.snapshot().positivity_repairs==0,"Junction equilibrium required positivity repairs"); + } + // A closed throttle must isolate a high-pressure supply; opening it must charge the tank. + const auto valve_test=[](bool open){MachineDocument doc;const auto supply=doc.add(MachinePartKind::Reservoir,{-.4f,.3f,0}).id;doc.find(supply)->pressure=120000; + const auto valve=doc.add(MachinePartKind::Throttle,{0,.3f,0}).id;doc.find(valve)->pedal_control=false;doc.find(valve)->opening=open?1.f:0.f; + const auto tank=doc.add(MachinePartKind::Chamber,{.4f,.3f,0}).id;duct(doc,{supply,0},{valve,0});duct(doc,{valve,1},{tank,0});MachineSimulation sim(doc);run(sim,8820);return state(sim.snapshot(),tank).pressure;}; + const float closed=valve_test(false),open=valve_test(true);check(std::abs(closed-101325)<1,"A closed throttle leaked");check(open>closed+1000,"Opening throttle did not increase downstream pressure"); + + auto pump=machine_pump_example();validate_machine(pump);const auto tank_id=pump.parts.front().id; + MachineSimulation pumping(pump);run(pumping,44100);const auto pump_state=pumping.snapshot(); + check(state(pump_state,tank_id).pressure<99000,"Piston pump did not evacuate its tank");check(pump_state.positivity_repairs==0,"Pump example required positivity repairs"); + std::cout<<"Vacuum tank after 1 s: "<brake.constant=0;engine.connect({machine_primary_shaft(engine),0},{test_load,0});unsigned cylinder_probe=0,downpipe_probe=0;for(const auto& p:engine.parts){if(p.kind==MachinePartKind::Cylinder)cylinder_probe=p.id;if(p.kind==MachinePartKind::Pipe)downpipe_probe=p.id;} + engine.probes={{1,cylinder_probe,"Cylinder pressure",MachineQuantity::Pressure},{2,downpipe_probe,"Downpipe flow",MachineQuantity::MassFlow},{3,downpipe_probe,"Downpipe AFR",MachineQuantity::AirFuelRatio},{4,downpipe_probe,"Downpipe lambda",MachineQuantity::Lambda},{5,cylinder_probe,"Fresh charge",MachineQuantity::ChargeAfr},{6,0,"Tachometer",MachineQuantity::Rpm},{7,0,"Torque",MachineQuantity::Torque},{8,0,"Power",MachineQuantity::Power}}; + validate_machine(engine);MachineSimulation combustion(engine);check(std::isnan(combustion.measure().values[2]),"An empty exhaust reported fabricated AFR");combustion.set_controls(.5f);float peak=0;bool fired=false,exhaust_mixture=false,charge_mixture=false; + for(int i=0;i<22050;++i){auto a=combustion.process();check(std::isfinite(a.left),"Engine audio became nonfinite");if(i%64==0){for(auto& part:combustion.snapshot().parts)if(engine.find(part.id)->kind==MachinePartKind::Cylinder){peak=std::max(peak,part.pressure);fired|=part.combustion;} + const auto readings=combustion.measure();if(std::isfinite(readings.values[2])){exhaust_mixture=true;check(close(readings.values[2]/14.7f,readings.values[3],1e-4f),"Downpipe lambda does not match transported bulk AFR");}charge_mixture|=std::isfinite(readings.values[4]);check(close(readings.values[5],1800,.01f),"Tachometer did not read the simulated shaft");}} + check(peak>500000,"Engine cylinder did not develop compression/combustion pressure");std::cout<<"Engine peak: "<brake.constant=1.5f;for(auto& p:engine.parts)if(p.kind==MachinePartKind::Ecu){p.ecu.idle_rpm=1600;p.ecu.power_afr=12.2f;}combustion.apply_tuning(engine); + check(combustion.snapshot().time==unchanged.time&&state(combustion.snapshot(),cylinder_probe).pressure==state(unchanged,cylinder_probe).pressure,"Live tuning reset time or cylinder state"); + auto changed_geometry=engine;changed_geometry.parts.front().position.x+=.1f;rejects([&]{combustion.apply_tuning(changed_geometry);},"Live tuning accepted changed geometry"); + auto changed_pipe=engine;changed_pipe.find(downpipe_probe)->point_diameters[0]*=1.1f;rejects([&]{combustion.apply_tuning(changed_pipe);},"Live tuning resized a pipe"); + run(combustion,256);check(combustion.snapshot().time>unchanged.time,"Live tuning stopped the simulation"); + const auto instrument_file=dir/"instruments.machine.toml";save_machine(engine,instrument_file);check(load_machine(instrument_file).probes==engine.probes,"Save/load lost instrument settings"); + auto bad_probe=engine;bad_probe.probes[0].part=999999;rejects([&]{validate_machine(bad_probe);},"Missing probe target was accepted");bad_probe=engine;bad_probe.probes.push_back(bad_probe.probes.front());rejects([&]{validate_machine(bad_probe);},"Too many probes were accepted"); + MachineSimulation cold_start(machine_engine_example());cold_start.set_controls(.15f,true);bool started=false; + for(int i=0;i<88200;++i){cold_start.process();if(i%128==0)for(auto& p:cold_start.snapshot().parts)started|=p.combustion;} + check(started&&cold_start.snapshot().rpm>500,"Starter could not start the zero-RPM EFI bench");cold_start.set_controls(0,false);run(cold_start,44100);check(cold_start.snapshot().rpm>500,"EFI bench stalled immediately after starter release"); + auto dry=engine;std::erase_if(dry.links,[](auto& l){return l.kind==MachineConnectionKind::Control;});MachineSimulation motoring(dry);run(motoring,16000);for(auto& p:motoring.snapshot().parts)if(dry.find(p.id)->kind==MachinePartKind::Cylinder)check(!p.combustion&&p.fuel_mass==0&&!p.fuel_source,"An unassigned ECU generated hidden fuel"); + auto no_inject=engine;for(auto& p:no_inject.parts)if(p.kind==MachinePartKind::Ecu)p.injection_enabled=false;MachineSimulation ignition_only(no_inject);run(ignition_only,16000);for(auto& p:ignition_only.snapshot().parts)if(no_inject.find(p.id)->kind==MachinePartKind::Cylinder)check(!p.combustion&&p.fuel_mass==0,"Ignition-only ECU injected fuel"); + MachineDocument shared;auto& node=shared.add(MachinePartKind::Junction,{0,.3f,0});const auto node_id=node.id; + for(int i=0;i<8;++i){auto& p=shared.add(MachinePartKind::Pipe);p.position={};const float a=2*pi*i/8;p.points={{0,.3f,0},{.3f*std::cos(a),.3f,.3f*std::sin(a)}};shared.connect({p.id,0},{node_id,0});}validate_machine(shared);MachineSimulation multiway(shared);run(multiway,1024); + auto group=shared;std::vector group_ids;for(auto& p:group.parts)group_ids.push_back(p.id);group.transform_selection(group_ids,{0,.3f,0},{.2f,.1f,-.1f},1,45);validate_machine(group);check(group.links.size()==shared.links.size(),"Group transform lost junction links"); + MachineDocument tee;const auto p1=tee.add(MachinePartKind::Pipe,{-.15f,.3f,0}).id,p2=tee.add(MachinePartKind::Pipe,{.15f,.3f,0}).id;tee.connect({p1,1},{p2,0});const auto p3=tee.add(MachinePartKind::Pipe,{0,.5f,0}).id;tee.connect({p3,0},{p1,1});validate_machine(tee);check(tee.parts.size()==4&&tee.links.size()==3,"Joining a third pipe failed to create a shared node");MachineSimulation tee_sim(tee);run(tee_sim,1024); + Ecu limiter;limiter.rev_limit_rpm=5000;limiter.rev_resume_rpm=4500;limiter.rev_limit_cut_time=.05f;limiter.step(.001f,1,5100);check(limiter.ignition_cut(),"Limiter failed to engage");for(int i=0;i<20;++i)limiter.step(.001f,1,4000);check(limiter.rev_limited(),"Limiter ignored minimum cutoff duration");for(int i=0;i<40;++i)limiter.step(.001f,1,4000);check(!limiter.rev_limited(),"Limiter did not resume below its hysteresis RPM"); + for(const char* id:{"single-450","twin","flat6","v8-h"}){const auto& preset=scenario_example(id);auto imported=machine_from_engine(preset);float inlet_volume=0,inlet_length=0; + for(auto& p:imported.parts)if(p.id==1||p.name=="Inlet metering stub"||p.kind==MachinePartKind::Carburetor||p.kind==MachinePartKind::Throttle){inlet_volume+=machine_part_volume(p);inlet_length+=p.kind==MachinePartKind::Pipe?machine_pipe_length(p):p.size.x;} + const float original_length=preset.engine.geometry.pipes.front().length(),diameter=preset.engine.pipes.front().diameter;check(close(inlet_length,original_length,1e-6f)&&close(inlet_volume,original_length*pi*.25f*diameter*diameter,1e-8f),"Inlet metering assembly changed imported route length or gas volume"); + save_machine(imported,dir/(std::string(id)+".machine.toml"));auto restored=load_machine(dir/(std::string(id)+".machine.toml"));check(restored.parts.size()==imported.parts.size()&&restored.links.size()==imported.links.size(),"Import roundtrip lost topology");MachineSimulation sim(restored);run(sim,2205);std::cout<<"Imported "< controllers;unsigned carb_id=0;for(auto& p:restored.parts){if(p.kind==MachinePartKind::Ecu)controllers.push_back(p.id);if(p.kind==MachinePartKind::Carburetor)carb_id=p.id;}for(auto controller:controllers)restored.remove(controller); + bool burning=false;MachineSimulation carb(restored);carb.set_controls(.25f);for(int i=0;i<44100;++i){carb.process();if(i%64==0)for(auto& p:carb.snapshot().parts)burning|=p.combustion;}check(burning,"A connected carburetor without an ECU failed to deliver combustible fuel"); + const auto fuel_rate=[&]{double sum=0;for(int i=0;i<4536;++i){carb.process();if(i%24==0)sum+=state(carb.snapshot(),carb_id).fuel_flow;}return sum;};const auto original=fuel_rate(); + auto tuned=restored;tuned.find(carb_id)->carburetor.pilot_jet_diameter*=1.25f;tuned.find(carb_id)->carburetor.main_jet_diameter*=1.25f;tuned.find(carb_id)->carburetor.needle_jet_diameter*=1.25f;const auto time=carb.snapshot().time;carb.apply_tuning(tuned);check(carb.snapshot().time==time,"Live carb tuning restarted the machine"); + check(fuel_rate()>original*1.05,"Changing carb jets live did not increase delivered fuel"); + } + } + if(argc==3&&std::string(argv[1])=="--imports"){for(const char* id:{"single-450","twin","flat6","v8-h"})save_machine(machine_from_engine(scenario_example(id)),std::filesystem::path(argv[2])/(std::string(id)+"-imported.machine.toml"));} + if(argc>=2&&std::string(argv[1])=="--rpm-sweep"){ + for(float rpm:{12000.f,16000.f,20000.f,24000.f}){auto fast=engine;machine_test::drive(fast,rpm);for(auto& p:fast.parts)if(p.kind==MachinePartKind::Ecu)p.ecu.rev_limit_enabled=false;MachineSimulation sim(fast);sim.set_controls(.75f);bool fire=false,floating=false; + for(int i=0;i<13230;++i){sim.process();if(i%64==0)for(auto& p:sim.snapshot().parts){fire|=p.combustion;floating|=p.valve_float;}} + std::cout<<"High RPM: "< +#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-10){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 balance(const ShaftState& s){check(close(s.initial_energy+s.external_work+s.drive_work,s.kinetic_energy+s.friction_loss+s.brake_loss,2e-9),"Mechanical work/energy balance failed");check(s.friction_loss>=0&&s.brake_loss>=0,"Mechanical drag created energy");} +} +int main()try{ + { + MechanicalSystem m;m.add_shaft(1,{.inertia=2});m.add_shaft(2,{.inertia=.5,.initial_speed=10});m.add_drive(3,1,{DriveMode::Torque,4}); + for(int i=0;i<100;++i)m.step(.01); + check(close(m.shaft(1).speed,2)&&close(m.shaft(1).angle,1),"Constant torque does not match analytic acceleration/angle"); + check(close(m.shaft(2).speed,10)&&close(m.shaft(2).angle,10),"Independent shaft was accelerated by another drive"); + check(close(m.drive(3).work,4)&&close(m.shaft(1).kinetic_energy,4),"Motor work did not become shaft kinetic energy");balance(m.shaft(1));balance(m.shaft(2)); + rejects([&]{m.add_flywheel(4,1,1);},"Topology changed after simulation began"); + } + { + MechanicalSystem m;m.add_shaft(1,{.inertia=1});m.add_flywheel(2,1,3);m.add_drive(3,1,{DriveMode::Torque,4});m.step(1); + check(close(m.shaft(1).inertia,4)&&close(m.shaft(1).speed,1),"Flywheel inertia was omitted or counted twice");balance(m.shaft(1)); + } + { + MechanicalSystem m;m.add_shaft(1,{.inertia=1,.initial_speed=3});m.add_brake(2,1,{.constant=10});m.step(1); + check(m.shaft(1).speed==0&&close(m.shaft(1).angle,.45),"Brake did not stop at the correct position within a timestep"); + check(close(m.brake(2).loss,4.5)&&m.brake(2).torque<0,"Brake did not absorb initial kinetic energy");m.step(1); + check(m.shaft(1).speed==0&&close(m.shaft(1).angle,.45),"Brake pushed a stopped shaft into reverse");balance(m.shaft(1)); + } + { + MechanicalSystem m;m.add_shaft(1,{.inertia=1});m.add_drive(2,1,{DriveMode::Torque,4});m.add_brake(3,1,{.constant=10});m.step(1); + check(m.shaft(1).speed==0&&close(m.brake(3).torque,-4)&&m.brake(3).power==0,"Static brake reaction or zero-speed power is wrong"); + m.set_drive(2,{DriveMode::Torque,12});m.step(1);check(close(m.shaft(1).speed,2),"Drive could not overcome static brake capacity");balance(m.shaft(1)); + } + { + MechanicalSystem m;m.add_shaft(1,{.inertia=1,.initial_speed=3});m.add_drive(2,1,{DriveMode::Torque,-20});m.add_brake(3,1,{.constant=10});m.step(1); + check(m.shaft(1).speed<0&&m.shaft(1).angle<0,"Drive could not stop and reverse a shaft");check(m.brake(3).loss>0,"Brake generated energy through reversal");balance(m.shaft(1)); + } + { + MechanicalSystem m;m.add_shaft(1,{.inertia=2,.friction_viscous=1});m.add_drive(2,1,{DriveMode::Speed,10});m.add_brake(3,1,{.constant=3});m.step(.1); + check(close(m.shaft(1).speed,10)&&close(m.shaft(1).angle,1),"Ideal drive did not impose its speed"); + check(close(m.drive(2).work,113)&&close(m.shaft(1).brake_loss,3)&&close(m.shaft(1).friction_loss,10),"Ideal drive did not account for startup work and losses"); + m.step(.1);check(close(m.drive(2).torque,13)&&close(m.drive(2).power,130),"Steady speed-drive reaction does not balance losses"); + m.set_drive(2,{DriveMode::Speed,0});m.step(.1);check(close(m.drive(2).power,-1000),"Ideal drive did not report recovered stopping energy");balance(m.shaft(1)); + } + { + MechanicalSystem m;m.add_shaft(1,{.inertia=.5,.initial_speed=-8,.friction_constant=.3,.friction_viscous=.2,.friction_quadratic=.02});m.add_drive(2,1,{DriveMode::Speed,-8});m.add_brake(3,1,{.constant=.4,.viscous=.1});m.step(.01); + check(m.drive(2).torque<0&&m.drive(2).power>0&&m.brake(3).torque>0&&m.brake(3).power>0,"Reverse rotation used the wrong torque or power sign");balance(m.shaft(1)); + } + for(double dt:{.01,.001}){ + MechanicalSystem m;m.add_shaft(1,{.inertia=2,.initial_speed=10,.friction_viscous=3});const int steps=int(std::round(1/dt));for(int i=0;i0,"Forward-only timing constraint failed to hold at rest");balance(m.shaft(1)); + rejects([&]{m.set_drive(2,{DriveMode::Speed,-1});},"Forward-only shaft accepted a reverse speed command"); + } + { + MechanicalSystem m;m.add_shaft(1,{});m.add_drive(2,1,{DriveMode::Speed,10}); + rejects([&]{m.add_drive(3,1,{DriveMode::Speed,10});},"Multiple ideal drives acquired undefined reaction sharing"); + m.add_drive(3,1,{DriveMode::Torque,1});rejects([&]{m.set_drive(3,{DriveMode::Speed,10});},"Tuning introduced a conflicting speed source"); + rejects([&]{m.add_brake(2,1,{});},"Duplicate mechanical ID was accepted"); + rejects([&]{m.add_flywheel(4,99,1);},"Flywheel attached to a nonexistent shaft"); + rejects([&]{m.add_shaft(4,{.inertia=0});},"Zero inertia was accepted"); + rejects([&]{m.add_brake(4,1,{.constant=-1});},"Negative brake loss was accepted"); + rejects([&]{m.step(0);},"Zero timestep was accepted"); + rejects([&]{m.add_torque(1,std::numeric_limits::quiet_NaN());},"Nonfinite torque was accepted"); + } + std::cout<<"Independent shafts, flywheel inertia, drive/brake work and stopping/reversal passed\n";return 0; +}catch(const std::exception& error){std::cerr< +#include +#include +using namespace fluidsim::port; +using namespace fluidsim::port::desktop; +void check(bool value,const char* message){if(!value)throw std::runtime_error(message);} +void key(ScenarioMenu& menu,sf::Keyboard::Key code,sf::Vector2u size={1280,720}){menu.handle_event(sf::Event::KeyPressed{.code=code},size);} +void click(ScenarioMenu& menu,sf::Vector2i at,sf::Vector2u size={1280,720}){menu.handle_event(sf::Event::MouseButtonPressed{sf::Mouse::Button::Left,at},size);menu.handle_event(sf::Event::MouseButtonReleased{sf::Mouse::Button::Left,at},size);} +int main(int argc,char** argv)try{ + if(argc==2&&std::string(argv[1])=="--window-check"){ + sf::RenderWindow window(sf::VideoMode({640,480}),"FluidSim maximize check");maximize_window(window); + check(window_is_maximized(window),"Window did not enter the native maximized state");window.close();std::cout<<"Native window maximization passed\n";return 0; + } + const auto examples=scenario_examples();check(!examples.empty(),"Scenario catalog is empty"); + ScenarioMenu workshop(examples,{"example.machine.toml"});check(workshop.screen()==MenuScreen::Home,"Menu did not start on Home"); + key(workshop,sf::Keyboard::Key::Enter);check(workshop.screen()==MenuScreen::Workshop&&!workshop.take_choice(),"Workshop navigation launched a machine immediately"); + key(workshop,sf::Keyboard::Key::Enter);auto choice=workshop.take_choice();check(choice&&!choice->example&&choice->machine.empty()&&!choice->play,"New machine choice is invalid"); + key(workshop,sf::Keyboard::Key::Down);key(workshop,sf::Keyboard::Key::Space);choice=workshop.take_choice();check(choice&&choice->machine=="example.machine.toml"&&choice->play,"Saved machine Play did not preserve its path"); + key(workshop,sf::Keyboard::Key::Enter);choice=workshop.take_choice();check(choice&&!choice->play&&!choice->example,"Saved machine Edit was treated as Play"); + key(workshop,sf::Keyboard::Key::Escape);check(workshop.screen()==MenuScreen::Home&&!workshop.finished(),"Back closed the application instead of returning Home"); + key(workshop,sf::Keyboard::Key::Down);key(workshop,sf::Keyboard::Key::Enter);check(workshop.screen()==MenuScreen::Scenarios,"Engine Scenarios did not open its own browser"); + click(workshop,{350,180});check(!workshop.take_choice(),"Selecting an engine row started it immediately"); + key(workshop,sf::Keyboard::Key::Enter);choice=workshop.take_choice();check(choice&&choice->example==0,"Engine browser lost catalog index mapping"); + // Required keyboard focus keeps search text separate from navigation shortcuts. + click(workshop,{350,120});for(char c:std::string("no-such-engine-xyz"))workshop.handle_event(sf::Event::TextEntered{std::uint32_t(c)},{1280,720}); + key(workshop,sf::Keyboard::Key::Enter);key(workshop,sf::Keyboard::Key::Enter);check(!workshop.take_choice(),"Empty search launched an engine"); + key(workshop,sf::Keyboard::Key::Escape);key(workshop,sf::Keyboard::Key::Enter);check(workshop.screen()==MenuScreen::Scenarios,"Returning to browser did not clear search"); + click(workshop,{100,206});key(workshop,sf::Keyboard::Key::End);key(workshop,sf::Keyboard::Key::Enter);choice=workshop.take_choice();check(choice&&choice->example&&examples[*choice->example].category==examples.front().category,"Category selection started an engine from another category"); + key(workshop,sf::Keyboard::Key::B);choice=workshop.take_choice();check(choice&&!choice->example&&choice->machine.empty(),"New machine keyboard shortcut failed"); + ScenarioMenu empty({});key(empty,sf::Keyboard::Key::Enter);key(empty,sf::Keyboard::Key::Enter);check(bool(empty.take_choice()),"Empty engine catalog disabled the workshop"); + ScenarioMenu quit(examples);key(quit,sf::Keyboard::Key::End);key(quit,sf::Keyboard::Key::Enter);check(quit.finished(),"Quit did not close the menu"); + if(argc==3&&std::string(argv[1])=="--preview"){ + const std::filesystem::path directory=argv[2];std::filesystem::create_directories(directory); + sf::ContextSettings context;context.depthBits=24;context.majorVersion=2;context.minorVersion=1; + sf::RenderWindow window(sf::VideoMode({1920,1080}),"Main menu review",sf::Style::Default,sf::State::Windowed,context);window.setVisible(false);sf::Font font("C:/Windows/Fonts/consola.ttf"); + for(const sf::Vector2u size:{sf::Vector2u{1920,1080},sf::Vector2u{1280,720}}){ + window.setSize(size);ScenarioMenu menu(examples,machine_files()); + const auto capture=[&](const std::string& name){menu.draw(window,&font);sf::Texture image(size);image.update(window);check(image.copyToImage().saveToFile(directory/(std::to_string(size.x)+"-"+name+".png")),"Menu screenshot save failed");}; + capture("home");key(menu,sf::Keyboard::Key::Down,size);key(menu,sf::Keyboard::Key::Enter,size);capture("engines"); + click(menu,{350,120},size);const auto it=std::find_if(examples.begin(),examples.end(),[](const auto& example){return example.id=="flat6";}); + if(it!=examples.end())for(char c:it->title)menu.handle_event(sf::Event::TextEntered{std::uint32_t(c)},size);key(menu,sf::Keyboard::Key::Enter,size);capture("engine-search"); + key(menu,sf::Keyboard::Key::Escape,size);key(menu,sf::Keyboard::Key::Up,size);key(menu,sf::Keyboard::Key::Enter,size);capture("workshop"); + key(menu,sf::Keyboard::Key::Down,size);capture("saved-machine"); + } + } + std::cout<<"Main menu, workshop and scenario navigation passed\n";return 0; +}catch(const std::exception& error){std::cerr<.9f&&small_intake_pulse.y>.8f&&small_intake_pulse.z<.1f, - "a small positive intake pulse must be visible as yellow"); - require(small_exhaust_pulse.x>.95f&&small_exhaust_pulse.y<.4f, - "a small positive exhaust pulse must be visible as orange-red"); + require(small_intake_pulse.x>atm.x&&small_intake_pulse.y>=atm.y&&small_intake_pulse.z<=atm.z, + "a positive intake pulse must move continuously toward warmer colors"); + require(small_exhaust_pulse.x>small_intake_pulse.x, + "the same exhaust pulse must reach warmer colors sooner than intake"); + const auto near_atmosphere=desktop::pressure_tint(101325.1f),strong_intake=desktop::pressure_tint(251325); + require(std::abs(near_atmosphere.x-atm.x)<.001f&&std::abs(near_atmosphere.y-atm.y)<.001f, + "the positive pressure palette must remain continuous at atmosphere"); + require(strong_intake.x>.95f&&strong_intake.y<.9f&&strong_intake.z<.1f, + "strong intake overpressure must reach warm colors"); Ecu fast_actuator; for(int i=0;i<230;++i)fast_actuator.step(1.f/44100,1,5000); require(fast_actuator.throttle_opening()>.99f,"throttle actuator must settle within about 5 ms"); @@ -140,6 +145,7 @@ int main() { for (int i = 8; i < 12; ++i) audio_ring.write(float(i), -float(i)); require(audio_ring.read_interpolated(left, right, 1.0) == 4 && left[0] == 7.0f && left[3] == 10.0f, "ring must resume continuously after underrun and wraparound"); + audio_ring.discard();require(audio_ring.available_samples()==0,"Discard must remove old playback latency while stopped");for(int i=100;i<105;++i)audio_ring.write(float(i),-float(i));require(audio_ring.read_interpolated(left,right,1)==4&&left[0]==100&&left[3]==103,"A restarted consumer replayed discarded audio"); SoundProcessor linear_source(44100, 1.0f); linear_source.process(0.0f); const float response = linear_source.process(0.01f); @@ -283,6 +289,17 @@ int main() { require(std::abs(profiled_pipe.cell_mass(1) - 1.225f * 0.002f * 0.2f) < 1e-8f && std::abs(profiled_pipe.minimum_cell_length() - 0.1f) < 1e-8f, "profiled pipes must retain each cell's area and length for mass/CFL calculations"); + Pipe1D contraction{{.001f,.00005f,.001f},{.01f,.01f,.01f}}; + Pipe1D uniform_comparison{.001f,.03f,3}; + require(contraction.prepare_step() pipes;pipes.emplace_back(.001f,1.f,40);auto& pipe=pipes[0];pipe.solver=method; + for(std::size_t i=0;i0,"Burned-fuel origin must survive conservative gas transport"); + } Vehicle bike; bike.set_first_gear(); diff --git a/tests/sensor_ecu_tests.cpp b/tests/sensor_ecu_tests.cpp new file mode 100644 index 0000000..9820905 --- /dev/null +++ b/tests/sensor_ecu_tests.cpp @@ -0,0 +1,141 @@ +#include "FluidSim/Machine.hpp" +#include "FluidSim/FuelInjector.hpp" +#include "FluidSim/Fluid.hpp" +#include +#include +#include +#include +using namespace fluidsim::port; +namespace { +void check(bool ok,const char* why){if(!ok)throw std::runtime_error(why);} +struct Pickup { + SignalValue speed,angle; + Pickup(double time,double unwrapped,double velocity=10,std::uint32_t sensor=1,std::uint32_t shaft=1){ + const double period=2*double(pi);double wrapped=std::fmod(unwrapped,period);if(wrapped<0)wrapped+=period; + speed=SignalValue::number(velocity,time);angle=SignalValue::number(wrapped,time); + speed.timing=angle.timing=SignalTiming{sensor,shaft,time,unwrapped,velocity}; + } +}; +} +int main(int argc,char** argv)try{ + if(argc==3&&std::string(argv[1])=="--write-examples"){ + const std::filesystem::path directory=argv[2];save_machine(machine_sensor_engine_example(false),directory/"sensor-ignition-bench.machine.toml");save_machine(machine_sensor_engine_example(true),directory/"sensor-alpha-n-bench.machine.toml");return 0; + } + const double period=2*double(pi); + Pickup stopped(0,0,0);check(bool(ecu_crank_sample(stopped.speed,stopped.angle,0)),"Stopped pickup rejected"); + check(!ecu_crank_sample(SignalValue::number(10),SignalValue::number(0),0),"Constants gained crank timing authority"); + Pickup a(0,period-.2),b(.01,period+.1); + auto mismatch=b.angle;mismatch.timing->sensor=2;check(!ecu_crank_sample(b.speed,mismatch,.01),"Independent pickups accepted as a coherent pair"); + mismatch=b.angle;mismatch.timing->shaft=2;check(!ecu_crank_sample(b.speed,mismatch,.01),"Different shafts accepted"); + mismatch=b.angle;mismatch.value=.3;check(!ecu_crank_sample(b.speed,mismatch,.01),"Changed angle retained authority"); + mismatch=b.angle;mismatch.timing->sampled_at=0;check(!ecu_crank_sample(b.speed,mismatch,.01),"Stale acquisition time accepted"); + Pickup reverse(0,0,-1);check(!ecu_crank_sample(reverse.speed,reverse.angle,0),"Reverse crank drove forward-only ignition"); + CrankIgnitionClock clock;check(clock.sample(a.speed,a.angle,0,0).sparks==0,"Initial sample invented a spark"); + auto event=clock.sample(b.speed,b.angle,.01,0);check(event.valid&&event.sparks==1,"Wrapped crossing lost a spark"); + check(!clock.sample(b.speed,b.angle,.01,0).valid,"Repeated sample generated timing"); + clock.reset();Pickup before(1,-.4),after(1.01,-.2); + clock.sample(before.speed,before.angle,1,20);event=clock.sample(after.speed,after.angle,1.01,20);check(event.valid&&event.sparks==1,"BTDC crossing did not use pickup angle"); + clock.reset();Pickup many(2,4*period+.1);clock.sample(b.speed,b.angle,.01,0);event=clock.sample(many.speed,many.angle,2,0);check(event.valid&&event.sparks==3,"Multiple unwrapped revolutions were lost"); + clock.reset();clock.sample(stopped.speed,stopped.angle,0,10);Pickup still(.1,0,0);event=clock.sample(still.speed,still.angle,.1,30);check(event.valid&&!event.sparks,"Stationary timing fired from an advance change"); + clock.reset();Pickup early(0,-.4),fired(.01,-.3),retarded(.02,.2); + clock.sample(early.speed,early.angle,0,20);check(clock.sample(fired.speed,fired.angle,.01,20).sparks==1,"Advanced spark was missed"); + check(clock.sample(retarded.speed,retarded.angle,.02,-10).sparks==0,"Advance change fired twice for the same revolution"); + clock.reset();Pickup phase_a(0,pi-.2),phase_b(.01,pi+.1);clock.sample(phase_a.speed,phase_a.angle,0,0,180);check(clock.sample(phase_b.speed,phase_b.angle,.01,0,180).sparks==1,"Explicit cylinder TDC calibration was ignored"); + + // Actual machine -> sensor reference -> arithmetic/delay demonstrates that + // only unchanged routing keeps current physical acquisition evidence. + MachineDocument doc;const auto shaft=doc.add(MachinePartKind::Shaft).id;doc.find(shaft)->shaft.initial_speed=10; + const auto sensor=doc.add_sensor(MachineSensorKind::Crank,shaft).id; + auto& g=doc.logic;const auto source=g.add(LogicNodeKind::Sensor).id;g.find(source)->sensor=sensor;g.find(source)->sensor_kind=MachineSensorKind::Crank; + const auto delay=g.add(LogicNodeKind::NumberDelay).id;g.find(delay)->quantity=SignalQuantity::Angle;g.connect({source,"angle"},{delay,"in"}); + const auto gain=g.add(LogicNodeKind::GainOffset).id;g.find(gain)->quantity=SignalQuantity::Angle;g.find(gain)->gain=1;g.find(gain)->offset=0;g.connect({source,"angle"},{gain,"in"}); + MachineSimulation machine(doc,1000);auto state=machine.sense(sensor); + check(state.outputs[0].timing&&state.outputs[0].timing==state.outputs[1].timing,"Physical crank channels lack shared evidence"); + MachineLogicRuntime runtime(doc,[&](std::uint32_t id){return machine.sense(id);});runtime.evaluate(0,.001); + check(bool(ecu_crank_sample(runtime.value({source,"speed"}),runtime.value({source,"angle"}),0)),"Graph dropped physical crank evidence"); + check(!runtime.value({gain,"out"}).timing,"Arithmetic kept timing authority even with identity calibration"); + machine.process();runtime.evaluate(machine.snapshot().time,.001); + const auto delayed=runtime.value({delay,"out"});check(delayed.timing&&delayed.timestamp!=delayed.timing->sampled_at,"Delay rewrote acquisition time"); + check(!ecu_crank_sample(runtime.value({source,"speed"}),delayed,machine.snapshot().time),"Delayed angle accepted as current crank timing"); + { + SensorEcuConfig config;config.strategy=SensorEcuConfig::Strategy::AlphaN; + config.fuel_rpm={0,6000};config.fuel_tps={0,1};config.fuel_per_cycle={0.000002,0.00001,0.000006,0.00003}; + SensorEcu ecu(config);Pickup mid(0,0,3000*period/60); + auto result=ecu.sample(mid.speed,mid.angle,SignalValue::number(.5,0),0); + check(!result.sensor_fault&&std::abs(result.fuel_per_cycle-.000012)<1e-12,"Alpha-N did not interpolate RPM/TPS table in kg/cycle"); + check(std::abs(result.fuel_mass_flow-.0003)<1e-12,"Four-stroke per-cylinder kg/cycle to kg/s conversion failed"); + Volume0D intake(.001f);FuelInjectorConfig rail;rail.supply=FuelInjectorConfig::Supply::IdealRail;FuelInjector delivery(rail); + delivery.deliver(intake,SignalValue::number(result.fuel_mass_flow,0),0,.02); + check(std::abs(intake.fuel_mass()-.000006)<1e-11,"Alpha-N demand failed to reach the physical gas host"); + Pickup next(.01,period,mid.speed.timing->angular_speed); + result=ecu.sample(next.speed,next.angle,SignalValue{},.01); + check(result.sensor_fault&&!result.spark_events&&!result.fuel_mass_flow,"Missing required TPS retained actuator commands"); + result=ecu.sample(next.speed,next.angle,SignalValue::number(.5,.01),.01);check(!result.sensor_fault&&!result.spark_events,"Recovery replayed old spark crossings"); + Pickup limited(.02,2*period,7100*period/60);result=ecu.sample(limited.speed,limited.angle,SignalValue::number(1,.02),.02); + check(result.rev_limited&&!result.sensor_fault&&!result.spark_events&&!result.fuel_mass_flow,"Rev limiter failed to cut fuel and spark"); + Pickup held(.03,3*period,6900*period/60);result=ecu.sample(held.speed,held.angle,SignalValue::number(1,.03),.03);check(result.rev_limited,"Limiter omitted hysteresis"); + Pickup resumed(.04,4*period,6700*period/60);result=ecu.sample(resumed.speed,resumed.angle,SignalValue::number(1,.04),.04);check(!result.rev_limited&&result.fuel_mass_flow>0&&result.spark_events==1,"Limiter failed to resume cleanly"); + auto bad=config;bad.fuel_rpm={100,100};bool rejected=false;try{SensorEcu invalid(bad);}catch(const std::invalid_argument&){rejected=true;}check(rejected,"Non-increasing fuel table axis accepted"); + SensorEcu ignition(SensorEcuConfig{});result=ignition.sample(mid.speed,mid.angle,SignalValue{},0);check(!result.sensor_fault&&!result.fuel_mass_flow,"Ignition-only ECU demanded TPS or created fuel"); + } + { + Volume0D volume(.001f);const auto initial_mass=volume.mass(),initial_energy=volume.internal_energy(); + FuelInjectorConfig config;config.initial_fuel_mass=.00001;config.maximum_mass_flow=.001; + FuelInjector injector(config); + for(int i=0;i<16;++i)injector.deliver(volume,SignalValue::number(.01,i*.01),i*.01,.01); + const auto state=injector.state(); + check(state.saturated&&state.empty&&state.delivered_mass<=config.initial_fuel_mass,"Injector exceeded supply or omitted capacity saturation"); + check(std::abs(double(volume.fuel_mass())-state.delivered_mass)<1e-10&&std::abs(double(volume.mass()-initial_mass)-state.delivered_mass)<1e-9,"Injected fuel did not enter conserved host mass"); + const double expected_energy=state.delivered_mass*volume.gas_constant/(volume.gamma-1)*config.fuel_temperature; + check(std::abs(volume.internal_energy()-initial_energy-expected_energy)<1e-4,"Injector omitted sensible energy"); + check(volume.fuel_equivalent_fraction()>0,"Injector omitted transported fuel tracer"); + const auto before=volume.fuel_mass();auto fault=SignalValue::number(.001,1);fault.validity=SignalValidity::Fault; + check(injector.deliver(volume,fault,1,.01).command_fault&&volume.fuel_mass()==before,"Faulting command delivered fuel"); + FuelInjectorConfig ideal;ideal.supply=FuelInjectorConfig::Supply::IdealRail;FuelInjector continuous(ideal); + continuous.deliver(volume,SignalValue::number(.001,1),1,.01);check(continuous.state().mass_flow>0&&!continuous.state().empty,"Declared ideal rail failed to supply fuel"); + const auto held=volume.fuel_mass();continuous.deliver(volume,SignalValue::number(.001,1),2,.01);check(continuous.state().command_fault&&volume.fuel_mass()==held,"Stale command retained injector flow"); + } + { + Crankshaft crank;FourStrokeCylinder cylinder;cylinder.transported_fuel=true;cylinder.initialize(crank); + cylinder.fuel_mass=.000001f;cylinder.air_mass=cylinder.fuel_mass*14.7f;cylinder.fuel_equivalent_mass=cylinder.fuel_mass;cylinder.fuel_injected=true; + cylinder.heat_transfer=0;cylinder.energy_variation=0; + cylinder.ignition_command=FourStrokeCylinder::IgnitionCommand{false,false}; + // Cross the legacy internal spark coordinate without a wired event. + crank.previous_angle=(720-cylinder.spark_advance-1)*pi/180;crank.angle=(720-cylinder.spark_advance+1)*pi/180; + cylinder.pre_step(crank,.0001f,0);check(!cylinder.combustion_active,"External timing fell back to hidden cylinder phase"); + // Fire at a different physical angle: event authority is the wire. + crank.previous_angle=crank.angle=0;cylinder.ignition_command=FourStrokeCylinder::IgnitionCommand{true,false}; + cylinder.pre_step(crank,.0001f,0);check(cylinder.combustion_active&&!cylinder.ignition_command->fire,"External pulse failed to ignite or was not consumed"); + const auto initial_fuel=cylinder.fuel_mass; + for(int i=0;i<100;++i){crank.previous_angle=crank.angle;crank.angle+=pi/180;cylinder.pre_step(crank,.0001f,0);} + check(!cylinder.combustion_active&&cylinder.fuel_mass0;}} + check(fuel,"Sensor engine example never transported fuel to cylinder");check(fire,injection?"Alpha-N example never combusted":"Carburetor ignition example never combusted"); + const auto before_tuning=engine.snapshot();double old_advance=0,old_delivered=0;for(const auto& p:before_tuning.parts){if(p.id==ecu)old_advance=p.spark_advance;if(p.id==host)old_delivered=p.injector_delivered;} + auto tuned=doc;for(auto& point:tuned.find(ecu)->ecu.ignition_map)point.degrees_btdc+=5;engine.apply_tuning(tuned);check(engine.snapshot().time==before_tuning.time,"Ignition tuning restarted engine time");engine.process();for(const auto& p:engine.snapshot().parts){if(p.id==ecu)check(std::abs(p.spark_advance-old_advance-5)<.001,"Sensor ECU ignored live ignition calibration");if(injection&&p.id==host)check(p.injector_delivered>old_delivered,"Live ignition tuning reset injector delivery history");} + const auto path=std::filesystem::temp_directory_path()/("fluidsim-ecu-"+std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())+".machine.toml");save_machine(doc,path);const auto restored=load_machine(path);std::filesystem::remove(path); + check(restored.logic==doc.logic&&restored.find(ecu)->sensor_ecu==doc.find(ecu)->sensor_ecu,"ECU connector or fuel calibration failed save/load"); + if(injection)check(restored.find(host)->injector==doc.find(host)->injector,"Injector attachment failed save/load"); + auto wrong=doc;const auto foreign=wrong.add(MachinePartKind::Shaft).id;wrong.find(foreign)->shaft.initial_speed=3000*2*pi/60;for(auto& sensor:wrong.sensors)if(sensor.kind==MachineSensorKind::Crank)sensor.target.part=foreign; + MachineSimulation other(wrong,22050);for(int i=0;i<32;++i)other.process();for(const auto& p:other.snapshot().parts){if(p.id==cylinder)check(p.ignition_cut&&!p.combustion,"ECU accepted timing from a different engine shaft");if(injection&&p.id==host)check(p.fuel_flow==0,"Wrong-shaft timing still delivered fuel");} + auto unbound=doc;std::uint32_t connector=0;for(const auto& node:unbound.logic.nodes)if(node.kind==LogicNodeKind::Ecu)connector=node.id;unbound.logic.disconnect({connector,"angle"});bool rejected=false;try{MachineSimulation invalid(unbound);}catch(const std::invalid_argument&){rejected=true;}check(rejected,"Missing required ECU angle wire accepted"); + auto fake=doc;fake.logic.disconnect({connector,"speed"});fake.logic.disconnect({connector,"angle"});const auto numeric_speed=fake.logic.add(LogicNodeKind::NumberConstant).id,numeric_angle=fake.logic.add(LogicNodeKind::NumberConstant).id;fake.logic.find(numeric_speed)->quantity=SignalQuantity::AngularSpeed;fake.logic.find(numeric_speed)->value=3000*2*pi/60;fake.logic.find(numeric_angle)->quantity=SignalQuantity::Angle;fake.logic.connect({numeric_speed,"out"},{connector,"speed"});fake.logic.connect({numeric_angle,"out"},{connector,"angle"});MachineSimulation no_pickup(fake,22050);for(int i=0;i<32;++i)no_pickup.process();const auto faulted=no_pickup.snapshot();for(const auto& p:faulted.parts){if(p.id==ecu)check(p.sensor_fault&&p.ignition_cut,"Typed numbers acquired physical timing authority");if(injection&&p.id==host)check(p.injector_delivered==0,"Fake timing created injected fuel");}for(const auto& n:faulted.logic)if(n.id==connector)check(!n.problem.empty(),"Timing rejection was invisible in graph diagnostics"); + std::vector ids;for(const auto& p:doc.parts)ids.push_back(p.id);const auto assembly=machine_extract_assembly(doc,ids,"Sensor engine assembly");MachineDocument copy;machine_insert_assembly(copy,assembly,{});MachineSimulation copied(copy,22050);copied.process();check(copy.logic.nodes.size()==doc.logic.nodes.size(),"Assembly lost ECU harness"); + if(injection){ + auto removed=doc;removed.remove(ecu);check(removed.find(host)->injector->ecu==0&&!removed.find(host)->injector->ecu_label.empty(),"Deleted ECU left an accidental injector binding");const auto partial=machine_extract_assembly(doc,{host},"Injector host");check(partial.find(host)->injector->ecu==0,"Partial assembly retained external injector ECU ID"); + auto routed=doc;const auto source=std::find_if(routed.logic.nodes.begin(),routed.logic.nodes.end(),[](const auto& n){return n.kind==LogicNodeKind::Sensor&&n.sensor_kind==MachineSensorKind::Crank;})->id; + const auto choose=routed.logic.add(LogicNodeKind::Switch).id,constant=routed.logic.add(LogicNodeKind::NumberConstant).id,selector=routed.logic.add(LogicNodeKind::Select).id;routed.logic.find(constant)->quantity=routed.logic.find(selector)->quantity=SignalQuantity::Angle; + routed.logic.disconnect({connector,"angle"});routed.logic.connect({source,"angle"},{selector,"a"});routed.logic.connect({constant,"out"},{selector,"b"});routed.logic.connect({choose,"out"},{selector,"select"});routed.logic.connect({selector,"out"},{connector,"angle"}); + MachineSimulation controlled(routed,22050);for(int i=0;i<10000;++i)controlled.process();controlled.set_logic_control(choose,true);for(int i=0;i<1000;++i)controlled.process();for(const auto& p:controlled.snapshot().parts){if(p.id==cylinder)check(p.ignition_cut&&!p.combustion,"Lost crank timing failed to stop controlled combustion");if(p.id==host)check(p.fuel_flow==0,"Lost crank timing retained fuel delivery");} + controlled.set_logic_control(choose,false);bool recovered=false;for(int i=0;i<10000;++i){controlled.process();if(i%16==0)for(const auto& p:controlled.snapshot().parts)if(p.id==cylinder)recovered|=p.combustion;}check(recovered,"Recovered physical crank timing failed to restart combustion"); + auto exhausted=doc;exhausted.find(host)->injector->initial_mass=.0000001f;MachineSimulation dry(exhausted,22050);for(int i=0;i<64;++i)dry.process();for(const auto& p:dry.snapshot().parts)if(p.id==host)check(p.injector_empty&&p.fuel_flow==0&&p.injector_delivered<=exhausted.find(host)->injector->initial_mass,"Physical injector ignored finite fuel budget"); + } + } + std::cout<<"Sensor-driven ignition/Alpha-N, saved machine bindings, physical combustion and injector delivery passed\n"; + return 0; +}catch(const std::exception& error){std::cerr< +#include +#include +#include +#include +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)});} +templatevoid 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(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;i0;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 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< +namespace fluidsim::port::bench { +inline void set_rpm(MachineDocument& design,float rpm){ + const auto shaft=machine_primary_shaft(design);if(!shaft)throw std::invalid_argument("Benchmark needs a primary shaft"); + const auto position=design.find(shaft)->position;design.find(shaft)->shaft.initial_speed=rpm*2*pi/60; + for(auto& part:design.parts)if(part.kind==MachinePartKind::DriveMotor&&machine_shaft_for(design,part.id)==shaft){part.drive={rpm*2*pi/60,0,true,true};return;} + auto& drive=design.add(MachinePartKind::DriveMotor,machine_math::add(position,{-.3f,0,0}));const auto id=drive.id;drive.name="Benchmark speed drive";drive.drive.target_speed=rpm*2*pi/60;design.connect({shaft,0},{id,0}); +} +} diff --git a/tools/machine_benchmark.cpp b/tools/machine_benchmark.cpp new file mode 100644 index 0000000..57fc8be --- /dev/null +++ b/tools/machine_benchmark.cpp @@ -0,0 +1,32 @@ +#include "MachineBench.hpp" +#include "FluidSim/Machine.hpp" +#include "FluidSim/Scenario.hpp" +#include "FluidSim/ScenarioCatalog.hpp" +#include +#include +#include +#include +#include +using namespace fluidsim::port; +template double measure(Step step,int frames){ + const auto begin=std::chrono::steady_clock::now(); + for(int i=0;i(std::chrono::steady_clock::now()-begin).count(); +} +int main(int argc,char** argv)try{ + const int frames=argc>1?std::stoi(argv[1]):32768;const float rpm=argc>2?std::stof(argv[2]):3000,pedal=argc>3?std::stof(argv[3]):.35f; + if(frames<1||!std::isfinite(rpm)||rpm<0||rpm>50000||!std::isfinite(pedal)||pedal<0||pedal>1)throw std::invalid_argument("Usage: fluidsim_machine_benchmark [frames>0] [rpm 0..50000] [pedal 0..1]"); + std::cout<<"scenario,runtime,frames,rpm,pedal,cells,min_cell_mm,substeps_per_frame,us_per_frame,wall_per_sim,positivity_repairs\n"; + for(const char* id:{"single-450","twin","flat6","v8-h"}){ + const auto& preset=scenario_example(id);FourStrokeScenario native(44100,preset);native.set_controls(pedal,0,1); + const auto step=[&]{native.crankshaft.omega=rpm*2*pi/60;return native.process();}; + for(int i=0;i<512;++i)step();const auto before=native.snapshot().solver_substeps;const double elapsed=measure(step,frames);const auto after=native.snapshot(); + float shortest=1e9f;unsigned cells=0;for(auto& p:native.pipes){cells+=unsigned(p.cells.size());shortest=std::min(shortest,p.minimum_cell_length());} + std::cout< +#include +#include +#include +using namespace fluidsim::port; +using namespace fluidsim::port::desktop; +int main(int argc,char** argv)try{ + const int frames=argc>1?std::stoi(argv[1]):120; + const bool play=argc>2&&std::string(argv[2])=="--play"; + const double speed=argc>3?std::stod(argv[3]):1.; + if(!std::isfinite(speed)||speed<.01||speed>1)throw std::invalid_argument("Playback speed must be 0.01..1"); + if(frames<1)throw std::invalid_argument("frame count must be positive"); + sf::ContextSettings context;context.depthBits=24;context.majorVersion=2;context.minorVersion=1; + sf::RenderWindow window(sf::VideoMode({1440,900}),"Workshop render benchmark",sf::Style::Default,sf::State::Windowed,context); + window.setVisible(false);window.setVerticalSyncEnabled(false); + sf::Font font;bool loaded=false; + for(const auto* path:{"C:/Windows/Fonts/consola.ttf","/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"})if(std::filesystem::exists(path)&&font.openFromFile(path)){loaded=true;break;} + std::cout<<(play?"scenario,speed,frames,wall_seconds,sim_seconds,sim_per_wall,underruns,buffered\n":"scenario,frames,ms_per_frame,render_fps\n"); + for(const char* id:{"flat6","v8-h"}){ + MachineBuilder builder;auto design=machine_from_engine(scenario_example(id));if(play){bench::set_rpm(design,3000);unsigned cylinder=0,pipe=0;for(const auto& p:design.parts){if(!cylinder&&p.kind==MachinePartKind::Cylinder)cylinder=p.id;if(p.name=="Exhaust atmosphere")for(const auto& link:design.links){const auto other=link.a.part==p.id?link.b.part:link.b.part==p.id?link.a.part:0;if(const auto part=design.find(other);part&&part->kind==MachinePartKind::Pipe)pipe=other;}} + design.probes={{1,cylinder,"Cylinder pressure",MachineQuantity::Pressure},{2,pipe,"Downpipe lambda",MachineQuantity::Lambda},{3,cylinder,"Fresh charge",MachineQuantity::ChargeAfr},{4,pipe,"Downpipe AFR",MachineQuantity::AirFuelRatio},{5,pipe,"Downpipe flow",MachineQuantity::MassFlow},{6,0,"Shaft RPM",MachineQuantity::Rpm},{7,0,"Shaft torque",MachineQuantity::Torque},{8,0,"Shaft power",MachineQuantity::Power}};} + builder.set_document(std::move(design));builder.set_test_speed(speed); + if(play){window.setFramerateLimit(60);builder.start_test();if(!builder.testing())throw std::runtime_error("Could not start workshop playback");builder.handle_event(sf::Event::KeyPressed{.code=sf::Keyboard::Key::M},window.getSize()); + for(int i=0;i<16;++i){builder.update();builder.draw(window,loaded?&font:nullptr);window.display();} + const auto before=builder.playback_stats();const auto begin=std::chrono::steady_clock::now(); + for(int i=0;i(std::chrono::steady_clock::now()-begin).count(); + std::cout<(std::chrono::steady_clock::now()-begin).count(); + std::cout<