Improve physical geometry, fluid solvers, audio, and profiling

This commit is contained in:
max
2026-10-30 18:57:56 +01:00
parent bf8621ad39
commit eb2b6366c2
130 changed files with 14649 additions and 862 deletions

110
docs/ACOUSTIC_BENCHMARK.md Normal file
View File

@@ -0,0 +1,110 @@
# Acoustic pulse benchmark
Run from the repository root:
```powershell
cmake --build --preset windows --target fluidsim_acoustic_benchmark
.\out\build\windows-gcc\fluidsim_acoustic_benchmark.exe out/acoustics
ctest --preset windows -R 'acoustic_benchmark|fluid_validation' --output-on-failure
```
`summary.csv` contains 408 cases. Each case also has a pressure/velocity probe
trace in CSV. No engine configuration or production solver behavior is changed.
## Cases and measurement
- A continuous 4 m pipe and an equivalent pair of directly connected 2 m pipes.
- Equal-area characteristic and legacy zero-storage three-way junctions, and an unequal-area characteristic junction (an idealized T/Y, without angles).
- Two-port and three-port finite-volume fittings, with 50, 500, and 5000 cc storage
and discharge coefficients 1.0 and 0.7. Opening area equals branch pipe area;
discharge variation represents restriction, not resolved entry geometry.
- Rusanov, HLLE, HLLC, and limited MUSCL-Hancock/HLLC; 40, 80, and 160 cells per 2 m; nominal Courant
settings 0.4 and 0.8. Solver CFL protection remains active. The requested
timestep uses reference sound speed plus a small wave-speed margin.
All pipes have area 0.001 m² and initially rest at 101325 Pa, 300 K. A 100 Pa
Gaussian pressure pulse, centered 0.6 m from the inlet with width parameter
0.12 m, is initialized as a right-going linear acoustic wave. Explicit pipe
damping and thermal relaxation are disabled.
The upstream probe is near x=1.4 m, and downstream probes are near x=2.6 m
(0.6 m into each outlet branch). Probe positions use containing cell centers.
The 9 ms integration ends before far-end reflections return to the probes.
For every probe, characteristics are `p± = (p' ± rho*c*u)/2`.
Acoustic pulse energy is the integral of `area * p±² / (rho*c)` over time.
Transmitted energy sums all outlet branches. Ratios divide by incident energy
at the upstream probe, so incident-path attenuation before that probe is excluded.
`transmission_over_straight` and `reflection_over_straight` divide the measured
ratios by transmission through the continuous pipe at matching mesh, scheme,
and timestep. This approximately removes shared propagation attenuation. Junction
filtering can change spectra, so this correction is not a complete de-embedding
of frequency-dependent pipe loss.
The ideal equal-area lossless three-way junction has reflected energy 1/9 and
total transmitted energy 8/9. Straight-pipe ideals are 0 and 1. Finite-volume
fittings have no constant analytic coefficient in this benchmark: their storage
and nozzle models introduce frequency and amplitude dependence.
`unaccounted_ratio` is acoustic energy missing from the measured outgoing pulse,
not a total conserved-energy error. It includes numerical attenuation, interface
loss, and any energy still stored or delayed beyond the measurement window.
It must not automatically be interpreted as physical dissipation.
Mass and total-energy drift, limited fluxes, positivity repairs, and substeps are
reported separately. Cases with limiting or outgoing acoustic energy above 1.05
times incident are flagged; they are not accepted as physical loss measurements.
The test fails for nonfinite results, positivity repairs, conservation drift
above 100 ppm, straight-pipe reflection above 0.0001, split-pipe mismatch, or a
failure of straight-pipe transmission to improve with mesh refinement at 0.4.
Flagged volume cases are retained as diagnostics rather than silently discarded.
## Initial findings
For Rusanov at nominal Courant 0.4:
| Cells per 2 m | Straight transmission | Three-way transmission / straight |
| --- | --- | --- |
| 40 | 0.6783 | 0.2215 |
| 80 | 0.7172 | 0.2219 |
| 160 | 0.7707 | 0.2222 |
The split pipe matches the continuous pipe. Straight-pipe attenuation decreases
with refinement, identifying spatial numerical diffusion as a contributor.
The legacy zero-storage junction's corrected transmission remains near 2/9 instead of
the ideal 8/9, indicating a separate interface problem that refinement does not
remove. Total mass and energy conservation alone does not establish correct
acoustic scattering.
The original three solvers flagged five coarse, high-timestep 50 cc three-port volume cases for flux
limiting or acoustic amplification. These show sensitivity to component storage
and explicit exchange timesteps; a pipe CFL limit alone does not validate that
coupling. A component timestep study should precede acoustic calibration of those
cases. Other cases passing robustness checks are not thereby physically validated.
These are small-signal, zero-mean-flow results for one pulse bandwidth. They do
not establish the dominant loss at every engine operating point. The single-450
and current V8 use storage volumes, so a zero-storage junction fix alone will not
address their sound. The next production-model investigation should address
pipe diffusion and finite-volume coupling stability; retain the zero-storage
scattering result as a separate junction regression target. Frequency sweeps,
phase comparison, mean-flow cases, and measured fitting data remain follow-up work.
## Production listening candidate
`muscl` is now a fourth selectable solver in the desktop, CLI, and capture tool.
Its straight-pipe energy transmission at Courant 0.4 is 0.7834, 0.8637, and
0.9446 for 40, 80, and 160 cells per 2 m, versus Rusanov's 0.6783, 0.7172,
and 0.7707. The benchmark also checks at least 0.05 improvement over Rusanov at
matching mesh/timestep. Split-pipe equivalence still passes without relaxed
thresholds. Adding MUSCL and the new characteristic/unequal-area comparisons brings the sweep to 408 cases, with seven flagged
small-volume cases; the component-coupling timestep issue is still open.
The new reconstruction applies to uniform pipe sections and matching direct
connections. Fittings, component boundaries, and profile transitions retain
first-order coupling. Production evaluation is opt-in (`--solver muscl` or F3);
Rusanov remains the startup default. Auto gain defaults off for listening.
The new characteristic solver corrects the legacy scattering error. Its corrected
transmission is about 8/9 for equal branches; unequal-area checks and transmitted
500/1000 Hz gain/phase checks are now included. See [junction solver details](JUNCTION_SOLVER.md).

View File

@@ -10,13 +10,16 @@ Start by copying a similar engine, such as `single-450.toml`, `five.toml`,
calibration values, including the previous carb and idle tunes.
```toml
schema_version = 1
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
@@ -25,6 +28,74 @@ 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:
@@ -59,8 +130,11 @@ Four-stroke definitions use these sections:
| Section | Contents |
| --- | --- |
| `[engine]` | Cylinder count, layout, bank angle, phases, intake volumes, throttle, startup, load, and exhaust topology |
| `[cylinder]` | Bore/stroke/rod, compression, valve geometry and timing, combustion and heat transfer |
| `[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 |
@@ -68,12 +142,22 @@ Four-stroke definitions use these sections:
| `[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
@@ -90,6 +174,34 @@ 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
@@ -101,10 +213,11 @@ unique; `count` repeats its dimensions across adjacent pipes.
id = "primaries"
count = 8
diameter_mm = 38.0
length_mm = 750.0
cells = 15
```
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:
@@ -123,4 +236,52 @@ 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
continue to use the shared scene routes for rendering and sound sources.
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).

98
docs/JUNCTION_SOLVER.md Normal file
View File

@@ -0,0 +1,98 @@
# Direction-aware characteristic junctions
## Model and scope
The new `BoundarySystem` node solves a nonlinear common-pressure mass balance
using each branch's outgoing Euler acoustic wave curve: an isentropic
rarefaction or Rankine-Hugoniot shock curve. Branch area enters mass flow,
and pipe-coordinate orientation is handled independently of physical direction.
Incoming total enthalpy and species are mixed by incoming mass flux. Outgoing
contact density follows the mixed total enthalpy and the local pressure/velocity.
Outgoing fluxes share that enthalpy and composition, conserving total energy,
air, and fuel at the zero-storage node. Fitting forces react momentum; axial
momentum is not summed as a scalar across differently directed branches.
Pressure is found by a bracketed Newton iteration. Setup allocates scratch
arrays; node updates allocate no memory. Outgoing mass is normalized to incoming
mass to remove root roundoff. A shared donor budget scales advective fluxes when
necessary, preserving node conservation, and reports `junction_limited_fluxes`.
This is a subsonic, common-pressure engineering closure. It has not been proven
entropy stable for arbitrary mixtures, large thermal contrasts, and shocks.
Endpoint Mach numbers at or above 0.95, predicted face Mach numbers at or above
0.98, and unsuccessful pressure solves select the conservative legacy Rusanov
fallback. `junction_fallbacks` reports these cases. It is not a choked/supersonic
multi-port Riemann solver. Frequent fallback or limiting indicates a case needing
further timestep/model investigation, not a validated acoustic measurement.
The broader research on entropy-preserving Euler junctions uses common total
enthalpy and entropy-based coupling; the implemented common-pressure/mixed-enthalpy
closure is a distinct engineering choice. See
[Lang and Mindt, 2018](https://www.aimsciences.org/article/doi/10.3934/nhm.2018008)
for the stronger mathematical coupling conditions and their subsonic assumptions.
## Direction and turning
Each endpoint can specify a unit axis pointing out of the node. For a receiving
branch, the loss coefficient is the incoming-mass-weighted average of
`turning_loss * (1 + dot(outward_receiver, outward_donor)) / 2`.
Opposite axes have zero turning loss; perpendicular axes have half the configured
loss. A local signed pressure-drop relation uses `K * rho * u² / 2`, with three
outer updates of the donor-weighted direction coefficients. Donors are recomputed
as flow reverses. This is a quasi-steady uncalibrated turning-loss approximation.
It does not resolve vortices, bend radii, separation, or transverse acoustic modes.
At zero mean flow, ideal low-frequency compact-junction scattering depends on
branch impedances, not a stereo direction or a branch-angle audio filter. Angles
enter the mean-flow/turning model. Defaults use schematic route tangents, which
can be overridden with actual branch axes in TOML. No 3D CFD is involved.
## Production selection
```powershell
.\out\build\windows-gcc\fluidsim_desktop.exe --example v8-h --solver rusanov --junctions characteristic --junction-loss 0.5 --scene dry
```
Use `--junctions volume` for the original finite-volume fittings or
`--junctions legacy` for the old zero-storage numerical node. The volume model
remains the default. Pipe solvers and junction models are independent. Restart
for model comparisons; auto gain remains off by default.
The same options work in `fluidsim_audio_capture`. Captures report junction
iterations, fallback, limiting, and pipe positivity repairs. On collector-topology
engines, selecting characteristic/legacy explicitly replaces collector storage
with a node. Changes there include removal of that physical compliance, so they
cannot all be attributed to numerical accuracy. The V8 and staged exhaust paths
also change from storage fittings to zero-storage nodes.
See [the configuration guide](ENGINE_CONFIG.md#characteristic-exhaust-junctions)
for branch direction overrides and endpoint indexing.
## Validation
`fluidsim_junction_tests` checks mass, total energy, air/fuel conservation,
unequal areas and thermal states, opposite donor configurations, all-pipe
coordinate reversal, world rotation, straight-versus-turning flow preference,
invalid direction rejection, and explicit supersonic fallback reporting.
The 408-case acoustic benchmark compares volume fittings, characteristic nodes,
and legacy nodes across all four pipe schemes, meshes, and timesteps. Equal-area
characteristic scattering approaches reflected energy 1/9 and transmitted energy
8/9 after approximately removing common pipe attenuation. With outlet areas
2A and 3A and inlet area A, the targets are 4/9 reflected and 5/9 transmitted.
Characteristic pulse cases require no fallback. At the finest mesh, transmitted
pressure gain and phase at 500 and 1000 Hz are compared against ideal equal-area
gain 2/3 and zero extra phase, after straight-pipe de-embedding.
For Rusanov, 160 cells per 2 m, Courant 0.4, corrected transmission is about
0.889 with the characteristic node versus 0.222 with the legacy node. Seven
small-volume cases remain flagged in the separate volume model. Full measured
junction spectra, geometry calibration, high-Mach coupling, and entropy
admissibility remain follow-up work.
Listening captures are in `out/audio/junction-ab-single-{volume,characteristic}-rendered.wav`
and `out/audio/junction-ab-v8-{volume,characteristic}-rendered.wav`. Their
`-level-matched.wav` copies use RMS 0.08 with constant per-file gain. The single
comparison is 3000 RPM / pedal 0.3; V8 is 3000 RPM / pedal 0.5 with turning-loss
coefficient 0.5 in the characteristic case. Both use dry Rusanov rendering.
These comparisons include the change in storage model described above.