# Engine definitions Each `engines/*.toml` file defines one engine and its default vehicle/load calibration. Files are loaded once when the program starts. Add a file, give it a unique `id`, then restart to see it in the browser. Editing a definition does not require recompiling the engine simulator. Start by copying a similar engine, such as `single-450.toml`, `five.toml`, `v8-h.toml` or `yz-racing.toml`. Existing definitions contain their effective calibration values, including the previous carb and idle tunes. ```toml schema_version = 2 id = "my-single" name = "My four-stroke single" category = "Singles / four-stroke" description = "My calibration" order = 100 cycle = "four_stroke" [simulation] cells_per_meter = 20.0 ``` `id`, `name`, `category`, `cycle` and `schema_version` are required. IDs use lowercase letters, digits, `-` and `_`. `order` controls menu order; ties sort by ID. Duplicate IDs, unknown keys, unsupported versions, incorrect types and invalid physical values stop loading with a diagnostic identifying the file and field. TOML syntax errors also report their location. Version 2 requires explicit physical geometry. Version 1 remains readable and receives a generated layout for compatibility. All bundled scenarios use version 2. ## Physical geometry Pipe centerlines and storage boxes are shared by simulation, rendering, mouth positions, and junction directions. Coordinates use millimetres, with Y up. Pipe indices follow the expanded pipe-group ordering below. ```toml [[geometry.pipes]] index = 0 points_mm = [[-200.0, 300.0, 0.0], [-100.0, 300.0, 0.0], [-100.0, 200.0, 0.0]] [[geometry.volumes]] id = "intake_plenum" size_mm = [200.0, 100.0, 50.0] center_mm = [0.0, 200.0, 0.0] [[geometry.cylinders]] origin_mm = [0.0, 0.0, 0.0] # crank axis origin tilt_deg = 0.0 # rotation about X ``` That pipe is 200 mm long: its length is the sum of the two straight segments. There is no independent pipe-length field in version 2 definitions. Mesh cells are distributed along the measured centerline; hard corners remain hard corners in both views. The 3D tube uses bounded miter joins rather than a spline. Paths require 2..64 finite points without consecutive duplicates. The box above is 1 litre. Storage volumes support either `size_mm` with optional `center_mm` (the center defaults to the origin), or two opposite corners: ```toml [[geometry.volumes]] id = "intake_plenum" corners_mm = [[-100.0, 150.0, -25.0], [100.0, 250.0, 25.0]] ``` Use one form per box. Corner order can be reversed. Zero or negative sizes, duplicate IDs, missing required volumes, or mismatched pipe/cylinder counts are rejected. Four-stroke storage IDs are `intake_plenum`, `exhaust_collector` for a volume collector, and `junction_0`, `junction_1`, etc. in topology order. Two-strokes use `intake_plenum` and `crankcase`; the latter defines crankcase clearance volume, and the renderer adds the live piston displacement to its box. Only active fluid storage components are drawn as pressure-colored boxes. Cylinder gas volume comes from bore, stroke, rod length, compression, and piston position; cylinder placement supplies the crank origin and bank tilt. The 3D cutaway uses these dimensions for sleeves, pistons, rods, heads, and valves. All 32 bundled layouts have explicit cylinder placements and routed plumbing. The four-stroke headers use direct swept bends and realistic collector placement, with new lengths measured from their geometry rather than folded detours to retain the old numerical lengths. This intentionally changes exhaust tuning. See [header length changes](../verification/geometry-header-lengths.csv). Intake, tailpipe, crossover and two-stroke profile lengths retain their existing dimensions. Smooth manufactured bends are explicitly sampled centerlines; authored hard corners still use miter joins. Profiled two-stroke exhaust and silencers retain their sectional dimensions. Their axial section proportions are mapped onto the measured centerline, so changing the overall route scales section lengths while preserving diameters. Silencer `diameter_mm` and `length_mm` define its cylindrical casing size; straight-through gas uses the separate core diameter. Rendering shows the gas profile and a cutaway casing. Bends do not add a new distributed bend-loss model. The existing named engine topologies still define connections; arbitrary graph editing is a future layer over this shared geometry. ## File discovery and distribution The first available directory is used: 1. `FLUIDSIM_ENGINE_DIR`, when set. A missing override directory is an error. 2. `engines/` under the working directory. 3. `engines/` beside the executable on Windows and Linux. 4. The source checkout's `engines/` directory as a development fallback. Definitions are cached for the process lifetime. The selected directory is loaded as one catalog; it is not merged with the bundled files. In a repository working directory, source definitions take priority over build copies. CMake synchronizes the definitions into the build output when building an executable. Distribute that `engines/` directory with the executable. The TOML parser is vendored, so loading definitions needs no network connection. ```powershell $env:FLUIDSIM_ENGINE_DIR = "C:\MyEngines" .\fluidsim_desktop.exe --example my-single ``` ## Units and sections File keys state dimensional units. `_mm` is millimetres, `_litres` is litres, `_cc` is cubic centimetres, `_deg` is crank degrees, `_s` is seconds, `_nm` is newton metres and `_kg_m2` is rotational inertia. The loader converts lengths and volumes to SI units. Openings and mixture/taper multipliers are normalized fractions, not percentages: `0.03` means 3%. Four-stroke definitions use these sections: | Section | Contents | | --- | --- | | `[engine]` | Cylinder count, layout, bank angle, phases, throttle, startup, load, and exhaust topology | | `[cylinder]` | Bore/stroke/rod, compression, valve diameters/counts, combustion and heat transfer | | `[cam.intake]`, `[cam.exhaust]` | Cam lift curves as `[angle_deg, lift_mm]` points over a 720 degree cycle | | `[valves.intake]`, `[valves.exhaust]` | Valve mass, spring, damping, seat preload, travel and contact restitution | | `[valve_sound]` | Contact resonance and radiating area for four-stroke valve impacts | | `[crankshaft]` | Inertia and mechanical friction | | `[ecu]` | Idle, actuator response, ignition curve, limiter and overrun thresholds | | `[vehicle]` | Vehicle mass, gears, clutch, wheel geometry and road resistance | | `[fuel]` | `system = "injection"` or `"carburetor"`, jet diameters and taper/film calibration | | `[junctions]` | Fitting volume and H crossover selection | | `[silencers]` | Connecting pipes, resonator and muffler profiles, when used | | `[[pipes]]` | Ordered pipe groups for the selected exhaust topology | | `[[geometry.pipes]]`, `[[geometry.volumes]]`, `[[geometry.cylinders]]` | Physical routes, storage sizes and component placements | `engine.phase_degrees` contains one phase per cylinder in cylinder index order, within `[0,720)`. It records the actual crank offsets rather than a firing-order label. `ecu.ignition_map` is an array of `[RPM, degrees BTDC]` pairs with strictly increasing RPM. Negative advance means ignition after TDC. Four-stroke valve lift is driven by the cam profile and a one dimensional mass-spring model. Cam points must cover 0 through 720 degrees, have strictly increasing angles, and start and end at zero lift. A valve can separate from the cam at high RPM, producing valve float; seat, cam and travel-stop impacts excite a damped contact mode spatialized from the engine through the existing stereo, ear-distance and travel-delay paths. These profiles and dynamics apply only to four-stroke cylinders. Valve masses are per valve; the model scales effective mass, spring force and contact impulse by the configured valve count. For a carbureted four-stroke, use `ecu.idle_control = "fixed"` and set `ecu.idle_base_opening`. Its limiter and overrun control cut ignition while the carburetor continues supplying fuel. Jet diameters in `[fuel]` are effective diameters after the previous displacement and jet multipliers have been applied. Two-stroke definitions use `[two_stroke]`, `[ecu]` and `[vehicle]`. `two_stroke.idle_slide_opening` sets the mechanical stop; `idle_spark_gain` and `idle_minimum_advance_deg` set its idle spark trim. Two-stroke jet multipliers retain their existing meaning. `chamber_sections_mm` contains six `[length, inlet diameter, outlet diameter]` sections: header, two diffusers, belly, baffle cone and stinger. Required cylinder geometry must be explicit. Optional calibration fields use the typed config defaults; a misspelled field is always an error. Existing files include explicit values so their behavior does not depend on future defaults. ## Scenario mesh resolution Every bundled scenario defines a shared pipe density, for example: ```toml [simulation] cells_per_meter = 20.0 ``` This applies to all intake, runner, header, tailpipe, silencer and two-stroke profile sections. Uniform pipes use `max(1, ceil(length_m * cells_per_meter))`; profiled pipes subdivide each geometry section at that maximum cell length. Geometry and integrated pipe volumes are preserved. Tiny floating-point rounding at exact integer counts is ignored. Legacy per-pipe `cells` and exhaust cell length fields remain readable for older definitions without a global density; when density is present it controls the mesh. Valid density is 1..500 cells/m, with existing per-pipe and network cell caps also enforced. Higher resolution reduces numerical diffusion and costs more CPU. In the desktop sidebar, **Cells/m - / +** changes density in steps of five. The mesh builds in the background and the engine restarts at its configured RPM; solver, scene, listener and driving-control inputs are retained. Each scenario's selection is remembered for the current app session. These controls do not write TOML; edit `cells_per_meter` in the scenario file for a permanent default. The desktop cam/valve, fuel and geometry editor has been removed. Engine calibration remains available through TOML. Escape returns directly to the menu. ## Pipe groups and exhaust topologies These definitions configure the existing topology builders. They do not yet describe arbitrary graphs of cylinders and junctions. A group's `id` must be unique; `count` repeats its dimensions across adjacent pipes. ```toml [[pipes]] id = "primaries" count = 8 diameter_mm = 38.0 ``` Each expanded pipe gets its own `geometry.pipes` centerline. A group's diameter remains the cross-section geometry; its length is derived from that centerline. The order is significant because the topology builder connects the expanded groups to fixed cylinder and junction ports: | `engine.topology` | Pipe order after expansion | | --- | --- | | `collector` | Intake, N runners, N primaries, one tailpipe | | `staged` | Intake, N runners, N primaries, outlet pipe(s), secondary pipe(s) | | `american_v8` | Intake, 8 runners, 8 primaries, 4 secondaries, 2 bank collectors, 2 tailpipes, H crossover | The staged inline triple has one secondary; other staged layouts have two. Staged V engines have two independent outlets. The American topology requires eight cylinders in a V layout; setting `junctions.h_pipe = false` closes both ends of the unused crossover and leaves the banks independent. A collector outlet can instead use `profile = "silenced"`. Its dimensions and mesh then come from `[silencers]`, `[silencers.resonator]` and `[silencers.muffler]`; omit the group's uniform pipe dimensions. The loader checks expanded pipe counts and mesh limits before construction. Pipe positions and sound-source locations come from the explicit centerlines. ## Characteristic exhaust junctions Existing files default to the finite-volume model. To opt into nonlinear, zero-storage exhaust junctions, add: ```toml [junctions] model = "characteristic" # "volume" (default), "characteristic", or "legacy" turning_loss = 0.5 # dimensionless, 0..10; 0 gives ideal angle-independent coupling ``` This applies to exhaust header fittings, staged collectors, and H-pipe nodes. For `collector` topology it replaces the exhaust collector storage volume with a zero-storage node, including the two-port single-cylinder transition. That changes the physical model and removes collector compliance. Intake plenums, cylinders, silencers, and intentional pipe volumes remain unchanged. The storage dimensions come from the corresponding geometry boxes in version 2. `junction_volume_cc` is retained only for legacy version 1 layout generation. Branch directions point **outward from the node into the pipe**. By default they come from the first/last segment of the physical pipe centerline. Optional direction overrides remain available for measured fitting axes. For example: ```toml [[junctions.branches]] pipe = 2 left = false direction = [-1.0, 0.0, 0.0] [[junctions.branches]] pipe = 3 left = true direction = [1.0, 0.0, 0.0] ``` Directions are normalized, must be finite and nonzero, and must identify active node endpoints in characteristic/legacy mode. Pipe indices follow expanded scenario ordering, not TOML pipe-group indices. `left` is the endpoint at pipe coordinate zero; it does not prescribe flow direction. Reversal is automatic. Opposing outward axes describe straight-through flow; perpendicular axes describe a 90-degree turn. A common world rotation leaves the solution unchanged. For listening without editing TOML, desktop and capture accept `--junctions characteristic --junction-loss 0.5`; use `--junctions volume` for the original fittings. Restart the scenario between models. These switches apply to four-stroke scenarios; two-stroke exhaust is currently a profiled pipe. See [junction solver details and validation](JUNCTION_SOLVER.md).