all of the changes

This commit is contained in:
2026-10-23 18:29:51 +02:00
parent 4c435fdd76
commit 5607dcf2b5
73 changed files with 4365 additions and 2059 deletions

View File

@@ -1,63 +0,0 @@
# Carburetor work in progress
Resumed on 2026-10-23 to fix throttle-release crashes and remove scheduled pops.
All changes are uncommitted. The historical results below predate that revision.
Current revision: bounded slide aperture to avoid negative roundoff near its
seat; removed the hardcoded firing intervals and forced late spark. Exhaust
combustion was subsequently removed at the user's request due to lag and limited
audible benefit. Fuel remains transported through pipes without reacting.
Cylinder combustion consumes air in proportion to burned fuel.
Warm-running startup now seeds fuel in the intake, crankcase and cylinder.
Pilot effective diameter is .40 mm with .08 base/.3 maximum idle slide opening.
The revised transient passes: peak 12954 RPM, idle 2583 RPM, peak audio .0985.
## Implemented
- Added `Carburetor`, with effective pilot, needle and main jet restrictions.
- Jet flow uses simulated air mass flow, air density, venturi dynamic pressure,
static pressure drop, fuel density, discharge coefficients and float-bowl
supply. It is not a cylinder-side target-AFR calculation.
- Added wall-film deposition and evaporation during throttle transients.
- Added fuel as a conserved transported species to pipe cells, boundaries,
0-D volumes, crankcase, cylinder and turbo internal transfers.
- Carb fuel enters the intake plenum, then moves through the reed, crankcase,
transfer pipe and cylinder with the simulated gas flow.
- Cylinder heat release is limited by transported fuel, trapped oxygen and a
basic 7:1--24:1 flammability window. Rich excess remains unburned.
- Disabled electronic overrun fuel cut for the two-stroke. A deterministic
sparse late-fire cadence overlaps exhaust-port opening to make separated
coast-down bangs through the pressure/audio solver; no sound sample is mixed.
- Added fuel conservation and downstream transport tests, carb bounds/cadence
checks, transient firing-count checks, benchmark AFR columns and desktop carb
labeling.
## Last verified state
- Before the final throttle-slide geometry change, the entire 10-test CTest
suite passed.
- That transient reached about 15,954 RPM, recovered to about 3,151 RPM, and
produced 11 late firings in the first second after release with no clipping
or pipe positivity repairs.
- After replacing the old butterfly-area formula with a circular-segment slide
aperture and reducing the idle slide calibration, the project and desktop
rebuilt successfully and `fluidsim_two_stroke_physics` passed.
## Current task / next step
The `fluidsim_two_stroke_regression` run after the slide-area change was stopped
mid-run when work was paused. Run it first, then run full CTest. If idle recovery,
mixture, or overrun firing count moved, tune `idle_base_opening`,
`idle_max_opening`, and the effective jet dimensions in `Carburetor.hpp`.
Re-run `fluidsim_two_stroke_benchmark` afterward and update the YZ125 verification
numbers if the slide geometry changed them.
The most relevant files are:
- `include/FluidSim/Carburetor.hpp`
- `src/carburetor.cpp`
- `src/two_stroke_scenario.cpp`
- `src/two_stroke_cylinder.cpp`
- `include/FluidSim/Fluid.hpp`
- `tests/two_stroke_physics.cpp`
- `tests/two_stroke_regression.cpp`

View File

@@ -7,7 +7,6 @@ set(FLUIDSIM_FONT_FILE "" CACHE FILEPATH "Optional font file copied beside the d
add_library(fluidsim_core add_library(fluidsim_core
src/carburetor.cpp src/carburetor.cpp
src/ecu.cpp src/ecu.cpp
src/turbocharger.cpp
src/engine.cpp src/engine.cpp
src/crankcase.cpp src/crankcase.cpp
src/two_stroke_cylinder.cpp src/two_stroke_cylinder.cpp
@@ -17,7 +16,6 @@ add_library(fluidsim_core
src/audio_processing.cpp src/audio_processing.cpp
src/outdoor_audio.cpp src/outdoor_audio.cpp
src/outdoor_exhaust_reverb.cpp src/outdoor_exhaust_reverb.cpp
src/mechanical_sound_processor.cpp
src/scenario.cpp src/scenario.cpp
src/solver.cpp src/solver.cpp
src/vehicle.cpp src/vehicle.cpp
@@ -64,8 +62,17 @@ if(FLUIDSIM_BUILD_DESKTOP)
) )
FetchContent_MakeAvailable(SFML) FetchContent_MakeAvailable(SFML)
endif() 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)
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 src/audio_output_stream.cpp src/sound_engine.cpp)
target_link_libraries(fluidsim_desktop PRIVATE fluidsim_core SFML::Graphics SFML::Audio) target_link_libraries(fluidsim_desktop PRIVATE fluidsim_desktop_rendering SFML::Audio)
if(MSVC) if(MSVC)
target_compile_options(fluidsim_desktop PRIVATE /W4 /permissive-) target_compile_options(fluidsim_desktop PRIVATE /W4 /permissive-)
else() else()
@@ -102,9 +109,14 @@ endif()
include(CTest) include(CTest)
if(BUILD_TESTING) if(BUILD_TESTING)
add_executable(fluidsim_saab_turbo_regression tests/saab_turbo_regression.cpp) add_executable(fluidsim_scenario_examples tests/scenario_examples.cpp)
target_link_libraries(fluidsim_saab_turbo_regression PRIVATE fluidsim_core) target_link_libraries(fluidsim_scenario_examples PRIVATE fluidsim_core)
add_test(NAME fluidsim_saab_turbo_regression COMMAND fluidsim_saab_turbo_regression) add_test(NAME fluidsim_scenario_examples COMMAND fluidsim_scenario_examples)
endif()
if(BUILD_TESTING AND FLUIDSIM_BUILD_DESKTOP)
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)
endif() endif()
if(BUILD_TESTING) if(BUILD_TESTING)
add_executable(fluidsim_physics_tests tests/physics_tests.cpp) add_executable(fluidsim_physics_tests tests/physics_tests.cpp)
@@ -116,10 +128,15 @@ add_executable(fluidsim_physics_benchmark tools/physics_benchmark.cpp)
target_link_libraries(fluidsim_physics_benchmark PRIVATE fluidsim_core) target_link_libraries(fluidsim_physics_benchmark PRIVATE fluidsim_core)
add_executable(fluidsim_scenario_benchmark tools/scenario_benchmark.cpp) add_executable(fluidsim_scenario_benchmark tools/scenario_benchmark.cpp)
target_link_libraries(fluidsim_scenario_benchmark PRIVATE fluidsim_core) target_link_libraries(fluidsim_scenario_benchmark PRIVATE fluidsim_core)
if(BUILD_TESTING)
add_test(NAME fluidsim_v8_exhaust_regression COMMAND fluidsim_scenario_benchmark 8 coast v8)
endif()
if(BUILD_TESTING) if(BUILD_TESTING)
add_executable(fluidsim_fluid_validation tests/fluid_validation.cpp) add_executable(fluidsim_fluid_validation tests/fluid_validation.cpp)
target_link_libraries(fluidsim_fluid_validation PRIVATE fluidsim_core) target_link_libraries(fluidsim_fluid_validation PRIVATE fluidsim_core)
add_test(NAME fluidsim_fluid_validation COMMAND fluidsim_fluid_validation) add_test(NAME fluidsim_fluid_validation COMMAND fluidsim_fluid_validation)
add_test(NAME fluidsim_fluid_validation_hlle COMMAND fluidsim_fluid_validation hlle)
add_test(NAME fluidsim_fluid_validation_hllc COMMAND fluidsim_fluid_validation hllc)
endif() endif()
find_package(Threads REQUIRED) find_package(Threads REQUIRED)
target_link_libraries(fluidsim_core PUBLIC Threads::Threads) target_link_libraries(fluidsim_core PUBLIC Threads::Threads)
@@ -135,8 +152,6 @@ add_executable(fluidsim_audio_capture tools/audio_capture.cpp)
target_link_libraries(fluidsim_audio_capture PRIVATE fluidsim_core) target_link_libraries(fluidsim_audio_capture PRIVATE fluidsim_core)
add_executable(fluidsim_two_stroke_benchmark tools/two_stroke_benchmark.cpp) add_executable(fluidsim_two_stroke_benchmark tools/two_stroke_benchmark.cpp)
target_link_libraries(fluidsim_two_stroke_benchmark PRIVATE fluidsim_core) target_link_libraries(fluidsim_two_stroke_benchmark PRIVATE fluidsim_core)
add_executable(fluidsim_turbo_benchmark tools/turbo_benchmark.cpp)
target_link_libraries(fluidsim_turbo_benchmark PRIVATE fluidsim_core)
if(BUILD_TESTING) if(BUILD_TESTING)
add_executable(fluidsim_outdoor_audio_tests tests/outdoor_audio_tests.cpp) add_executable(fluidsim_outdoor_audio_tests tests/outdoor_audio_tests.cpp)
target_link_libraries(fluidsim_outdoor_audio_tests PRIVATE fluidsim_core) target_link_libraries(fluidsim_outdoor_audio_tests PRIVATE fluidsim_core)
@@ -150,12 +165,6 @@ if(BUILD_TESTING)
add_executable(fluidsim_b205_regression tests/b205_regression.cpp) add_executable(fluidsim_b205_regression tests/b205_regression.cpp)
target_link_libraries(fluidsim_b205_regression PRIVATE fluidsim_core) target_link_libraries(fluidsim_b205_regression PRIVATE fluidsim_core)
add_test(NAME fluidsim_b205_regression COMMAND fluidsim_b205_regression) add_test(NAME fluidsim_b205_regression COMMAND fluidsim_b205_regression)
add_executable(fluidsim_turbo_physics tests/turbo_physics.cpp)
target_link_libraries(fluidsim_turbo_physics PRIVATE fluidsim_core)
add_test(NAME fluidsim_turbo_physics COMMAND fluidsim_turbo_physics)
add_executable(fluidsim_turbo_twin_regression tests/turbo_twin_regression.cpp)
target_link_libraries(fluidsim_turbo_twin_regression PRIVATE fluidsim_core)
add_test(NAME fluidsim_turbo_twin_regression COMMAND fluidsim_turbo_twin_regression)
endif() endif()
include(cmake/MinGWRuntime.cmake) include(cmake/MinGWRuntime.cmake)

260
README.md
View File

@@ -83,10 +83,11 @@ installed Windows or Linux system fonts. To bundle another font, add
headless build, add `-DFLUIDSIM_BUILD_DESKTOP=OFF`; this skips SFML and the headless build, add `-DFLUIDSIM_BUILD_DESKTOP=OFF`; this skips SFML and the
desktop's graphics/audio dependencies. desktop's graphics/audio dependencies.
Controls: hold `W` for throttle, `C` for clutch, `A` / `D` to shift, `Z` for Controls: W/S raise/lower the pedal over 0.5 seconds, C disengages the clutch,
neutral, up/down to adjust throttle target, left/right to adjust engine load, V applies starter torque while held,
`M` toggles mechanical engine noise, mouse wheel changes simulation playback A/D shift, Z selects neutral, and left/right adjust engine load. The on-screen
speed, and Space toggles real time. pedal is direct and springs back on release; zero pedal retains ECU idle control.
Page Up/Down change playback speed; Space toggles real time.
The command-line runner is useful for repeatable checks: The command-line runner is useful for repeatable checks:
@@ -118,8 +119,11 @@ it performs no simulation, allocation or waiting. An underrun fades out and
recovery crossfades over 64 samples rather than stepping abruptly to silence. recovery crossfades over 64 samples rather than stepping abruptly to silence.
The callback never advances physical time to conceal missing samples. The callback never advances physical time to conceal missing samples.
The source is a linear compact-monopole approximation, The primary source is a linear compact-monopole approximation,
`p(r,t) = (d mass_flow/dt)/(4*pi*r)`. Intake and exhaust have separate static `p(r,t) = rho_ambient * (d volume_flow/dt)/(4*pi*r)`. Mouth volume flow
comes from the boundary face velocity and area. Fluid conservation retains the
actual mass flux; hot/cold density changes are excluded from the acoustic source.
Intake and exhaust have separate static
positions, fractional-sample propagation delays, inverse-distance spreading, positions, fractional-sample propagation delays, inverse-distance spreading,
and left/right ear paths. Playback calibration is 200 Pa per device full scale; and left/right ear paths. Playback calibration is 200 Pa per device full scale;
this is not calibrated speaker SPL. A second-order 6 kHz monitoring filter this is not calibrated speaker SPL. A second-order 6 kHz monitoring filter
@@ -128,19 +132,27 @@ A roughly 3.5 Hz device DC blocker and final PCM clipping protection remain.
Use 1x playback for acoustic evaluation. Use 1x playback for acoustic evaluation.
The default environment is a nearby wall. Press **R** to cycle dry, field, The default environment is a nearby wall. Press **R** to cycle dry, field,
wall and woodland. Ground/wall image paths provide early reflections; the wall, woodland, and tunnel. Ground/wall image paths provide early reflections;
woodland scatterers and decaying feedback-delay tail are approximate effects, the live eight-line feedback-delay network supplies a scene-dependent late
not validated outdoor acoustics. Synthetic mechanical noise remains optional. tail. Woodland scatterers and the hard-surface tunnel images are approximate.
The compact source does not implement pipe-end radiation impedance, validated These are listening profiles, not validated acoustics. Mechanical sounds are
directivity, or exhaust afterfire chemistry. Filtering cannot recover aliased not implemented.
Open pipe mouths now use a causal unflanged radiation boundary with frequency-
dependent reflection and end correction. Intake mouths draw fresh ambient gas
directly, replacing the fixed atmosphere component and its inlet orifice.
The model assumes plane waves and low mean flow; strong jet and vortex effects
remain unmodeled. The compact source does not implement validated directivity
or exhaust afterfire chemistry. Filtering cannot recover aliased
source information. See [audio verification](verification/AUDIO_RENDERING.md) source information. See [audio verification](verification/AUDIO_RENDERING.md)
for tests, capture commands, limitations and measured transient reductions. for capture commands, current renderer details and limitations.
Launching `fluidsim_desktop` without `--scenario` opens a small scenario menu. Launching `fluidsim_desktop` without `--scenario` opens a small scenario menu.
Click the twin, YZ125 or Saab B205 card to start, or select with Up/Down and press Enter. Choose the twin, YZ125, Saab B205, or American V8 card, or select with Up/Down
and press Enter.
Escape closes the menu. Simulation and audio start after selection. Passing Escape closes the menu. Simulation and audio start after selection. Passing
`--scenario twin`, `--scenario two-stroke` or `--scenario b205` bypasses the menu; `--scene` and `--scenario twin`, `--scenario two-stroke`, `--scenario b205`, or `--scenario v8`
`--exhaust` options also apply when choosing through the menu. bypasses the menu; `--scene` and `--exhaust` options also apply
when choosing through the menu.
To test the two desktop engines from the repository root: To test the two desktop engines from the repository root:
@@ -151,7 +163,8 @@ cmake --build build-desktop --target fluidsim_desktop -j 4
./build-desktop/fluidsim_desktop --scenario twin --scene field ./build-desktop/fluidsim_desktop --scenario twin --scene field
``` ```
Hold **W** for throttle, use **Up/Down** to adjust the persistent pedal request, Hold **W** to raise the pedal and **S** to lower it. Releasing the keyboard
returns the pedal over 0.5 seconds. The UI lever is direct and springs back. Use
**Space** to restore real-time playback and **R** to change environment. The two-stroke desktop uses **Space** to restore real-time playback and **R** to change environment. The two-stroke desktop uses
the existing 125 cc configuration; its dimensions and calibrations must be the existing 125 cc configuration; its dimensions and calibrations must be
adapted before it represents a particular engine. Both scenarios use the same adapted before it represents a particular engine. Both scenarios use the same
@@ -214,6 +227,13 @@ seconds per simulated second here, including coast-down (previously up to
repairs. The CFL limit remains enforced; other operating points may still repairs. The CFL limit remains enforced; other operating points may still
require subdivisions. The desktop retains the 4x playback gain. require subdivisions. The desktop retains the 4x playback gain.
The V8 uses the same per-pipe resolution policy as other scenarios. Uniform
mesh benchmarks found that 35 cells/m was near real time under one pedal
workload and 33 cells/m was near real time at steady 4500 RPM on the tested
machine; denser uniform meshes ran slower than real time. The existing
per-pipe mesh is faster and remains in use. See [V8 performance notes](verification/V8_PIPE_PERFORMANCE.md)
for measurements and reproduction commands.
Benchmark or capture with: Benchmark or capture with:
```sh ```sh
@@ -222,36 +242,9 @@ Benchmark or capture with:
./build-desktop/fluidsim_audio_capture --scenario v8 --seconds 6 --throttle .3 --out /tmp/v8 ./build-desktop/fluidsim_audio_capture --scenario v8 --seconds 6 --throttle .3 --out /tmp/v8
``` ```
## Twin with a VZ21-size turbo
Choose **600 cc twin + VZ21-size turbo** in the startup menu, or bypass it:
```sh
./build-desktop/fluidsim_desktop --scenario twin --turbo vz21 --scene field
./build-audit/fluidsim_cpp --scenario twin --turbo vz21 --seconds 5 --throttle 0.7
```
`Turbocharger` connects to four existing pipe ends and couples turbine and
compressor work through a rigid rotor. Small housing volumes preserve unsteady
exhaust pressure. Backpressure, charge temperature and spool speed emerge from
flow restrictions and energy exchange. A pressure-can/spring wastegate opens a
bypass; no boost pressure is imposed. HUD pressures are absolute. The default
unboosted twin is unchanged unless `--turbo vz21` is selected.
This is an estimated VZ21-size model, not a measured turbo map. Engine exhaust
pulses are implemented; blade-passing whistle/wakes are still pending. The
seven-second twin transient and all nine CTest suites passed. The optimized
7,000-RPM workload takes about 0.65 wall seconds per simulated second on the audit
machine; actual playback headroom depends on the computer. See
[turbo verification](verification/TURBO_VZ21.md) for assumptions and results.
## Saab B205 / 9-5 wagon baseline ## Saab B205 / 9-5 wagon baseline
The Saab menu entry now includes an estimated GT17 turbo and ECU wastegate boost The Saab menu entry models the naturally aspirated B205 baseline.
control (up to 0.45 bar gauge). Launch with `--scenario b205 --turbo gt17`, or use
`--turbo none` for the naturally aspirated baseline. See [GT17 verification](verification/SAAB_GT17.md)
for controller behavior, assumptions and tests. Charge heating and backpressure
are simulated; an intercooler is not yet included.
Select **Saab B205 / 9-5 wagon** from the desktop menu, or launch directly: Select **Saab B205 / 9-5 wagon** from the desktop menu, or launch directly:
@@ -266,10 +259,8 @@ cylinder and 1-3-4-2 firing order. Four intake runners and four exhaust headers
connect shared plenums and a tailpipe. The vehicle approximates a 9-5 wagon: connect shared plenums and a tailpipe. The vehicle approximates a 9-5 wagon:
1675 kg with driver, five-speed manual, FM55 B05 overall ratios 1675 kg with driver, five-speed manual, FM55 B05 overall ratios
13.70 / 7.12 / 4.77 / 3.62 / 2.67, and 4.05 final drive. No sixth gear is offered. 13.70 / 7.12 / 4.77 / 3.62 / 2.67, and 4.05 final drive. No sixth gear is offered.
Controls and dyno match the other scenarios. The original atmospheric-breathing Controls and dyno match the other scenarios. Pipe/cam dimensions,
geometry baseline remains available with `--turbo none`. Pipe/cam dimensions,
friction, idle/ignition maps, clutch, drag and loaded tire radius are estimates. friction, idle/ignition maps, clutch, drag and loaded tire radius are estimates.
Turbo calibration and acoustics are approximate; factory power is not validated.
See [B205 verification](verification/B205_BASELINE.md) for sources and checks. See [B205 verification](verification/B205_BASELINE.md) for sources and checks.
## Current port status ## Current port status
@@ -280,8 +271,8 @@ configurable 0-D volume components (including thermal relaxation and moving
volume work), profiled compressible 1-D pipes and boundaries, adaptive solver volume work), profiled compressible 1-D pipes and boundaries, adaptive solver
stepping, the impulse-clutch motorcycle/road-load model, and the active 600 cc stepping, the impulse-clutch motorcycle/road-load model, and the active 600 cc
twin topology. Audio includes the flow-derivative source, spatialized twin topology. Audio includes the flow-derivative source, spatialized
intake/exhaust radiation, outdoor feedback-delay reverb, optional mechanical intake/exhaust radiation, outdoor feedback-delay reverb, a lock-free simulation
noise, a lock-free simulation audio buffer, a `SoundEngine` lifecycle wrapper, audio buffer, a `SoundEngine` lifecycle wrapper,
and an SFML desktop frontend. The shared 50-RPM-bin dyno history and graph are and an SFML desktop frontend. The shared 50-RPM-bin dyno history and graph are
also ported. A crankcase-scavenged 125 cc two-stroke scenario is available in also ported. A crankcase-scavenged 125 cc two-stroke scenario is available in
the core and desktop, with an ECU, finite-response throttle and dynamic reed. the core and desktop, with an ECU, finite-response throttle and dynamic reed.
@@ -313,15 +304,17 @@ Press **G** to toggle it; the dashboard shows its state. It tracks stereo RMS
and peaks after DC removal, lifting quiet idle audio gradually (1.5-second and peaks after DC removal, lifting quiet idle audio gradually (1.5-second
gain-rise time constant) and reducing gain faster for loud revs (30 ms). gain-rise time constant) and reducing gain faster for loud revs (30 ms).
Extra gain is capped at 12x above the existing fixed 4x playback gain, with Extra gain is capped at 12x above the existing fixed 4x playback gain, with
linked stereo peak protection. Silence does not request additional boost and linked stereo peak protection. Silence does not change the engine controls and
underruns freeze the detector. Disabling smoothly returns to fixed playback underruns freeze the detector. Disabling smoothly returns to fixed playback
gain. Simulation pressures and WAV capture levels are unaffected. gain. Simulation pressures and WAV capture levels are unaffected.
Both desktop engines use `Ecu` (`include/FluidSim/Ecu.hpp`). The desktop and Both desktop engines use `Ecu` (`include/FluidSim/Ecu.hpp`). The desktop and
CLI throttle values are pedal requests; the actual throttle is advanced every CLI throttle values are pedal requests; the actual throttle is advanced every
simulation sample, independently of rendering. Opening and closing use an simulation sample, independently of rendering. Opening and closing use an
80 ms first-order actuator response and a maximum travel rate of two full 1 ms first-order throttle-body response capped at 1000 full travels per second.
travels per second. The four-stroke uses an ideal butterfly projection; the The two-stroke carb slide uses 0.2 ms and 5000 full travels per second.
Only the keyboard pedal request ramps over 0.5 seconds; the UI slider is direct.
Physical fuel-film and mixture transport dynamics remain in the carburetor. The four-stroke uses an ideal butterfly projection; the
two-stroke uses the circular-segment aperture uncovered by a translating slide. two-stroke uses the circular-segment aperture uncovered by a translating slide.
Shaft obstruction and edge leakage are not modeled. Shaft obstruction and edge leakage are not modeled.
@@ -352,13 +345,19 @@ is transported out through the exhaust without pipe combustion. No sound
samples are mixed in. samples are mixed in.
All ECU parameters are accessible calibrations, not measured engine maps. All ECU parameters are accessible calibrations, not measured engine maps.
There is no starter motor, automatic restart, knock sensor, lambda sensor, Holding V applies configurable constant starter torque at the crankshaft in every
scenario; release or loss of window focus disengages it. This is a torque source,
without battery, motor electrical, or starter gearing models. There is no
automatic restart, knock sensor, lambda sensor,
catalyst, cold-start temperature model or injector electrical model. Idle catalyst, cold-start temperature model or injector electrical model. Idle
control can still stall under excessive load and does not impose crankshaft control can still stall under excessive load and does not impose crankshaft
speed or add artificial sustaining torque. Existing combustion/thermal models speed or add artificial sustaining torque. Existing combustion/thermal models
remain approximations; ECU changes do not establish their physical accuracy. remain approximations; ECU changes do not establish their physical accuracy.
The two-stroke targets 3000 RPM idle and uses idle ignition trim in addition to The two-stroke uses a fixed mechanical carb slide stop instead of RPM-controlled
throttle control. Its idle throttle or idle ignition trim. Each carb scenario config sets
`TwoStrokeConfig::idle_slide_opening` (normalized 0..1); the default is 0.045,
or 4.5% slide travel. Pedal demand above that stop opens the slide further.
The resulting idle RPM depends on the simulation and load. Its
reed is a damped lumped cantilever model with finite opening time and seating reed is a damped lumped cantilever model with finite opening time and seating
contacts. Cylinder and crankcase moving-volume work use adiabatic energy changes. contacts. Cylinder and crankcase moving-volume work use adiabatic energy changes.
Jet dimensions are effective flow calibrations rather than measured Mikuni Jet dimensions are effective flow calibrations rather than measured Mikuni
@@ -396,3 +395,154 @@ The twin intake is supplied by an explicit 300 K atmospheric reservoir rather
than a pressure-only boundary with extrapolated entropy. The repeated-blip than a pressure-only boundary with extrapolated entropy. The repeated-blip
`fluidsim_intake_regression` checks fresh-air supply and pressure/idle recovery. `fluidsim_intake_regression` checks fresh-air supply and pressure/idle recovery.
Manifold vacuum at closed throttle remains part of the simulated pumping cycle. Manifold vacuum at closed throttle remains part of the simulated pumping cycle.
## Pipe solver comparisons
The desktop accepts `--solver rusanov|hlle|hllc`. Press **F3** during playback to
cycle Rusanov, HLLE and HLLC; the dashboard shows the current solver. All pipes
switch together between simulation samples. Rusanov remains the startup default.
Changing solver affects the ongoing engine state; allow several cycles and the
audio queue to settle before judging it. Use the same scene and auto-gain setting.
Each method currently uses the same first-order spatial discretization and CFL
limit. HLLC resolves contact transport with less diffusion and uses HLLE when
its intermediate states are invalid. These are solver experiments; sharper
signals do not by themselves prove more accurate combustion or sound.
For repeatable comparisons, WAV capture accepts `--solver`, `--rpm` (an ideal
speed-holding dyno) and `--warmup` in seconds. Capture gains are identical across
solvers and do not include desktop automatic gain. For example:
```sh
./build-desktop/fluidsim_desktop --scenario v8 --scene dry --solver hllc
./build-desktop/fluidsim_desktop --scenario two-stroke --scene dry --solver hllc
./build-desktop/fluidsim_scenario_benchmark 8 loaded v8 0 hllc
./build-desktop/fluidsim_scenario_benchmark 8 loaded two-stroke-racing 0 hllc
./build-desktop/fluidsim_audio_capture --scenario v8 --solver hllc --scene dry --rpm 4500 --warmup 2 --seconds 6 --throttle .7 --out /tmp/v8-hllc
```
The benchmark's fourth argument is the existing optional uniform V8 cells/m
setting (0 keeps the scenario mesh); its fifth argument selects the pipe solver.
See [solver measurements](verification/SOLVER_COMPARISON.md) for results.
## Desktop workspace and 3D view
The desktop has a neutral black/gray workspace: a left telemetry and control
panel, an engine viewport, a dyno panel, and a cylinder inspector. Resize or
maximize the window to give the engine view more space. All desktop engine
presets use the OpenGL 3D renderer by default. Tab switches to the existing
schematic view. Left drag orbits, right/middle drag pans, the wheel zooms, and
Home fits the engine again. Camera gestures are confined to the engine viewport.
The spring-return pedal slider sends a pedal request to the ECU. W/S pedal
travel takes 0.5 seconds in either direction; releasing W returns toward zero
at the same speed. End releases the pedal immediately. Zero pedal lets the ECU
operate its idle throttle opening. Solver cycling is now F3; S lowers the pedal.
Pipes are continuous indexed tube meshes with smooth bends, physical diameters
from the simulated area profile, and per-vertex pressure colors. Route points
are keyed by distance along the simulated pipe, in metres. Moving those drawing
points changes the schematic route, not the fluid solver lengths. The fixed
pressure scale is blue at vacuum, green at 101325 Pa and red at/above 1 MPa.
Volumes and cylinder gas use the same pressure scale. Pistons and rods animate
from snapshot piston positions and crank phases. Geometry is schematic rather
than a dimensionally complete engine CAD model.
Code responsibilities are split into `DesktopUi`/`WorkspaceLayout`, `OrbitCamera`,
`PedalControl`, `PipePath`, `EngineRenderer3D`, `EngineSchematic` and `DynoRenderer`.
The renderer consumes snapshots and reuses meshes; it does not step the solver.
OpenGL is linked through CMake's `OpenGL::GL`. This first renderer uses indexed
client arrays, lighting and a depth buffer in an OpenGL compatibility context,
with SFML's GL state preservation around the viewport pass. It is not a core
profile shader renderer. Windows runtime still needs verification on Windows.
### Saab exhaust silencers
The B205 exhaust now includes a 6 L straight-through resonator and an 18 L
straight-through rear muffler, separated by 0.8 m of pipe and followed by a
radiating tailpipe. Saab uses two minimum fluid substeps per audio sample; the
acoustic CFL condition can request more. These are estimated large
passenger-car silencers, not measured Saab internals. `ExhaustConfig` and
`SilencerConfig` in `include/FluidSim/Silencer.hpp` configure length, nominal body
volume, core diameter, chamber count, baffle passage diameter, linear resistance
(1/s), and quadratic loss coefficient. Pass an `ExhaustConfig` as the third
`FourStrokeScenario` constructor argument; `enabled=false` bypasses the silencers.
Expansion and chambered silencers use resolved variable-area passages. Body
radius is derived from volume and length; tapered transitions reduce actual gas
volume below the nominal cylindrical body volume. Straight-through mode retains
a constant core through the housing and uses a linear resistance surrogate for
packing. It does not yet resolve perforation coupling or porous-material acoustics.
Loss coefficients are tuning inputs, not calibrated predictions from body geometry.
Momentum losses retain total gas energy, converting kinetic energy into heat.
The renderer draws round housings with their configured lengths and diameters.
### N-port junctions and V8 4-2-1 exhaust
`BoundarySystem::add_junction({{pipe_index, left_end}, ...})` connects any number
of pipe faces without a storage volume. A shared stationary Rusanov state is computed algebraically from all connected
faces. Integrated mass, total energy and species fluxes sum to zero at each node. Setup claims every endpoint;
updates allocate no memory. This is a dissipative one-dimensional numerical coupling, not a
geometry-resolving junction model. Momentum changes
are reacted by the fitting. Branch angle losses are not modeled. The separate
`Junction` class remains available for explicit finite-volume chambers.
Each V8 bank now has a 4-2-1 header: four equal 750 mm x 38 mm primaries feed two
equal 400 mm x 45 mm secondaries, then a 50 mm exhaust. Pairings are 1+2 / 3+4
and 5+7 / 6+8 with this simulation's bank numbering and firing order. Two T
junctions and a 780 mm x 38 mm tube form the H-pipe. Small configurable port-coupled junction volumes are used by the V8. The intake
plenum also remains a storage volume.
`V8ExhaustConfig` configures primary length/diameter and enables the H crossover;
pass it as the fourth `FourStrokeScenario` constructor argument. The renderer
routes the primary pairs into Y merges, then the final Y collectors and H-pipe,
using local pipe diameter and pressure colors without collector boxes.
The previous header optimization measurements applied to the earlier finite-volume
4-1 layout and do not establish an optimum for this new 4-2-1 network.
### Example browser and small port-coupled junctions
The startup browser now scrolls with the mouse wheel, Up/Down, Page Up/Down,
Home and End. It adapts to the window size and shows a scroll indicator. Click
an example or press Enter to start it. The examples are defined in
`ScenarioCatalog.hpp`, grouped into categories with selectable variants. These are configuration comparisons of
the existing engine models, not measurements of additional production engines.
The V8 currently uses port-coupled finite-volume fittings, configured by
`V8ExhaustConfig::junction_volume` (default 50 cc per fitting). They use the
existing conservative component/port exchange and have no volume boxes in the
renderer. The earlier zero-storage Rusanov junction remains available for
comparison through `BoundarySystem::add_junction`, but is not used by the V8.
The 4-2-1 piping and the H crossover geometry remain the same.
### 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.
`EngineConfig` supplies cylinder count (up to 12), inline/V/boxer layout,
bank angle, bore/stroke/rod, compression, firing phases, rev limit and fueling.
The catalog includes four-stroke singles, parallel and V twins, inline 3/4/5/6,
V6/V8/V10/V12 and flat 4/6 examples. Inspired builds include F20C, Audi five,
RB26, Busso V6, EJ20, 911 flat-six, 916 V-twin and 250 V12 concepts. These use
estimated calibrations and simplified common-plenum/exhaust networks: they are
not factory replicas, measured torque curves, or turbocharged configurations.
The 8.0 L big-block variant transports carburetor fuel through its intake into
the cylinders. Its fixed idle stop is configured with `EngineConfig::idle_opening`.
It reuses the lumped carb model with scaled effective jets; multiple carb barrels,
floats and cylinder-specific fuel distribution are not resolved. Larger engine
geometry requires its own mixture and idle calibration.
Two-stroke families include 50, 125 and 500 cc singles. The 125 variants compare
standard, straight, long, short and wide chambers. The 50/500 concepts scale
bore/stroke, rod, crankcase clearance, port widths, flywheel inertia and chamber
geometry; they retain estimated carb and combustion calibrations. A different
chamber is a geometry experiment, not a guaranteed RPM or power improvement.
Launch a specific variant directly with `--example big-carb`, `--example flat6`,
`--example fifty` or another catalog ID. Carb jet scaling is configurable per
engine (`EngineConfig::carb_jet_scale`, `carb_pilot_scale`, or
`TwoStrokeConfig::jet_scale`).

View File

@@ -1,13 +1,11 @@
#pragma once #pragma once
#include <vector>
namespace fluidsim::port { namespace fluidsim::port {
struct AudioFrame { float left{}, right{}; }; struct AudioFrame { float left{}, right{}; };
struct Vec3 { float x{}, y{}, z{}; }; struct Vec3 { float x{}, y{}, z{}; };
// Flow-derivative sound source used by the C# two-stroke and pulse scenarios. // Simple flow-derivative source used by the Helmholtz and pulse scenarios.
class SoundProcessor { class SoundProcessor {
public: public:
float gain{0.005f}; float gain{0.005f};
@@ -17,34 +15,4 @@ private:
float dt_{}, scale_factor_{}, previous_flow_{}; float dt_{}, scale_factor_{}, previous_flow_{};
}; };
class RadiationSource {
public:
// Optional diagnostic filters; disabled for the linear radiation path.
float flow_tau{0.0f}, smooth_tau{0.0f}, gain{0.005f};
Vec3 pipe_end{}, pipe_axis{1.0f, 0.0f, 0.0f};
Vec3 listener{0.0f, 0.0f, 1.5f}, listener_right{1.0f, 0.0f, 0.0f};
float ear_spacing{0.18f}, head_shadow_cutoff{0.0f}, speed_of_sound{343.0f};
float directivity_balance{0.0f}, air_absorption_exponent{0.0f}, min_air_cutoff{1500.0f};
explicit RadiationSource(int sample_rate = 44100);
AudioFrame process(float mass_flow);
float last_mono_pressure() const { return last_mono_pressure_; }
private:
class DelayLine;
class OnePole;
int sample_rate_{};
float dt_{}, flow_lp_{}, previous_flow_lp_{}, smooth_derivative_{};
float last_mono_pressure_{};
std::vector<float> left_delay_, right_delay_;
std::size_t write_position_{}, left_offset_{}, right_offset_{};
float air_filter_{}, shadow_left_{}, shadow_right_{};
static float dot(Vec3 a, Vec3 b);
static float length(Vec3 value);
static Vec3 subtract(Vec3 a, Vec3 b);
float one_pole(float input, float cutoff, float& state) const;
float delayed(float input, std::vector<float>& buffer, std::size_t offset);
};
} // namespace fluidsim::port } // namespace fluidsim::port

View File

@@ -0,0 +1,37 @@
#pragma once
#include "FluidSim/Scenario.hpp"
#include "FluidSim/DynoCurve.hpp"
#include "FluidSim/Desktop/PedalControl.hpp"
#include "FluidSim/Desktop/EngineRenderer3D.hpp"
#include <SFML/Graphics.hpp>
namespace fluidsim::port::desktop {
struct PlaybackInfo {
float pedal_target{},load{};
double speed{1};
std::size_t buffered{},capacity{};
std::uint64_t underruns{};
bool auto_gain{},time_warp{};
};
struct WorkspaceLayout {
sf::FloatRect sidebar,viewport,chart,inspector,pedal;
static WorkspaceLayout calculate(sf::Vector2u size);
};
class DesktopUi {
public:
float pedal() const {return pedal_.value();}
void nudge_pedal(float amount) {pedal_.set(pedal_.value()+amount);}
void update_pedal(float dt,bool raise,bool lower) {if(!pedal_drag_)pedal_.advance(dt,raise,lower);}
bool handle_event(const sf::Event&,sf::Vector2u);
void draw(sf::RenderWindow&,const sf::Font*,const ScenarioSnapshot&,const PlaybackInfo&);
private:
DynoCurve dyno_;
EngineRenderer3D renderer_;
OrbitCamera camera_;
bool three_d_{true},camera_fitted_{},pan_drag_{},pedal_drag_{};
PedalControl pedal_;
sf::Vector2f pan_{};
sf::Vector2i previous_mouse_{};
bool dragging_{};
float zoom_{1};
};
}

View File

@@ -0,0 +1,6 @@
#pragma once
#include "FluidSim/DynoCurve.hpp"
#include <SFML/Graphics.hpp>
namespace fluidsim::port::desktop {
void draw_dyno_curve(sf::RenderWindow&,const sf::Font*,const DynoCurve&,float,float,float,float,float,float);
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include "FluidSim/Scenario.hpp"
#include "FluidSim/Desktop/OrbitCamera.hpp"
#include <SFML/Graphics.hpp>
#include <memory>
namespace fluidsim::port::desktop {
// Visualization consumes snapshots only; it never accesses mutable solver state.
class EngineRenderer3D {
public:
EngineRenderer3D();
~EngineRenderer3D();
EngineRenderer3D(const EngineRenderer3D&)=delete;
EngineRenderer3D& operator=(const EngineRenderer3D&)=delete;
void draw(sf::RenderWindow&,const ScenarioSnapshot&,sf::FloatRect,const OrbitCamera&);
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
}

View File

@@ -0,0 +1,6 @@
#pragma once
#include "FluidSim/Scenario.hpp"
#include <SFML/Graphics.hpp>
namespace fluidsim::port::desktop {
void draw_schematic(sf::RenderWindow&, const sf::Font*, const ScenarioSnapshot&);
}

View File

@@ -0,0 +1,20 @@
#pragma once
#include "FluidSim/AudioProcessing.hpp"
#include <SFML/System/Vector2.hpp>
#include <algorithm>
#include <cmath>
namespace fluidsim::port::desktop {
struct OrbitCamera {
Vec3 target{.12f,.25f,0};
float yaw{-28},pitch{24},distance{2.2f};
void reset(float aspect) {target={.12f,.25f,0};yaw=-28;pitch=24;distance=std::max(.65f,1.6f/aspect)/.414214f;}
void orbit(sf::Vector2i delta) {yaw+=delta.x*.35f;pitch=std::clamp(pitch+delta.y*.3f,-75.f,80.f);}
void pan(sf::Vector2i delta,float height) {
const float y=yaw*.017453293f,p=pitch*.017453293f,k=distance*.828427f/std::max(height,1.f);
target.x+=k*(-delta.x*std::cos(y)+delta.y*std::sin(p)*std::sin(y));
target.y+=k*delta.y*std::cos(p);
target.z+=k*(-delta.x*std::sin(y)-delta.y*std::sin(p)*std::cos(y));
}
void zoom(float delta) {distance=std::clamp(distance*std::exp(-delta*.12f),.35f,12.f);}
};
}

View File

@@ -0,0 +1,15 @@
#pragma once
#include <algorithm>
namespace fluidsim::port::desktop {
// Keyboard input travel is separate from the ECU actuator response.
class PedalControl {
public:
float value() const {return value_;}
void set(float value) {value_=std::clamp(value,0.f,1.f);}
void advance(float dt,bool raise,bool lower) {
if(dt>0) set(value_+(raise && !lower?2.f:-2.f)*dt);
}
private:
float value_{};
};
}

View File

@@ -0,0 +1,46 @@
#pragma once
#include "FluidSim/AudioProcessing.hpp"
#include <algorithm>
#include <cmath>
#include <stdexcept>
#include <vector>
#include <utility>
namespace fluidsim::port::desktop {
struct PipeAnchor {float distance;Vec3 position;};
// Knots are distances along the simulated pipe, in metres. Display positions
// define its schematic route; changing them never changes fluid cell lengths.
class PipePath {
public:
explicit PipePath(std::vector<PipeAnchor> anchors):anchors_(std::move(anchors)) {
if(anchors_.size()<2 || anchors_.front().distance!=0)throw std::invalid_argument("pipe path needs origin and endpoint");
for(std::size_t i=0;i<anchors_.size();++i) {
const auto& p=anchors_[i];
if(!std::isfinite(p.distance) || !std::isfinite(p.position.x) || !std::isfinite(p.position.y) || !std::isfinite(p.position.z) ||
(i && !(p.distance>anchors_[i-1].distance))) throw std::invalid_argument("invalid distance-referenced pipe path");
}
}
Vec3 position(float distance) const {
distance=std::clamp(distance,0.f,anchors_.back().distance);
auto it=std::upper_bound(anchors_.begin(),anchors_.end(),distance,[](float s,const PipeAnchor& a){return s<a.distance;});
std::size_t i=std::clamp<std::size_t>(it-anchors_.begin(),1,anchors_.size()-1)-1;
const auto& a=anchors_[i];const auto& b=anchors_[i+1];
const float span=b.distance-a.distance,t=(distance-a.distance)/span,t2=t*t,t3=t2*t;
const auto ma=tangent_at(i),mb=tangent_at(i+1);
auto coordinate=[&](float pa,float pb,float ta,float tb){return (2*t3-3*t2+1)*pa+(t3-2*t2+t)*span*ta+(-2*t3+3*t2)*pb+(t3-t2)*span*tb;};
return {coordinate(a.position.x,b.position.x,ma.x,mb.x),coordinate(a.position.y,b.position.y,ma.y,mb.y),coordinate(a.position.z,b.position.z,ma.z,mb.z)};
}
Vec3 tangent(float distance) const {
const float epsilon=anchors_.back().distance*.0001f;
const auto a=position(distance-epsilon),b=position(distance+epsilon);
const float x=b.x-a.x,y=b.y-a.y,z=b.z-a.z,n=std::sqrt(x*x+y*y+z*z);
return n>1e-8f?Vec3{x/n,y/n,z/n}:Vec3{1,0,0};
}
private:
Vec3 tangent_at(std::size_t i) const {
const auto& a=anchors_[i?i-1:0];const auto& b=anchors_[std::min(i+1,anchors_.size()-1)];
const float d=b.distance-a.distance;
return {(b.position.x-a.position.x)/d,(b.position.y-a.position.y)/d,(b.position.z-a.position.z)/d};
}
std::vector<PipeAnchor> anchors_;
};
}

View File

@@ -0,0 +1,12 @@
#pragma once
#include "FluidSim/AudioProcessing.hpp"
#include <algorithm>
#include <cmath>
namespace fluidsim::port::desktop {
// Fixed absolute scale: vacuum blue, atmosphere green, >=1 MPa red.
inline Vec3 pressure_tint(float pressure) {
if(pressure<=101325) {float t=std::clamp(pressure/101325,0.f,1.f);return {.05f,.15f+.70f*t,1-.95f*t};}
float t=std::sqrt(std::clamp((pressure-101325)/(1000000-101325),0.f,1.f));
return {.05f+.95f*t,.85f-.80f*t,.05f-.03f*t};
}
}

View File

@@ -0,0 +1,7 @@
#pragma once
#include "FluidSim/ScenarioCatalog.hpp"
#include <SFML/Graphics.hpp>
#include <optional>
namespace fluidsim::port::desktop {
std::optional<std::size_t> choose_scenario(sf::RenderWindow&,const sf::Font*,const std::vector<ScenarioExample>&);
}

View File

@@ -9,13 +9,12 @@ namespace fluidsim::port {
// the twin already protects its own simulation state. // the twin already protects its own simulation state.
class DesktopScenario { class DesktopScenario {
public: public:
explicit DesktopScenario(ScenarioKind kind, TwoStrokeConfig config = {}, bool saab_turbo = true) { explicit DesktopScenario(ScenarioKind kind, TwoStrokeConfig config = {}, ExhaustConfig exhaust = {}, V8ExhaustConfig v8 = {}, EngineConfig engine = {}, std::string name = {}) : name_(std::move(name)) {
if (kind == ScenarioKind::TwoStroke) if (kind == ScenarioKind::TwoStroke)
two_ = std::make_unique<TwoStrokeScenario>(44100, config); two_ = std::make_unique<TwoStrokeScenario>(44100, config);
else else
twin_ = std::make_unique<FourStrokeScenario>(44100, kind == ScenarioKind::SaabB205 ? twin_ = std::make_unique<FourStrokeScenario>(44100, kind == ScenarioKind::SaabB205 ?
FourStrokePreset::SaabB205 : kind == ScenarioKind::AmericanV8 ? FourStrokePreset::AmericanV8 : FourStrokePreset::Twin, FourStrokePreset::SaabB205 : kind == ScenarioKind::AmericanV8 ? FourStrokePreset::AmericanV8 : FourStrokePreset::Twin, exhaust, v8,engine);
kind == ScenarioKind::TurboTwin || (kind == ScenarioKind::SaabB205 && saab_turbo));
} }
AudioFrame process() { AudioFrame process() {
if (twin_) if (twin_)
@@ -26,23 +25,23 @@ public:
void set_controls(float pedal, float load, float clutch) { void set_controls(float pedal, float load, float clutch) {
visit([&](auto& scenario) { scenario.set_controls(pedal, load, clutch); }); visit([&](auto& scenario) { scenario.set_controls(pedal, load, clutch); });
} }
void set_starter(bool engaged) { visit([&](auto& scenario) { scenario.set_starter(engaged); }); }
void shift_up() { visit([](auto& scenario) { scenario.shift_up(); }); } void shift_up() { visit([](auto& scenario) { scenario.shift_up(); }); }
void shift_down() { visit([](auto& scenario) { scenario.shift_down(); }); } void shift_down() { visit([](auto& scenario) { scenario.shift_down(); }); }
void set_neutral() { visit([](auto& scenario) { scenario.set_neutral(); }); } void set_neutral() { visit([](auto& scenario) { scenario.set_neutral(); }); }
void toggle_mechanical_sound() { void set_pipe_solver(PipeSolver solver) { visit([&](auto& scenario) { scenario.set_pipe_solver(solver); }); }
if (twin_)
twin_->toggle_mechanical_sound();
}
void set_audio_scene(OutdoorScene scene) { void set_audio_scene(OutdoorScene scene) {
visit([&](auto& scenario) { scenario.set_audio_scene(scene); }); visit([&](auto& scenario) { scenario.set_audio_scene(scene); });
} }
ScenarioSnapshot snapshot() const { ScenarioSnapshot snapshot() const {
if (twin_) if (twin_) { auto s=twin_->snapshot();s.example_name=name_;return s; }
return twin_->snapshot();
const std::lock_guard lock(mutex_); const std::lock_guard lock(mutex_);
const auto source = two_->snapshot(); const auto source = two_->snapshot();
ScenarioSnapshot s; ScenarioSnapshot s;
s.example_name=name_;
s.pipe_solver=two_->pipes.front().solver;
s.two_stroke = true; s.two_stroke = true;
s.cylinder_angles[0]=two_->crankshaft.angle;
s.cylinder_count = 1; s.cylinder_count = 1;
s.racing_exhaust = two_->configuration.exhaust == TwoStrokeExhaust::Racing; s.racing_exhaust = two_->configuration.exhaust == TwoStrokeExhaust::Racing;
s.rpm = source.rpm; s.rpm = source.rpm;
@@ -81,6 +80,7 @@ private:
if (twin_) function(*twin_); if (twin_) function(*twin_);
else function(*two_); else function(*two_);
} }
std::string name_;
std::unique_ptr<FourStrokeTwinScenario> twin_; std::unique_ptr<FourStrokeTwinScenario> twin_;
std::unique_ptr<TwoStrokeScenario> two_; std::unique_ptr<TwoStrokeScenario> two_;
mutable std::mutex mutex_; mutable std::mutex mutex_;

View File

@@ -6,12 +6,14 @@ namespace fluidsim::port {
// Ideal sensors and injectors; calibration parameters are not engine measurements. // Ideal sensors and injectors; calibration parameters are not engine measurements.
class Ecu { class Ecu {
public: public:
enum class IdleControl { Feedback, FixedOpening };
IdleControl idle_control{IdleControl::Feedback};
struct IgnitionPoint { float rpm, degrees_btdc; }; struct IgnitionPoint { float rpm, degrees_btdc; };
std::vector<IgnitionPoint> ignition_map{{0, 10}, {15000, 40}}; std::vector<IgnitionPoint> ignition_map{{0, 10}, {15000, 40}};
float idle_rpm{1800.0f}; float idle_rpm{1800.0f};
float idle_base_opening{0.08f}, idle_max_opening{0.25f}; float idle_base_opening{0.08f}, idle_max_opening{0.25f};
float idle_kp{0.0001f}, idle_ki{0.00004f}; float idle_kp{0.0001f}, idle_ki{0.00004f};
float throttle_time_constant{0.08f}, throttle_rate{2.0f}; // seconds, full travel/second float throttle_time_constant{0.001f}, throttle_rate{1000.0f}; // seconds, full travel/second
// Zero uses the opening calibration, preserving symmetric response. // Zero uses the opening calibration, preserving symmetric response.
float throttle_closing_time_constant{}, throttle_closing_rate{}; float throttle_closing_time_constant{}, throttle_closing_rate{};
float rev_limit_rpm{10000.0f}, rev_resume_rpm{9750.0f}; float rev_limit_rpm{10000.0f}, rev_resume_rpm{9750.0f};
@@ -20,12 +22,6 @@ public:
bool overrun_fuel_cut_enabled{true}; bool overrun_fuel_cut_enabled{true};
float idle_spark_gain{0.0f}, idle_minimum_advance{-5.0f}; // degrees/RPM, degrees BTDC float idle_spark_gain{0.0f}, idle_minimum_advance{-5.0f}; // degrees/RPM, degrees BTDC
float stoichiometric_afr{14.7f}, power_afr{12.8f}; float stoichiometric_afr{14.7f}, power_afr{12.8f};
bool boost_control_enabled{false};
float maximum_boost{45000}; // Pa gauge, estimated low-pressure B205 calibration
float boost_kp{1e-5f}, boost_ki{4e-6f};
void control_boost(float dt, float pedal, float rpm, float manifold, float charge, float ambient = 101325);
float boost_target() const { return boost_target_; }
float wastegate_duty() const { return boost_duty_; }
void step(float dt, float pedal, float rpm); void step(float dt, float pedal, float rpm);
float fuel_for_air(float trapped_air_mass) const; float fuel_for_air(float trapped_air_mass) const;
@@ -33,14 +29,12 @@ public:
float throttle_target() const { return target_; } float throttle_target() const { return target_; }
float target_afr() const { return afr_; } float target_afr() const { return afr_; }
float spark_advance() const { return spark_; } float spark_advance() const { return spark_; }
bool fuel_cut() const { return limiter_ || (overrun_ && overrun_fuel_cut_enabled) || fault_ || boost_cut_; } bool fuel_cut() const { return limiter_ || (overrun_ && overrun_fuel_cut_enabled) || fault_; }
bool rev_limited() const { return limiter_; } bool rev_limited() const { return limiter_; }
bool idle_active() const { return idle_; } bool idle_active() const { return idle_; }
bool sensor_fault() const { return fault_; } bool sensor_fault() const { return fault_; }
private: private:
float boost_target_{}, boost_duty_{}, boost_integral_{}, filtered_boost_{};
bool boost_cut_{};
float filtered_rpm_{}; float filtered_rpm_{};
bool initialized_{}; bool initialized_{};
float opening_{}, target_{}, integral_{}, afr_{14.7f}, spark_{10.0f}; float opening_{}, target_{}, integral_{}, afr_{14.7f}, spark_{10.0f};

View File

@@ -1,6 +1,8 @@
#pragma once #pragma once
#include "FluidSim/Engine.hpp" #include "FluidSim/Engine.hpp"
#include "FluidSim/OpenEndRadiation.hpp"
#include "FluidSim/PipeSolver.hpp"
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <deque> #include <deque>
@@ -165,10 +167,14 @@ struct GasState {
class Pipe1D { class Pipe1D {
public: public:
std::vector<float> linear_resistance, quadratic_resistance;
// Integrated face fluxes: kg/s, N, W, kg/s of fresh air and fuel, positive along +x. // Integrated face fluxes: kg/s, N, W, kg/s of fresh air and fuel, positive along +x.
struct Flux { struct Flux {
float m{}, p{}, e{}, y{}, fuel{}; float m{}, p{}, e{}, y{}, fuel{};
}; };
struct RoeState { double root_density{}, enthalpy{}; };
PipeSolver solver{PipeSolver::Rusanov};
std::uint64_t hllc_fallbacks{};
float area{}, dx{}; float area{}, dx{};
float damping_multiplier{0.0f}, energy_relaxation_rate{0.0f}, ambient_pressure{101325.0f}; float damping_multiplier{0.0f}, energy_relaxation_rate{0.0f}, ambient_pressure{101325.0f};
std::vector<float> cell_areas, cell_lengths; std::vector<float> cell_areas, cell_lengths;
@@ -180,7 +186,7 @@ public:
Pipe1D(std::vector<float> areas, std::vector<float> lengths) Pipe1D(std::vector<float> areas, std::vector<float> lengths)
: area(areas.empty() ? 0 : areas.front()), dx(lengths.empty() ? 0 : lengths.front()), : area(areas.empty() ? 0 : areas.front()), dx(lengths.empty() ? 0 : lengths.front()),
cell_areas(std::move(areas)), cell_lengths(std::move(lengths)), cells(cell_areas.size()), cell_areas(std::move(areas)), cell_lengths(std::move(lengths)), cells(cell_areas.size()),
next_cells_(cells.size()), states_(cells.size()), waves_(cells.size()), faces_(cells.size() + 1) { next_cells_(cells.size()), states_(cells.size()), waves_(cells.size()), faces_(cells.size() + 1), roe_states_(cells.size()) {
if (cells.empty() || cell_lengths.size() != cells.size()) if (cells.empty() || cell_lengths.size() != cells.size())
throw std::invalid_argument("pipe profiles must be nonempty and have equal length"); throw std::invalid_argument("pipe profiles must be nonempty and have equal length");
for (std::size_t i = 0; i < cells.size(); ++i) for (std::size_t i = 0; i < cells.size(); ++i)
@@ -219,6 +225,12 @@ public:
throw std::runtime_error("nonphysical pipe state in preparation"); throw std::runtime_error("nonphysical pipe state in preparation");
dt = std::min(dt, cfl * cell_lengths[i] / waves_[i]); dt = std::min(dt, cfl * cell_lengths[i] / waves_[i]);
} }
if (solver != PipeSolver::Rusanov)
for (std::size_t i=0; i<cells.size(); ++i) {
const auto& s=states_[i];
const double energy=s.pressure*pipe_gm1_inverse+.5*double(s.rho)*s.velocity*s.velocity;
roe_states_[i]={std::sqrt(s.rho),(energy+s.pressure)/s.rho};
}
prepared_ = true; prepared_ = true;
return dt; return dt;
} }
@@ -251,6 +263,70 @@ public:
static Flux riemann(const GasState &a, const GasState &b) { static Flux riemann(const GasState &a, const GasState &b) {
return riemann(a, b, std::max(wave(a), wave(b))); return riemann(a, b, std::max(wave(a), wave(b)));
} }
// HLLE/HLLC use bounds enclosing endpoint and Roe acoustic speeds.
template<PipeSolver method>
static Flux flux_kernel(const GasState& a, const GasState& b,
float ca, float cb, bool* fallback = nullptr,
const RoeState* left_roe = nullptr, const RoeState* right_roe = nullptr) {
if constexpr (method == PipeSolver::Rusanov)
return riemann(a,b,std::max(std::abs(a.velocity)+ca,std::abs(b.velocity)+cb));
const double ra=left_roe?left_roe->root_density:std::sqrt(a.rho);
const double rb=right_roe?right_roe->root_density:std::sqrt(b.rho), sum=ra+rb;
const double ea=a.pressure*pipe_gm1_inverse+.5*double(a.rho)*a.velocity*a.velocity;
const double eb=b.pressure*pipe_gm1_inverse+.5*double(b.rho)*b.velocity*b.velocity;
const double ua=a.velocity, ub=b.velocity;
const double uroe=(ra*ua+rb*ub)/sum;
const double ha=left_roe?left_roe->enthalpy:(ea+a.pressure)/a.rho;
const double hb=right_roe?right_roe->enthalpy:(eb+b.pressure)/b.rho;
const double hroe=(ra*ha+rb*hb)/sum;
const double croe=std::sqrt(std::max(0.,double(gamma-1)*(hroe-.5*uroe*uroe)));
const double sl=std::min({ua-ca,ub-cb,uroe-croe});
const double sr=std::max({ua+ca,ub+cb,uroe+croe});
const auto physical=[](const GasState& q,double energy) {
const float m=q.rho*q.velocity;
return Flux{m,m*q.velocity+q.pressure,float((energy+q.pressure)*q.velocity),m*q.air,m*q.fuel};
};
const Flux fa=physical(a,ea), fb=physical(b,eb);
if(sl>=0) return fa;
if(sr<=0) return fb;
const auto hlle=[&] {
const double inv=1/(sr-sl), diff=sl*sr;
return Flux{float((sr*fa.m-sl*fb.m+diff*(b.rho-a.rho))*inv),
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)};
};
if constexpr(method==PipeSolver::Hlle) return hlle();
const double dl=a.rho*(sl-ua), dr=b.rho*(sr-ub);
const double sm=(b.pressure-a.pressure+dl*ua-dr*ub)/(dl-dr);
const double ps=a.pressure+dl*(sm-ua);
// Reject nonphysical HLLC intermediate states without modifying conserved cells.
const bool left=sm>=0;
const auto& q=left?a:b;
const auto f=left?fa:fb;
const double speed=left?sl:sr;
const double inv=1/(speed-sm);
const double density=(left?dl:dr)*inv;
const double energy=((speed-q.velocity)*(left?ea:eb)-q.pressure*q.velocity+ps*sm)*inv;
if(!std::isfinite(sm) || !(sl<sm && sm<sr) || !(ps>0) || !(density>0) ||
!(energy>.5*density*sm*sm)) {
if(fallback) *fallback=true;
return hlle();
}
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)};
}
static Flux numerical_flux(const GasState& a,const GasState& b,PipeSolver solver,
float ca,float cb,bool* fallback=nullptr) {
if(solver==PipeSolver::Rusanov) return flux_kernel<PipeSolver::Rusanov>(a,b,ca,cb,fallback);
if(solver==PipeSolver::Hlle) return flux_kernel<PipeSolver::Hlle>(a,b,ca,cb,fallback);
return flux_kernel<PipeSolver::Hllc>(a,b,ca,cb,fallback);
}
static Flux numerical_flux(const GasState& a,const GasState& b,PipeSolver solver) {
return numerical_flux(a,b,solver,std::sqrt(gamma*a.pressure/a.rho),std::sqrt(gamma*b.pressure/b.rho));
}
void advance(float dt) { void advance(float dt) {
if (dt <= 0) if (dt <= 0)
return; return;
@@ -259,12 +335,27 @@ public:
const std::size_t n = cells.size(); const std::size_t n = cells.size();
if (!prepared_) if (!prepared_)
prepare_step(); prepare_step();
faces_[0] = left_override_ ? left_flux_ : scaled(riemann(left_ghost, states_[0]), cell_areas[0]); faces_[0] = left_override_ ? left_flux_ : scaled(numerical_flux(left_ghost, states_[0], solver), cell_areas[0]);
faces_[n] = faces_[n] =
right_override_ ? right_flux_ : scaled(riemann(states_[n - 1], right_ghost), cell_areas[n - 1]); right_override_ ? right_flux_ : scaled(numerical_flux(states_[n - 1], right_ghost, solver), cell_areas[n - 1]);
for (std::size_t i = 1; i < n; ++i) if (solver == PipeSolver::Rusanov) {
faces_[i] = scaled(riemann(states_[i - 1], states_[i], std::max(waves_[i - 1], waves_[i])), for (std::size_t i = 1; i < n; ++i)
.5f * (cell_areas[i - 1] + cell_areas[i])); faces_[i] = scaled(riemann(states_[i-1], states_[i], std::max(waves_[i-1], waves_[i])),
.5f*(cell_areas[i-1]+cell_areas[i]));
} else {
const auto fill_faces=[&]<PipeSolver method>() {
for (std::size_t i = 1; i < n; ++i) {
bool fallback=false;
faces_[i] = scaled(flux_kernel<method>(states_[i-1], states_[i],
waves_[i-1]-std::abs(states_[i-1].velocity), waves_[i]-std::abs(states_[i].velocity),
&fallback, &roe_states_[i-1], &roe_states_[i]),
.5f*(cell_areas[i-1]+cell_areas[i]));
hllc_fallbacks += fallback;
}
};
if(solver==PipeSolver::Hlle) fill_faces.template operator()<PipeSolver::Hlle>();
else fill_faces.template operator()<PipeSolver::Hllc>();
}
const float relaxation = energy_relaxation_rate > 0 ? std::exp(-energy_relaxation_rate * dt) : 1.0f; const float relaxation = energy_relaxation_rate > 0 ? std::exp(-energy_relaxation_rate * dt) : 1.0f;
for (std::size_t i = 0; i < n; ++i) { for (std::size_t i = 0; i < n; ++i) {
const auto fl = faces_[i], fr = faces_[i + 1]; const auto fl = faces_[i], fr = faces_[i + 1];
@@ -283,6 +374,11 @@ public:
rho = 1e-12f; rho = 1e-12f;
++positivity_repairs; ++positivity_repairs;
} }
if (!linear_resistance.empty()) {
const float speed = std::abs(mom/rho);
mom /= 1 + dt*(linear_resistance[i] + .5f*quadratic_resistance[i]*speed);
// Retain total energy: dissipated kinetic energy becomes heat.
}
if (damping_multiplier > 0) if (damping_multiplier > 0)
mom *= std::exp(-.1f * damping_multiplier * dt / cells[i].rho); mom *= std::exp(-.1f * damping_multiplier * dt / cells[i].rho);
if (energy_relaxation_rate > 0) if (energy_relaxation_rate > 0)
@@ -315,6 +411,7 @@ private:
std::vector<GasState> states_; std::vector<GasState> states_;
std::vector<float> waves_; std::vector<float> waves_;
std::vector<Flux> faces_; std::vector<Flux> faces_;
std::vector<RoeState> roe_states_;
Flux left_flux_{}, right_flux_{}; Flux left_flux_{}, right_flux_{};
bool left_override_{}, right_override_{}, prepared_{}; bool left_override_{}, right_override_{}, prepared_{};
static std::vector<float> uniform(float v, int n) { static std::vector<float> uniform(float v, int n) {
@@ -387,6 +484,19 @@ public:
claim(pipe, left); claim(pipe, left);
opens_.push_back({pipe, left, p, a}); opens_.push_back({pipe, left, p, a});
} }
// Full circular mouth radiating into free space; no flange or mean-flow correction.
void add_radiating_open_end(int pipe, bool left, float pressure, float area, float temperature = 300) {
if (!std::isfinite(temperature) || temperature <= 0)
throw std::invalid_argument("invalid ambient temperature");
if (pipe < 0 || static_cast<std::size_t>(pipe) >= pipes_.size())
throw std::invalid_argument("invalid boundary pipe index");
const float face_area = pipes_[pipe].cell_area(end(pipes_[pipe], left));
if (std::abs(area-face_area) > 1e-6f*face_area)
throw std::invalid_argument("radiating mouth must match pipe face area");
add_open_end(pipe, left, pressure, area);
opens_.back().radiating = true;
opens_.back().temperature = temperature;
}
// An ideal, zero-volume junction between two duct faces. Orientation is explicit. // An ideal, zero-volume junction between two duct faces. Orientation is explicit.
void add_connection(int a, bool left_a, int b, bool left_b) { void add_connection(int a, bool left_a, int b, bool left_b) {
if (a < 0 || b < 0 || static_cast<std::size_t>(a) >= pipes_.size() || if (a < 0 || b < 0 || static_cast<std::size_t>(a) >= pipes_.size() ||
@@ -404,7 +514,23 @@ public:
claim(b, left_b); claim(b, left_b);
connections_.push_back({a, b, left_a, left_b}); connections_.push_back({a, b, left_a, left_b});
} }
struct JunctionFace { int pipe; bool left; };
void add_junction(std::initializer_list<JunctionFace> ends) {
if(ends.size()<2) throw std::invalid_argument("junction needs at least two faces");
JunctionNode node;
for(auto e:ends) {
check_face(e.pipe,e.left);
for(auto previous:node.ends) {
if(previous.pipe==e.pipe && previous.left==e.left) throw std::invalid_argument("duplicate junction face");
}
node.ends.push_back(e);
}
for(auto e:ends) claim(e.pipe,e.left);
node.states.resize(ends.size());
nodes_.push_back(std::move(node));
}
float open_end_mass_flow(std::size_t i) const { return opens_.at(i).last_mass_flow; } float open_end_mass_flow(std::size_t i) const { return opens_.at(i).last_mass_flow; }
float open_end_volume_flow(std::size_t i) const { return opens_.at(i).last_volume_flow; }
float open_end_pressure(std::size_t i) const { return opens_.at(i).last_face_pressure; } float open_end_pressure(std::size_t i) const { return opens_.at(i).last_face_pressure; }
std::uint64_t limited_fluxes{}, nonlinear_iterations{}; std::uint64_t limited_fluxes{}, nonlinear_iterations{};
void validate_topology() const { void validate_topology() const {
@@ -421,12 +547,52 @@ public:
for (auto &o : orifices_) for (auto &o : orifices_)
resolve_orifice(o, dt); resolve_orifice(o, dt);
for (auto &o : opens_) for (auto &o : opens_)
resolve_open(o); resolve_open(o, dt);
for (auto &o : connections_) for (auto &o : connections_)
resolve_connection(o); resolve_connection(o);
for(auto& node:nodes_) resolve_junction(node,dt);
} }
private: private:
struct JunctionNode {
std::vector<JunctionFace> ends;
std::vector<GasState> states;
};
std::vector<JunctionNode> nodes_;
// Zero-storage Rusanov star state. Integrated mass, total energy and
// species fluxes sum to zero algebraically across every connected face.
// The node has no conserved state, timestep or storage volume of its own.
void resolve_junction(JunctionNode& node,float) {
double speed=0;
for(std::size_t k=0;k<node.ends.size();++k) {
auto e=node.ends[k];auto& p=pipes_[e.pipe];auto s=p.state(end(p,e.left));
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;
for(std::size_t k=0;k<node.ends.size();++k) {
auto e=node.ends[k];auto& p=pipes_[e.pipe];const auto s=node.states[k];
const double a=p.cell_area(end(p,e.left)), u=s.velocity;
const double en=s.pressure/pipe_gm1+.5*s.rho*u*u;
area_sum+=a;rho+=a*s.rho*(1+u/speed);
energy+=a*(en+(en+s.pressure)*u/speed);
air+=a*s.rho*s.air*(1+u/speed);fuel+=a*s.rho*s.fuel*(1+u/speed);
}
rho/=area_sum;energy/=area_sum;air/=area_sum;fuel/=area_sum;
const double pressure=energy*pipe_gm1;
for(std::size_t k=0;k<node.ends.size();++k) {
auto e=node.ends[k];auto& p=pipes_[e.pipe];const auto s=node.states[k];
const double a=p.cell_area(end(p,e.left)), u=s.velocity;
const double en=s.pressure/pipe_gm1+.5*s.rho*u*u;
const double orientation=e.left?-1:1;
const double m=.5*a*(s.rho*u-speed*(rho-s.rho));
const double heat=.5*a*((en+s.pressure)*u-speed*(energy-en));
const double y=.5*a*(s.rho*s.air*u-speed*(air-s.rho*s.air));
const double f=.5*a*(s.rho*s.fuel*u-speed*(fuel-s.rho*s.fuel));
const double momentum=.5*a*(s.rho*u*u+s.pressure+pressure+speed*s.rho*u);
p.set_boundary_flux(e.left,{float(orientation*m),float(momentum),float(orientation*heat),float(orientation*y),float(orientation*f)});
}
}
struct Budget { struct Budget {
float *air{}, *exhaust{}, *fuel{}, *energy{}; float *air{}, *exhaust{}, *fuel{}, *energy{};
float mass{}, heat{}; float mass{}, heat{};
@@ -443,7 +609,10 @@ private:
struct Open { struct Open {
int pipe; int pipe;
bool left; bool left;
float pressure, area, last_mass_flow{}, last_face_pressure{}; float pressure, area, last_mass_flow{}, last_face_pressure{}, last_volume_flow{};
bool radiating{};
float temperature{300};
OpenEndRadiation reflection{};
}; };
struct Connection { struct Connection {
int a, b; int a, b;
@@ -630,7 +799,7 @@ private:
if (!from) if (!from)
v.fuel_fraction = s.fuel; v.fuel_fraction = s.fuel;
} }
void resolve_open(Open &o) { void resolve_open(Open &o, float dt) {
auto &p = pipes_[o.pipe]; auto &p = pipes_[o.pipe];
int i = end(p, o.left); int i = end(p, o.left);
auto s = p.state(i); auto s = p.state(i);
@@ -638,6 +807,40 @@ private:
if (o.left ? s.velocity >= c : s.velocity <= -c) if (o.left ? s.velocity >= c : s.velocity <= -c)
throw std::runtime_error( throw std::runtime_error(
"supersonic inlet needs full upstream state; pressure-only boundary is insufficient"); "supersonic inlet needs full upstream state; pressure-only boundary is insufficient");
if (o.radiating) {
const float direction = o.left ? -1.f : 1.f;
const double outward = direction*s.velocity;
GasState face = s;
if (outward < c) {
const double reference_c = c*std::pow(double(o.pressure)/s.pressure, (gamma-1)/(2*gamma));
const double incident = outward + 2*(c-reference_c)/(gamma-1);
const double radius = std::sqrt(o.area/pi);
const double ambient_c = std::sqrt(gamma*gas_constant*o.temperature);
const double reflected = o.reflection.process(incident, dt*ambient_c/radius);
const double sound = reference_c + (gamma-1)*.25*(incident+reflected);
if (!(sound > 0)) throw std::runtime_error("radiation characteristic outside subsonic model");
face.pressure = o.pressure*std::pow(sound/reference_c, 2*gamma/(gamma-1));
face.velocity = direction*.5*(incident-reflected);
face.rho = s.rho*std::pow(face.pressure/s.pressure, 1/gamma);
if (direction*face.velocity < 0) {
// Fresh ambient gas enters with ambient stagnation enthalpy.
const float temperature = o.temperature - .5f*face.velocity*face.velocity /
(gamma/(gamma-1)*gas_constant);
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;
}
}
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});
o.last_volume_flow = direction*face.velocity*o.area;
o.last_mass_flow = direction*mass_flux;
o.last_face_pressure = face.pressure;
return;
}
const float entropy = s.pressure / std::pow(s.rho, gamma); const float entropy = s.pressure / std::pow(s.rho, gamma);
const float rho = std::pow(o.pressure / entropy, 1 / gamma); const float rho = std::pow(o.pressure / entropy, 1 / gamma);
const float cb = std::sqrt(gamma * o.pressure / rho); const float cb = std::sqrt(gamma * o.pressure / rho);
@@ -653,6 +856,7 @@ private:
f.p += s.pressure * (p.cell_area(i) - a); f.p += s.pressure * (p.cell_area(i) - a);
p.set_boundary_flux(o.left, f); p.set_boundary_flux(o.left, f);
o.last_mass_flow = f.m * (o.left ? -1 : 1); o.last_mass_flow = f.m * (o.left ? -1 : 1);
o.last_volume_flow = o.last_mass_flow / (ambient_inflow ? rho : s.rho);
o.last_face_pressure = f.p / p.cell_area(i); o.last_face_pressure = f.p / p.cell_area(i);
} }
void resolve_connection(const Connection &o) { void resolve_connection(const Connection &o) {
@@ -664,7 +868,7 @@ private:
sa.velocity *= da; sa.velocity *= da;
sb.velocity *= db; sb.velocity *= db;
const float area = std::min(a.cell_area(ia), b.cell_area(ib)); const float area = std::min(a.cell_area(ia), b.cell_area(ib));
auto f = Pipe1D::scaled(Pipe1D::riemann(sa, sb), area); 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, a.set_boundary_flux(o.left_a,
{da * f.m, f.p + sa.pressure * (a.cell_area(ia) - area), da * f.e, {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});

View File

@@ -0,0 +1,34 @@
#pragma once
#include "FluidSim/Fluid.hpp"
#include <span>
namespace fluidsim::port {
struct JunctionEnd { int pipe; bool left; float discharge{1.f}; };
// Finite-volume N-port fitting: its specified physical volume stores gas and
// energy. All ports share pressure and mixture, with bidirectional nozzle flow.
// T, Y, cross and collectors share this model; angles require calibrated losses.
class Junction : public Volume0D {
public:
using Volume0D::Volume0D;
void connect(BoundarySystem& boundaries, const std::vector<Pipe1D>& pipes,
std::span<const JunctionEnd> ends) {
if(connected_ || ends.size()<2) throw std::invalid_argument("junction needs at least two distinct ends and one connection call");
for(std::size_t i=0;i<ends.size();++i) {
const auto& e=ends[i];
if(e.pipe<0 || std::size_t(e.pipe)>=pipes.size() || !std::isfinite(e.discharge) || e.discharge<=0 || e.discharge>1)
throw std::invalid_argument("invalid junction endpoint");
for(std::size_t j=0;j<i;++j) if(e.pipe==ends[j].pipe && e.left==ends[j].left)
throw std::invalid_argument("duplicate junction endpoint");
}
for(const auto& e:ends) {
const auto& pipe=pipes[e.pipe];
areas_.push_back(pipe.cell_area(e.left?0:int(pipe.cells.size())-1));
boundaries.add_orifice(create_port(),e.pipe,e.left,areas_.back(),e.discharge);
}
connected_=true;
}
private:
std::deque<float> areas_; // stable addresses for boundary area references
bool connected_{};
};
}

View File

@@ -1,49 +0,0 @@
#pragma once
#include "FluidSim/Engine.hpp"
#include <random>
namespace fluidsim::port {
// Synthetic valve, piston, combustion, chain, and gear sounds driven by live
// engine state; direct port of Core/MechanicalSoundProcessor.cs.
class MechanicalSoundProcessor {
public:
float master_gain{0.6f}, valve_tick_gain{0.9f}, piston_slap_gain{1.2f};
float combustion_gain{0.5f}, gear_whine_gain{0.12f}, cam_chain_gain{0.25f};
explicit MechanicalSoundProcessor(int sample_rate = 44100);
float process(const Crankshaft& crank, const EngineCylinder& cylinder, float load);
private:
class Resonator {
public:
Resonator(float frequency_hz, float q, float sample_rate);
float process(float input);
private:
float b1_{}, b2_{}, gain_{}, y1_{}, y2_{};
};
class OnePoleLowPass {
public:
OnePoleLowPass(float cutoff_hz, float sample_rate);
float process(float input);
private:
float alpha_{}, state_{};
};
float dt_{};
Resonator intake_tick_, exhaust_tick_;
OnePoleLowPass slap_filter_, combustion_filter_, chain_filter_;
float slap_envelope_{}, combustion_envelope_{}, chain_envelope_{};
float slap_decay_{}, combustion_decay_{}, chain_decay_{};
float gear_phase1_{}, gear_phase2_{}, gear_phase3_{};
float previous_intake_area_{}, previous_exhaust_area_{}, previous_pressure_{};
float previous_cycle_position_{-1.0f}, previous_crank_fraction_{-1.0f};
std::mt19937 random_{0x5EEDu};
std::uniform_real_distribution<float> unit_{-1.0f, 1.0f};
float random_sample();
static bool crossed_fraction(float previous, float current, float target);
};
} // namespace fluidsim::port

View File

@@ -10,11 +10,15 @@ class MotorcycleTest {
public: public:
Vehicle vehicle{}; Vehicle vehicle{};
float load{}, clutch{1.0f}; float load{}, clutch{1.0f};
float starter_torque{18.f}; // N m at the crankshaft while held
bool starter_engaged{};
void set_starter(bool engaged) { starter_engaged=engaged; }
protected: protected:
void step_motorcycle(Crankshaft& crank, float dt, float maximum_load_torque) { void step_motorcycle(Crankshaft& crank, float dt, float maximum_load_torque) {
crank.load_torque = std::clamp(load, 0.0f, 1.0f) * maximum_load_torque; crank.load_torque = std::clamp(load, 0.0f, 1.0f) * maximum_load_torque;
vehicle.clutch_input = std::clamp(clutch, 0.0f, 1.0f); vehicle.clutch_input = std::clamp(clutch, 0.0f, 1.0f);
if(starter_engaged) crank.add_torque(std::max(starter_torque,0.f));
crank.step(dt); crank.step(dt);
crank.drivetrain_torque = vehicle.update(crank.omega, crank.inertia, dt); crank.drivetrain_torque = vehicle.update(crank.omega, crank.inertia, dt);
} }

View File

@@ -0,0 +1,27 @@
#pragma once
#include <cmath>
namespace fluidsim::port {
// Causal unflanged circular-pipe reflection fit, Silva et al. (2009), Eq. 16.
// R(s)=-(1+n*a/c*s)/(1+d*a/c*s+e*(a/c*s)^2).
// Trapezoidal state integration supports the solver's variable CFL timestep.
class OpenEndRadiation {
public:
double process(double incident, double scaled_dt) {
if (!initialized_) { position_ = incident; previous_ = incident; initialized_ = true; }
constexpr double n = .167, d = n + 2*.6133;
constexpr double e = (d*d-n*n-1)/2;
const double h = scaled_dt*.5;
const double rhs = velocity_ + h/e*(previous_ + incident - position_ - d*velocity_);
const double next_velocity = (rhs - h/e*(position_ + h*velocity_)) /
(1 + h*d/e + h*h/e);
position_ += h*(velocity_ + next_velocity);
velocity_ = next_velocity;
previous_ = incident;
return -(position_ + n*velocity_);
}
private:
double position_{}, velocity_{}, previous_{};
bool initialized_{};
};
} // namespace fluidsim::port

View File

@@ -1,12 +1,14 @@
#pragma once #pragma once
#include "FluidSim/AudioProcessing.hpp" #include "FluidSim/AudioProcessing.hpp"
#include "FluidSim/OutdoorExhaustReverb.hpp"
#include <array> #include <array>
#include <atomic>
#include <cstddef> #include <cstddef>
#include <vector> #include <vector>
namespace fluidsim::port { namespace fluidsim::port {
enum class OutdoorScene { Dry, Field, Wall, Woodland }; enum class OutdoorScene { Dry, Field, Wall, Woodland, Tunnel };
const char *scene_name(OutdoorScene scene); const char *scene_name(OutdoorScene scene);
struct OutdoorAudioConfig { struct OutdoorAudioConfig {
@@ -28,9 +30,11 @@ struct OutdoorAudioConfig {
class OutdoorAudio { class OutdoorAudio {
public: public:
explicit OutdoorAudio(int sample_rate = 44100, OutdoorAudioConfig config = {}); explicit OutdoorAudio(int sample_rate = 44100, OutdoorAudioConfig config = {});
// Acoustic source inputs are ambient-density times mouth volume flow, kg/s.
// Fluid transport still uses actual donor-density mass flux independently.
AudioFrame process(float intake_mass_flow, float exhaust_mass_flow, float second_exhaust_mass_flow = 0); AudioFrame process(float intake_mass_flow, float exhaust_mass_flow, float second_exhaust_mass_flow = 0);
void set_scene(OutdoorScene scene) { scene_ = scene; } void set_scene(OutdoorScene scene) { requested_scene_.store(scene, std::memory_order_relaxed); }
OutdoorScene scene() const { return scene_; } OutdoorScene scene() const { return requested_scene_.load(std::memory_order_relaxed); }
void set_monitor_filter(bool enabled) { monitor_filter_ = enabled; } // Set before playback/capture. void set_monitor_filter(bool enabled) { monitor_filter_ = enabled; } // Set before playback/capture.
AudioFrame dry_frame() const { return last_dry_; } AudioFrame dry_frame() const { return last_dry_; }
AudioFrame unfiltered_frame() const { return last_raw_; } AudioFrame unfiltered_frame() const { return last_raw_; }
@@ -44,7 +48,7 @@ private:
std::size_t delay_left{}, delay_right{}; std::size_t delay_left{}, delay_right{};
float fraction_left{}, fraction_right{}, gain_left{}, gain_right{}, alpha{1}; float fraction_left{}, fraction_right{}, gain_left{}, gain_right{}, alpha{1};
float state_left{}, state_right{}; float state_left{}, state_right{};
int group{}; // direct, ground, wall, trees int group{}; // direct, ground, wall, trees, tunnel
}; };
struct Emitter { struct Emitter {
std::vector<float> filtered, raw; std::vector<float> filtered, raw;
@@ -53,22 +57,17 @@ private:
float previous_flow{}; float previous_flow{};
LowPass filter; LowPass filter;
}; };
struct TailLine {
std::vector<float> buffer;
std::size_t position{};
float gain{}, state{};
};
int rate_{}; int rate_{};
OutdoorAudioConfig config_; OutdoorAudioConfig config_;
OutdoorScene scene_{OutdoorScene::Wall}; std::atomic<OutdoorScene> requested_scene_{OutdoorScene::Wall};
OutdoorScene active_scene_{OutdoorScene::Wall};
bool monitor_filter_{true}; bool monitor_filter_{true};
std::array<Emitter, 3> emitters_; std::array<Emitter, 3> emitters_;
std::array<TailLine, 4> tail_; OutdoorExhaustReverb late_reverb_;
std::array<float, 4> weights_{1, 0, 0, 0}; std::array<float, 5> weights_{1, 0, 0, 0, 0};
float scene_alpha_{}, tail_alpha_{}, tail_mix_{}; float scene_alpha_{}, tail_mix_{};
AudioFrame last_dry_{}, last_raw_{}; AudioFrame last_dry_{}, last_raw_{};
void configure_emitter(Emitter &emitter, Vec3 position); void configure_emitter(Emitter &emitter, Vec3 position);
AudioFrame render(Emitter &emitter, float flow, AudioFrame &dry, AudioFrame &raw); AudioFrame render(Emitter &emitter, float flow, AudioFrame &dry, AudioFrame &raw);
AudioFrame diffuse(float input);
}; };
} // namespace fluidsim::port } // namespace fluidsim::port

View File

@@ -19,6 +19,7 @@ public:
explicit OutdoorExhaustReverb(int sample_rate = 44100); explicit OutdoorExhaustReverb(int sample_rate = 44100);
AudioFrame process_stereo(float input); AudioFrame process_stereo(float input);
float process(float input); float process(float input);
void set_decay_times(float low_seconds, float mid_seconds, float high_seconds);
private: private:
class DelayLine { class DelayLine {
@@ -40,7 +41,7 @@ private:
}; };
int sample_rate_{}; int sample_rate_{};
float dt_{}, gain_low_{}, gain_mid_{}, gain_high_{}; float dt_{}, average_delay_seconds_{}, gain_low_{}, gain_mid_{}, gain_high_{};
std::vector<DelayLine> fdn_; std::vector<DelayLine> fdn_;
std::vector<OnePoleLowPass> low_filters_, mid_filters_; std::vector<OnePoleLowPass> low_filters_, mid_filters_;
std::vector<float> read_buffer_, mix_buffer_; std::vector<float> read_buffer_, mix_buffer_;
@@ -49,6 +50,7 @@ private:
float fast_envelope_{}, slow_envelope_{}, bloom_{}; float fast_envelope_{}, slow_envelope_{}, bloom_{};
float fast_attack_{}, fast_release_{}, slow_attack_{}, slow_release_{}, bloom_decay_{}; float fast_attack_{}, fast_release_{}, slow_attack_{}, slow_release_{}, bloom_decay_{};
void update_feedback_gains();
static std::size_t delay_samples(int sample_rate, float milliseconds); static std::size_t delay_samples(int sample_rate, float milliseconds);
}; };

View File

@@ -0,0 +1,20 @@
#pragma once
#include <stdexcept>
#include <string_view>
namespace fluidsim::port {
enum class PipeSolver { Rusanov, Hlle, Hllc };
inline const char* pipe_solver_name(PipeSolver solver) {
switch(solver) {
case PipeSolver::Rusanov: return "rusanov";
case PipeSolver::Hlle: return "hlle";
case PipeSolver::Hllc: return "hllc";
}
return "unknown";
}
inline PipeSolver parse_pipe_solver(std::string_view name) {
if(name=="rusanov") return PipeSolver::Rusanov;
if(name=="hlle") return PipeSolver::Hlle;
if(name=="hllc") return PipeSolver::Hllc;
throw std::invalid_argument("choose rusanov, hlle or hllc pipe solver");
}
} // namespace fluidsim::port

View File

@@ -1,16 +1,17 @@
#pragma once #pragma once
#include "FluidSim/Silencer.hpp"
#include "FluidSim/Carburetor.hpp"
#include <string>
#include "FluidSim/Junction.hpp"
#include "FluidSim/AudioProcessing.hpp" #include "FluidSim/AudioProcessing.hpp"
#include "FluidSim/Atmosphere.hpp"
#include "FluidSim/Engine.hpp" #include "FluidSim/Engine.hpp"
#include "FluidSim/Ecu.hpp" #include "FluidSim/Ecu.hpp"
#include "FluidSim/Fluid.hpp" #include "FluidSim/Fluid.hpp"
#include "FluidSim/MechanicalSoundProcessor.hpp"
#include "FluidSim/OutdoorAudio.hpp" #include "FluidSim/OutdoorAudio.hpp"
#include "FluidSim/Solver.hpp" #include "FluidSim/Solver.hpp"
#include "FluidSim/Vehicle.hpp" #include "FluidSim/Vehicle.hpp"
#include "FluidSim/MotorcycleTest.hpp" #include "FluidSim/MotorcycleTest.hpp"
#include "FluidSim/Turbocharger.hpp"
#include <memory> #include <memory>
#include <array> #include <array>
#include <cstdint> #include <cstdint>
@@ -19,34 +20,60 @@
namespace fluidsim::port { namespace fluidsim::port {
enum class ScenarioKind { Twin, TwoStroke, SaabB205, TurboTwin, AmericanV8 }; enum class ScenarioKind { Twin, TwoStroke, SaabB205, AmericanV8 };
enum class FourStrokePreset { Twin, SaabB205, AmericanV8 }; enum class FourStrokePreset { Twin, SaabB205, AmericanV8 };
enum class EngineLayout { Inline, V, Boxer };
struct EngineConfig {
int cylinders{}; // zero selects the existing preset
EngineLayout layout{EngineLayout::Inline};
float bank_angle{60}, bore{.086f}, stroke{.086f}, rod{.145f}, compression{10};
float idle_rpm{1000}, rev_limit{7000}, idle_opening{.045f};
bool carbureted{};
float carb_pilot_scale{1.f};
float carb_jet_scale{1.f}; // multiplier on displacement-scaled effective jet diameters
std::array<float,12> phase_degrees{}; // used when custom_phases is true
bool custom_phases{};
};
struct V8ExhaustConfig {
float primary_length{.75f}, primary_diameter{.038f};
bool h_pipe{true};
float junction_volume{50e-6f}; // m3, 50 cc per fitting
};
struct ScenarioSnapshot { struct ScenarioSnapshot {
bool turbo_enabled{}; std::string example_name;
float boost_target{}, wastegate_duty{}; EngineLayout layout{EngineLayout::Inline};
TurboSnapshot turbo{}; float bank_angle{};
bool v8_headers{};
PipeSolver pipe_solver{PipeSolver::Rusanov};
int cylinder_count{2}; int cylinder_count{2};
float rev_limit_rpm{10000}; float rev_limit_rpm{10000};
std::array<float, 8> cylinder_pressures{}, piston_fractions{}, intake_areas{}, exhaust_areas{}; std::array<float, 12> cylinder_angles{};
std::uint64_t solver_substeps{}, boundary_iterations{}; std::array<float, 12> cylinder_pressures{}, piston_fractions{}, intake_areas{}, exhaust_areas{};
std::uint64_t solver_substeps{}, boundary_iterations{}, pipe_positivity_repairs{};
float rpm{}, speed_kmh{}, kinetic_energy{}, average_power{}, average_torque{}; float rpm{}, speed_kmh{}, kinetic_energy{}, average_power{}, average_torque{};
float cylinder_pressure1{}, cylinder_pressure2{}; float cylinder_pressure1{}, cylinder_pressure2{};
float cylinder_temperature1{}, cylinder_temperature2{}; float cylinder_temperature1{}, cylinder_temperature2{};
float cylinder_piston_fraction1{}, cylinder_piston_fraction2{}; float cylinder_piston_fraction1{}, cylinder_piston_fraction2{};
float valve1_intake_area{}, valve1_exhaust_area{}, valve2_intake_area{}, valve2_exhaust_area{}; float valve1_intake_area{}, valve1_exhaust_area{}, valve2_intake_area{}, valve2_exhaust_area{};
float intake_pressure{}, exhaust_pressure{}; float intake_pressure{}, exhaust_pressure{}, second_bank_pressure{};
std::array<float,2> crossover_pressures{};
float throttle{}, pedal{}, target_afr{}, clutch{}, intake_flow{}, exhaust_flow{}; float throttle{}, pedal{}, target_afr{}, clutch{}, intake_flow{}, exhaust_flow{};
bool idle_control{}, fuel_cut{}, two_stroke{}, reed_open{}; bool idle_control{}, fuel_cut{}, two_stroke{}, reed_open{};
bool racing_exhaust{}; bool racing_exhaust{};
bool silencers{}, h_pipe{};
ExhaustConfig exhaust_config{};
float crankcase_pressure{}; float crankcase_pressure{};
OutdoorScene audio_scene{OutdoorScene::Wall}; OutdoorScene audio_scene{OutdoorScene::Wall};
int gear{}; int gear{};
bool rev_limit1{}, rev_limit2{}, mechanical_sound_enabled{}; bool rev_limit1{}, rev_limit2{};
std::array<std::vector<GasState>, 22> pipe_states; std::array<std::vector<GasState>, 32> pipe_states;
std::array<std::vector<float>, 22> pipe_areas, pipe_lengths; std::array<std::vector<float>, 32> pipe_areas, pipe_lengths;
void capture_pipes(const std::vector<Pipe1D>& pipes) { void capture_pipes(const std::vector<Pipe1D>& pipes) {
for (std::size_t k = 0; k < pipes.size(); ++k) { for (std::size_t k = 0; k < pipes.size(); ++k) {
pipe_positivity_repairs += pipes[k].positivity_repairs;
pipe_areas[k] = pipes[k].cell_areas; pipe_areas[k] = pipes[k].cell_areas;
pipe_lengths[k] = pipes[k].cell_lengths; pipe_lengths[k] = pipes[k].cell_lengths;
pipe_states[k].reserve(pipes[k].cells.size()); pipe_states[k].reserve(pipes[k].cells.size());
@@ -60,17 +87,20 @@ struct ScenarioSnapshot {
// Presets include a 600 cc twin, B205-sized inline-four and American 5.0 V8. // Presets include a 600 cc twin, B205-sized inline-four and American 5.0 V8.
class FourStrokeScenario : public MotorcycleTest { class FourStrokeScenario : public MotorcycleTest {
public: public:
explicit FourStrokeScenario(int sample_rate = 44100, FourStrokePreset preset = FourStrokePreset::Twin, explicit FourStrokeScenario(int sample_rate = 44100, FourStrokePreset preset = FourStrokePreset::Twin, ExhaustConfig exhaust = {}, V8ExhaustConfig v8_exhaust = {}, EngineConfig engine = {});
bool turbo_enabled = false);
const FourStrokePreset preset; const FourStrokePreset preset;
int cylinder_count() const { return preset == FourStrokePreset::AmericanV8 ? 8 : preset == FourStrokePreset::SaabB205 ? 4 : 2; } const ExhaustConfig exhaust_config;
std::array<FourStrokeCylinder*, 8> cylinders() { return {&cylinder1, &cylinder2, &cylinder3, &cylinder4, const V8ExhaustConfig v8_exhaust_config;
&cylinder5, &cylinder6, &cylinder7, &cylinder8}; } const EngineConfig engine_config;
int cylinder_count() const { return engine_config.cylinders ? engine_config.cylinders : preset == FourStrokePreset::AmericanV8 ? 8 : preset == FourStrokePreset::SaabB205 ? 4 : 2; }
std::array<FourStrokeCylinder*, 12> cylinders() { return {&cylinder1, &cylinder2, &cylinder3, &cylinder4,
&cylinder5, &cylinder6, &cylinder7, &cylinder8,&cylinder9,&cylinder10,&cylinder11,&cylinder12}; }
AudioFrame process(); AudioFrame process();
void set_controls(float throttle_value, float load_value, float clutch_value); void set_controls(float throttle_value, float load_value, float clutch_value);
void toggle_mechanical_sound();
void set_audio_scene(OutdoorScene scene); void set_audio_scene(OutdoorScene scene);
void set_pipe_solver(PipeSolver solver);
void set_monitor_filter(bool enabled); void set_monitor_filter(bool enabled);
void set_starter(bool engaged);
AudioFrame dry_audio() const; AudioFrame dry_audio() const;
AudioFrame unfiltered_audio() const; AudioFrame unfiltered_audio() const;
void shift_up(); void shift_up();
@@ -80,34 +110,31 @@ public:
ScenarioSnapshot snapshot() const; ScenarioSnapshot snapshot() const;
float throttle{}; float throttle{};
bool mechanical_sound_enabled{};
float mechanical_mix{0.02f};
Ecu ecu{}; Ecu ecu{};
std::unique_ptr<Turbocharger> turbo;
Crankshaft crankshaft{}; Crankshaft crankshaft{};
FourStrokeCylinder cylinder1{}, cylinder2{}, cylinder3{}, cylinder4{}; FourStrokeCylinder cylinder1{}, cylinder2{}, cylinder3{}, cylinder4{};
FourStrokeCylinder cylinder5{}, cylinder6{}, cylinder7{}, cylinder8{}; FourStrokeCylinder cylinder5{}, cylinder6{}, cylinder7{}, cylinder8{};
Atmosphere intake_atmosphere{}; FourStrokeCylinder cylinder9{}, cylinder10{}, cylinder11{}, cylinder12{};
Volume0D intake_plenum{1.0e-3f, 101325.0f, 300.0f}; Volume0D intake_plenum{1.0e-3f, 101325.0f, 300.0f};
Volume0D exhaust_collector{0.15e-3f, 101325.0f, 700.0f}; Junction exhaust_collector{0.15e-3f, 101325.0f, 700.0f};
Volume0D second_bank_collector{1.5e-3f, 101325.0f, 700.0f};
std::vector<Pipe1D> pipes; std::vector<Pipe1D> pipes;
float intake_flow{}, exhaust_flow{}; float intake_flow{}, exhaust_flow{};
float last_mix_l{}, last_mix_r{};
std::uint64_t sample_count{}; std::uint64_t sample_count{};
private: private:
float intake_boundary_area_{}, throttle_area_{}, runner_area_{}, header_area_{}, tail_area_{}; float throttle_area_{}, runner_area_{}, header_area_{}, tail_area_{};
std::array<float, 8> intake_valve_areas_{}, exhaust_valve_areas_{}; std::array<float, 12> intake_valve_areas_{}, exhaust_valve_areas_{};
float dt_{}; float dt_{};
BoundarySystem boundaries_; BoundarySystem boundaries_;
Solver solver_; Solver solver_;
OutdoorAudio audio_; OutdoorAudio audio_;
MechanicalSoundProcessor mechanical_sound_;
mutable std::mutex mutex_; mutable std::mutex mutex_;
std::vector<std::unique_ptr<Junction>> exhaust_junctions_;
std::unique_ptr<Carburetor> carb_;
Port* throttle_port_{};
static float area_for_diameter(float diameter); static float area_for_diameter(float diameter);
static std::vector<Pipe1D> create_pipes(FourStrokePreset preset, bool turbo_enabled); static std::vector<Pipe1D> create_pipes(FourStrokePreset preset, const ExhaustConfig& exhaust, const V8ExhaustConfig& v8_exhaust, const EngineConfig& engine);
}; };
using FourStrokeTwinScenario = FourStrokeScenario; using FourStrokeTwinScenario = FourStrokeScenario;
@@ -116,17 +143,10 @@ public:
explicit AmericanV8Scenario(int sample_rate = 44100) explicit AmericanV8Scenario(int sample_rate = 44100)
: FourStrokeScenario(sample_rate, FourStrokePreset::AmericanV8) {} : FourStrokeScenario(sample_rate, FourStrokePreset::AmericanV8) {}
}; };
class TurboTwinScenario : public FourStrokeScenario {
public:
explicit TurboTwinScenario(int sample_rate = 44100) : FourStrokeScenario(sample_rate, FourStrokePreset::Twin, true) {}
};
class SaabB205Scenario : public FourStrokeScenario { class SaabB205Scenario : public FourStrokeScenario {
public: public:
explicit SaabB205Scenario(int sample_rate = 44100) : FourStrokeScenario(sample_rate, FourStrokePreset::SaabB205) {} explicit SaabB205Scenario(int sample_rate = 44100) : FourStrokeScenario(sample_rate, FourStrokePreset::SaabB205) {}
}; };
class SaabTurboScenario : public FourStrokeScenario {
public:
explicit SaabTurboScenario(int sample_rate = 44100) : FourStrokeScenario(sample_rate, FourStrokePreset::SaabB205, true) {}
};
} // namespace fluidsim::port } // namespace fluidsim::port

View File

@@ -0,0 +1,83 @@
#pragma once
#include "FluidSim/Scenario.hpp"
#include "FluidSim/TwoStrokeConfig.hpp"
#include <string>
namespace fluidsim::port {
struct ScenarioExample {
std::string id,title,description;
ScenarioKind kind;
TwoStrokeConfig two{};
ExhaustConfig exhaust{};
V8ExhaustConfig v8{};
EngineConfig engine{};
std::string category;
};
inline std::vector<ScenarioExample> scenario_examples() {
std::vector<ScenarioExample> examples;
auto add=[&](const char* id,const char* title,const char* description,ScenarioKind kind)->ScenarioExample& {
examples.push_back({id,title,description,kind,{},{},{},{},{}});return examples.back();
};
add("twin","600 cc parallel twin","EFI motorcycle baseline",ScenarioKind::Twin);
add("yz-racing","YZ125 / expansion chamber","Racing pipe, fixed 4.5% carb idle",ScenarioKind::TwoStroke);
add("yz-straight","YZ125 / straight exhaust","Same engine and carb, straight exhaust comparison",ScenarioKind::TwoStroke).two.exhaust=TwoStrokeExhaust::Straight;
add("saab-straight","Saab B205 / straight-through","6 L resonator and 18 L straight-through rear muffler",ScenarioKind::SaabB205);
auto& chambered=add("saab-chambered","Saab B205 / chambered","Expansion resonator and two-chamber rear muffler",ScenarioKind::SaabB205);
chambered.exhaust.resonator.type=SilencerType::ExpansionResonator;
chambered.exhaust.muffler.type=SilencerType::Chambered;chambered.exhaust.muffler.chambers=2;
auto& resonator=add("saab-expansion","Saab B205 / expansion silencers","Two single expansion chambers, same body volumes",ScenarioKind::SaabB205);
resonator.exhaust.resonator.type=SilencerType::ExpansionResonator;resonator.exhaust.muffler.type=SilencerType::ExpansionResonator;
add("saab-open","Saab B205 / open exhaust","Silencers bypassed; naturally aspirated baseline",ScenarioKind::SaabB205).exhaust.enabled=false;
add("v8-h","American 5.0 / 4-2-1 + H","750 mm primaries, 50 cc junctions, H crossover",ScenarioKind::AmericanV8);
add("v8-dual","American 5.0 / independent banks","Same 4-2-1 headers with H crossover disconnected",ScenarioKind::AmericanV8).v8.h_pipe=false;
add("v8-short","American 5.0 / short headers","600 mm primaries with H crossover",ScenarioKind::AmericanV8).v8.primary_length=.60f;
add("v8-long","American 5.0 / long headers","900 mm primaries with H crossover",ScenarioKind::AmericanV8).v8.primary_length=.90f;
add("v8-large","American 5.0 / larger primaries","42 mm x 750 mm primaries with H crossover",ScenarioKind::AmericanV8).v8.primary_diameter=.042f;
for(auto& e:examples) e.category=e.kind==ScenarioKind::TwoStroke?"Two-stroke / 125 cc":e.kind==ScenarioKind::SaabB205?"Inline four / Saab":e.kind==ScenarioKind::AmericanV8?"V8 / American 5.0":"Twins / parallel";
auto chamber=[&](const char* id,const char* title,float length,float width) {
auto& e=add(id,title,"Estimated chamber geometry / same 125 cc engine",ScenarioKind::TwoStroke);
e.category="Two-stroke / 125 cc";
for(auto& section:e.two.racing_pipe) {section.length*=length;section.inlet_diameter*=width;section.outlet_diameter*=width;}
};
chamber("yz-torque","125 / long torque chamber",1.18f,1.f);
chamber("yz-short","125 / short high-RPM chamber",.82f,1.f);
chamber("yz-fat","125 / wide belly chamber",1.f,1.18f);
auto two=[&](const char* id,const char* title,const char* category,float bore,float stroke,float carb) {
auto& e=add(id,title,"Concept single / transported carb fuel",ScenarioKind::TwoStroke);e.category=category;
const float scale=std::cbrt((bore*bore*stroke)/(.054f*.054f*.0545f));
e.two.jet_scale=bore<.05f?.5f:1.f;
e.two.bore=bore;e.two.stroke=stroke;e.two.rod*=scale;e.two.carburetor_diameter=carb;
e.two.transfer_width*=scale;e.two.exhaust_width*=scale;e.two.crankcase_clearance*=scale*scale*scale;
e.two.minimum_burn_seconds*=scale;
for(auto& section:e.two.racing_pipe) {section.length*=scale;section.inlet_diameter*=scale;section.outlet_diameter*=scale;}
e.two.exhaust_cell_length=.015f;
};
two("fifty","50 cc / little screamer","Two-stroke / 50 cc",.040f,.0398f,.019f);
two("fivehundred","500 cc / big single","Two-stroke / 500 cc",.086f,.086f,.041f);
auto custom=[&](const char* id,const char* title,const char* category,int count,EngineLayout layout,float bore,float stroke)->ScenarioExample& {
auto& e=add(id,title,"Estimated concept calibration / EFI",ScenarioKind::Twin);e.category=category;
e.engine.cylinders=count;e.engine.layout=layout;e.engine.bore=bore;e.engine.stroke=stroke;
e.engine.rod=std::max(.11f,stroke*1.8f);return e;
};
custom("single-450","450 cc / four-stroke thumper","Singles / four-stroke",1,EngineLayout::Inline,.096f,.062f);
custom("single-650","650 cc / big thumper","Singles / four-stroke",1,EngineLayout::Inline,.100f,.083f).engine.rev_limit=6500;
auto& twin270=custom("twin-270","700 cc / 270-degree parallel twin","Twins / parallel",2,EngineLayout::Inline,.080f,.068f);
twin270.engine.custom_phases=true;twin270.engine.phase_degrees[1]=450;
auto& ducati=custom("ltwin","916-inspired / 90-degree V twin","Twins / V",2,EngineLayout::V,.094f,.066f);
ducati.engine.bank_angle=90;ducati.engine.custom_phases=true;ducati.engine.phase_degrees[1]=450;
custom("triple","1.0 litre / inline triple","Inline three",3,EngineLayout::Inline,.079f,.068f);
auto& four=custom("f20","F20C-inspired / 2.0 high-rev four","Inline four / specials",4,EngineLayout::Inline,.087f,.084f);four.engine.rev_limit=9000;
custom("five","Audi-inspired / 2.2 inline five","Inline five",5,EngineLayout::Inline,.081f,.0864f);
custom("rb26","RB26-inspired / 2.6 inline six NA","Inline six",6,EngineLayout::Inline,.086f,.0737f);
custom("six-long","3.0 litre / long-stroke inline six","Inline six",6,EngineLayout::Inline,.086f,.086f);
custom("busso","Busso-inspired / 3.0 V6","V6",6,EngineLayout::V,.093f,.0726f);
auto& flat4=custom("ej20","EJ20-inspired / 2.0 flat four NA","Boxer / flat four",4,EngineLayout::Boxer,.092f,.075f);
flat4.engine.bank_angle=180;
custom("flat6","911-inspired / 3.0 flat six","Boxer / flat six",6,EngineLayout::Boxer,.095f,.0704f).engine.bank_angle=180;
auto& big=custom("big-carb","8.0 litre / carbureted big-block","V8 / big-block",8,EngineLayout::V,.1143f,.098f);
big.engine.carb_pilot_scale=.4f;big.engine.carb_jet_scale=.47f;big.engine.bank_angle=90;big.engine.compression=9;big.engine.carbureted=true;big.engine.idle_rpm=800;big.engine.idle_opening=.08f;
big.description="Carb fuel transport / fixed configurable idle stop";
custom("v10","5.0 litre / even-fire V10","V10",10,EngineLayout::V,.092f,.0752f).engine.bank_angle=72;
custom("v12","250-inspired / 3.0 V12","V12",12,EngineLayout::V,.073f,.0588f).engine.bank_angle=60;
return examples;
}
}

View File

@@ -0,0 +1,54 @@
#pragma once
#include "FluidSim/PipeProfile.hpp"
namespace fluidsim::port {
enum class SilencerType { StraightThrough, ExpansionResonator, Chambered };
struct SilencerConfig {
SilencerType type{SilencerType::Chambered};
float volume{.018f}, length{.65f}, core_diameter{.055f};
int chambers{2};
float passage_diameter{.058f};
float resistance{2.f}; // linear momentum decay, 1/s (packing surrogate)
float loss_coefficient{.35f}; // total dimensionless quadratic flow resistance
float diameter() const { return std::sqrt(4*volume/(pi*length)); }
};
struct ExhaustConfig {
bool enabled{true};
float inlet_length{1.4f}, outlet_length{.35f}, connecting_length{.8f}, diameter{.060f}, cell_length{.035f};
SilencerConfig resonator{SilencerType::StraightThrough,.006f,.40f,.060f,1,.060f,2, .1f};
SilencerConfig muffler{SilencerType::StraightThrough,.018f,.65f,.060f,1,.060f,4, .35f};
};
inline Pipe1D make_silenced_exhaust(const ExhaustConfig& c) {
std::vector<PipeSection> sections;
std::vector<float> linear, quadratic;
auto add=[&](float length,float a,float b,float r=0,float k=0) {
if(!std::isfinite(length)||length<=0||!std::isfinite(r)||r<0||!std::isfinite(k)||k<0)
throw std::invalid_argument("invalid silencer resistance or length");
sections.push_back({length,a,b}); linear.push_back(r); quadratic.push_back(k/length);
};
add(c.inlet_length,c.diameter,c.diameter);
if(c.enabled) for(int index=0;index<2;++index) {
if(index) add(c.connecting_length,c.diameter,c.diameter);
const auto& s=index?c.muffler:c.resonator;
if(!std::isfinite(s.volume)||s.volume<=0||!std::isfinite(s.length)||s.length<=0||s.chambers<1||s.chambers>16 || s.core_diameter<=0 || s.passage_diameter<=0)
throw std::invalid_argument("invalid silencer geometry");
const float d=s.type==SilencerType::StraightThrough?s.core_diameter:s.diameter();
const int n=s.type==SilencerType::Chambered?s.chambers:1;
float previous=c.diameter;
for(int i=0;i<n;++i) {
add(s.length*.1f/n,previous,d,s.resistance,s.loss_coefficient*.5f/n);
add(s.length*.8f/n,d,d,s.resistance,0);
const float end=i+1==n?c.diameter:s.passage_diameter;
add(s.length*.1f/n,d,end,s.resistance,s.loss_coefficient*.5f/n);
previous=end;
}
}
add(c.outlet_length,c.diameter,c.diameter);
auto pipe=make_profiled_pipe(sections,c.cell_length);
for(std::size_t i=0;i<sections.size();++i) {
const int n=int(std::ceil(sections[i].length/c.cell_length));
for(int j=0;j<n;++j) { pipe.linear_resistance.push_back(linear[i]);pipe.quadratic_resistance.push_back(quadratic[i]); }
}
return pipe;
}
}

View File

@@ -1,24 +0,0 @@
#pragma once
#include <atomic>
namespace fluidsim::port {
class ThreadLoadTracker {
public:
void record(double busy_milliseconds, double budget_milliseconds) {
if (budget_milliseconds <= 0.0) return;
constexpr double alpha = 0.05;
const double instant = busy_milliseconds / budget_milliseconds * 100.0;
const double previous = load_percent_.load(std::memory_order_relaxed);
load_percent_.store(previous + alpha * (instant - previous), std::memory_order_relaxed);
last_busy_ms_.store(busy_milliseconds, std::memory_order_relaxed);
}
double load_percent() const { return load_percent_.load(std::memory_order_relaxed); }
double last_step_microseconds() const { return last_busy_ms_.load(std::memory_order_relaxed) * 1000.0; }
private:
std::atomic<double> load_percent_{};
std::atomic<double> last_busy_ms_{};
};
} // namespace fluidsim::port

View File

@@ -1,81 +0,0 @@
#pragma once
#include "FluidSim/Fluid.hpp"
#include <array>
#include <cstdint>
namespace fluidsim::port {
// RHB31/VZ21-size mean-line approximation. Geometry, losses and coefficients
// are estimates, not a digitized or validated manufacturer performance map.
struct TurboConfig {
static TurboConfig gt17(); // estimated Saab GT1752-size calibration
double shaft_inertia{4e-6}; // kg m^2, compressor+turbine+shaft rigid rotor
double compressor_radius{.018}, turbine_radius{.0135};
double compressor_head_coefficient{.60}, compressor_efficiency{.65};
double turbine_peak_efficiency{.65}, turbine_optimum_speed_ratio{.70};
double compressor_effective_area{110e-6}, turbine_effective_area{100e-6};
double bearing_viscous{1e-7}, windage{2e-11}; // Nm/(rad/s), Nm/(rad/s)^2
// Estimated pressure-can/spring wastegate. Set maximum lift to zero for a
// pure ungated turbo bench test. This changes bypass AREA, never boost.
double gate_diaphragm_area{8e-4}, gate_spring_preload{40}, gate_spring_stiffness{6000};
double gate_damping{120}, gate_diameter{.014}, gate_maximum_lift{.004};
double reference_pressure{101325};
float compressor_inlet_volume{20e-6f}, compressor_outlet_volume{30e-6f};
float turbine_inlet_volume{30e-6f}, turbine_outlet_volume{20e-6f};
std::array<float, 4> connection_diameters{.024f, .024f, .025f, .026f};
};
struct TurboSnapshot {
double rpm{}, angle{}, shaft_energy{}, turbine_power{}, compressor_power{}, bearing_power{};
double compressor_flow{}, turbine_flow{}, compressor_pressure_ratio{}, turbine_pressure_ratio{};
double turbine_inlet_pressure{}, turbine_outlet_pressure{}, charge_pressure{}, charge_temperature{};
double bearing_heat{}, energy_residual{};
std::uint64_t limited_transfers{};
double wastegate_flow{}, wastegate_lift{};
};
// Four pressure-storage nodes joined by turbine/nozzle and centrifugal-pump
// characteristics. Existing pipe orifices exchange conserved mass/enthalpy.
// The whole turbo is ONE solver component; do not also register its volumes.
class Turbocharger final : public Component {
public:
explicit Turbocharger(TurboConfig config = {});
const TurboConfig config;
Volume0D compressor_inlet, compressor_outlet, turbine_inlet, turbine_outlet;
Port& compressor_inlet_port;
Port& compressor_outlet_port;
Port& turbine_inlet_port;
Port& turbine_outlet_port;
void connect(BoundarySystem& boundaries, const std::vector<Pipe1D>& pipes,
int compressor_in, bool compressor_in_left, int compressor_out, bool compressor_out_left,
int turbine_in, bool turbine_in_left, int turbine_out, bool turbine_out_left);
std::vector<Port*> ports() override;
void update_state(float dt) override;
TurboSnapshot snapshot() const;
double omega() const { return omega_; }
double shaft_energy() const { return .5*config.shaft_inertia*omega_*omega_; }
double fluid_energy() const;
// Intended for standalone initialization/tests, never an imposed running speed.
void set_initial_rpm(double rpm);
void set_wastegate_duty(double duty) { gate_duty_ = std::isfinite(duty) ? std::clamp(duty, 0., .85) : 0.; }
private:
std::array<Port*, 4> internal_;
std::array<float, 4> connection_areas_{};
double omega_{}, angle_{}, bearing_heat_{};
double turbine_power_{}, compressor_power_{}, bearing_power_{}, energy_residual_{};
double compressor_flow_{}, turbine_flow_{};
double wastegate_flow_{}, wastegate_lift_{};
std::uint64_t limited_transfers_{};
bool connected_{};
double gate_duty_{}; // pneumatic bleed fraction; zero restores spring boost
static double nozzle(double upstream_pressure, double upstream_temperature,
double downstream_pressure, double area);
double transfer_flow(Volume0D& inlet, Volume0D& outlet, Port& inlet_external,
Port& outlet_external, double requested, double dt);
static void set_transfer(Volume0D& inlet, Volume0D& outlet, Port& inlet_internal,
Port& outlet_internal, double flow, double shaft_work, double dt);
};
} // namespace fluidsim::port

View File

@@ -13,7 +13,10 @@ struct TwoStrokeConfig {
float trapped_compression{8.2f}; // exhaust-closure volume / clearance volume float trapped_compression{8.2f}; // exhaust-closure volume / clearance volume
float transfer_duration{130}, exhaust_duration{190}; float transfer_duration{130}, exhaust_duration{190};
float transfer_width{.050f}, exhaust_width{.036f}; // summed effective window widths float transfer_width{.050f}, exhaust_width{.036f}; // summed effective window widths
float jet_scale{1.f}; // multiplier on effective jet diameters
float crankcase_clearance{150e-6f};
float carburetor_diameter{.0381f}; float carburetor_diameter{.0381f};
float idle_slide_opening{.045f}; // fixed mechanical stop, normalized 0..1 (4.5%)
float minimum_burn_seconds{.0008f}; float minimum_burn_seconds{.0008f};
// Empirical mechanical FMEP = A + B*mean_piston_speed + C*speed^2. // Empirical mechanical FMEP = A + B*mean_piston_speed + C*speed^2.
// Generic correlation estimates, not a measured YZ125 coast-down fit. // Generic correlation estimates, not a measured YZ125 coast-down fit.

View File

@@ -5,7 +5,6 @@
#include "FluidSim/Carburetor.hpp" #include "FluidSim/Carburetor.hpp"
#include "FluidSim/Fluid.hpp" #include "FluidSim/Fluid.hpp"
#include "FluidSim/OutdoorAudio.hpp" #include "FluidSim/OutdoorAudio.hpp"
#include "FluidSim/Atmosphere.hpp"
#include "FluidSim/Ecu.hpp" #include "FluidSim/Ecu.hpp"
#include "FluidSim/ReedValve.hpp" #include "FluidSim/ReedValve.hpp"
#include "FluidSim/MotorcycleTest.hpp" #include "FluidSim/MotorcycleTest.hpp"
@@ -36,6 +35,7 @@ public:
void shift_up() { vehicle.shift_up(); } void shift_up() { vehicle.shift_up(); }
void shift_down() { vehicle.shift_down(); } void shift_down() { vehicle.shift_down(); }
void set_neutral() { vehicle.set_neutral(); } void set_neutral() { vehicle.set_neutral(); }
void set_pipe_solver(PipeSolver solver) { for(auto& pipe : pipes) pipe.solver=solver; }
void set_audio_scene(OutdoorScene scene) { audio_.set_scene(scene); } void set_audio_scene(OutdoorScene scene) { audio_.set_scene(scene); }
void set_monitor_filter(bool enabled) { audio_.set_monitor_filter(enabled); } void set_monitor_filter(bool enabled) { audio_.set_monitor_filter(enabled); }
AudioFrame dry_audio() const { return audio_.dry_frame(); } AudioFrame dry_audio() const { return audio_.dry_frame(); }
@@ -47,7 +47,6 @@ public:
Ecu ecu{}; Ecu ecu{};
Carburetor carburetor; Carburetor carburetor;
ReedValve reed{}; ReedValve reed{};
Atmosphere intake_atmosphere{};
float intake_flow{}, exhaust_flow{}; float intake_flow{}, exhaust_flow{};
Crankshaft crankshaft{}; Crankshaft crankshaft{};
TwoStrokeCylinder cylinder; TwoStrokeCylinder cylinder;
@@ -56,7 +55,7 @@ public:
std::vector<Pipe1D> pipes; std::vector<Pipe1D> pipes;
private: private:
float dt_{}, max_throttle_area_{}, intake_boundary_area_{}; float dt_{}, max_throttle_area_{};
float reed_in_area_{}, reed_out_area_{}, transfer_pipe_area_{}, transfer_valve_area_{}, exhaust_valve_area_{}; float reed_in_area_{}, reed_out_area_{}, transfer_pipe_area_{}, transfer_valve_area_{}, exhaust_valve_area_{};
bool reed_open_{}; bool reed_open_{};
std::uint64_t step_count_{}; std::uint64_t step_count_{};

View File

@@ -2,7 +2,6 @@
#include "FluidSim/Engine.hpp" #include "FluidSim/Engine.hpp"
#include <algorithm> #include <algorithm>
#include <cmath>
namespace fluidsim::port { namespace fluidsim::port {
@@ -20,67 +19,4 @@ float SoundProcessor::process(float mass_flow_out) {
return derivative * scale_factor_ * gain; return derivative * scale_factor_ * gain;
} }
RadiationSource::RadiationSource(int sample_rate)
: sample_rate_(std::max(sample_rate, 1)), dt_(1.0f / static_cast<float>(std::max(sample_rate, 1))) {
const std::size_t max_delay = static_cast<std::size_t>(std::max(1.0f,
100.0f / std::max(speed_of_sound, 1.0f) * static_cast<float>(sample_rate_)));
left_delay_.resize(max_delay);
right_delay_.resize(max_delay);
}
AudioFrame RadiationSource::process(float mass_flow) {
const float flow_alpha = flow_tau > 0.0f ? 1.0f - std::exp(-dt_ / flow_tau) : 1.0f;
const float derivative_decay = smooth_tau > 0.0f ? std::exp(-dt_ / smooth_tau) : 0.0f;
flow_lp_ += flow_alpha * (mass_flow - flow_lp_);
const float raw_derivative = (flow_lp_ - previous_flow_lp_) / dt_;
previous_flow_lp_ = flow_lp_;
smooth_derivative_ = derivative_decay * smooth_derivative_ + (1.0f - derivative_decay) * raw_derivative;
const Vec3 to_listener = subtract(listener, pipe_end);
const float distance = std::max(length(to_listener), 0.1f);
const Vec3 direction{to_listener.x / distance, to_listener.y / distance, to_listener.z / distance};
const float cosine = dot(pipe_axis, direction);
const float directivity = (1.0f - directivity_balance) + directivity_balance * cosine;
last_mono_pressure_ = smooth_derivative_ * gain * directivity / (4.0f * 3.14159265358979323846f * distance);
const float air_cutoff = std::max(min_air_cutoff,
20000.0f * std::exp(-distance * air_absorption_exponent));
const float filtered = air_absorption_exponent > 0.0f
? one_pole(last_mono_pressure_, air_cutoff, air_filter_) : last_mono_pressure_;
const Vec3 ear_offset{listener_right.x * ear_spacing * 0.5f,
listener_right.y * ear_spacing * 0.5f,
listener_right.z * ear_spacing * 0.5f};
const Vec3 left_ear = subtract(listener, ear_offset);
const Vec3 right_ear{listener.x + ear_offset.x, listener.y + ear_offset.y, listener.z + ear_offset.z};
const float left_distance = std::max(length(subtract(left_ear, pipe_end)), 0.1f);
const float right_distance = std::max(length(subtract(right_ear, pipe_end)), 0.1f);
left_offset_ = std::min(left_delay_.size() - 1, static_cast<std::size_t>(left_distance / speed_of_sound / dt_));
right_offset_ = std::min(right_delay_.size() - 1, static_cast<std::size_t>(right_distance / speed_of_sound / dt_));
const auto delay_sample = [this, filtered](std::vector<float>& buffer, std::size_t offset, float ear_distance) {
const std::size_t read = (write_position_ + buffer.size() - offset) % buffer.size();
buffer[write_position_] = filtered * (length(subtract(listener, pipe_end)) / ear_distance);
const float value = buffer[read];
return value;
};
float left = delay_sample(left_delay_, left_offset_, left_distance);
float right = delay_sample(right_delay_, right_offset_, right_distance);
write_position_ = (write_position_ + 1) % left_delay_.size();
if (head_shadow_cutoff > 0.0f && left_distance > right_distance) left = one_pole(left, head_shadow_cutoff, shadow_left_);
else if (head_shadow_cutoff > 0.0f && right_distance > left_distance) right = one_pole(right, head_shadow_cutoff, shadow_right_);
return {left, right};
}
float RadiationSource::dot(Vec3 a, Vec3 b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
float RadiationSource::length(Vec3 v) { return std::sqrt(v.x * v.x + v.y * v.y + v.z * v.z); }
Vec3 RadiationSource::subtract(Vec3 a, Vec3 b) { return {a.x - b.x, a.y - b.y, a.z - b.z}; }
float RadiationSource::one_pole(float input, float cutoff, float& state) const {
const float angular = 2.0f * 3.14159265358979323846f * cutoff / static_cast<float>(sample_rate_);
const float alpha = 1.0f - std::exp(-angular);
state += alpha * (input - state);
return state;
}
} // namespace fluidsim::port } // namespace fluidsim::port

View File

@@ -1,6 +1,7 @@
#include "FluidSim/DesktopScenario.hpp" #include "FluidSim/DesktopScenario.hpp"
#include "FluidSim/SoundEngine.hpp" #include "FluidSim/SoundEngine.hpp"
#include "FluidSim/DynoCurve.hpp" #include "FluidSim/Desktop/DesktopUi.hpp"
#include "FluidSim/Desktop/ScenarioMenu.hpp"
#include <SFML/Graphics.hpp> #include <SFML/Graphics.hpp>
#include <algorithm> #include <algorithm>
@@ -18,510 +19,41 @@
namespace { namespace {
using namespace fluidsim::port; using namespace fluidsim::port;
constexpr unsigned int window_width = 1280; constexpr unsigned int window_width = 1440;
constexpr unsigned int window_height = 720; constexpr unsigned int window_height = 900;
constexpr std::size_t ring_capacity = 8192; constexpr std::size_t ring_capacity = 8192;
constexpr std::size_t target_fill = 4096; constexpr std::size_t target_fill = 4096;
constexpr std::size_t preroll_samples = 4096; constexpr std::size_t preroll_samples = 4096;
constexpr float throttle_lerp_rate = 10.0f; constexpr float throttle_lerp_rate = 10.0f;
sf::Color pressure_color(float pressure) {
const float bar = pressure / 1.0e5f;
if (bar < 1.0f) {
const float f = std::clamp(bar, 0.0f, 1.0f);
return {0, static_cast<std::uint8_t>(255.0f * f), static_cast<std::uint8_t>(255.0f * (1.0f - f))};
}
const float f = std::min((bar - 1.0f) / 9.0f, 1.0f);
return {static_cast<std::uint8_t>(255.0f * f), static_cast<std::uint8_t>(255.0f * (1.0f - f)), 0};
}
sf::Color temperature_color(float temperature) {
const float t = std::clamp(temperature, 0.0f, 2000.0f);
if (t < 300.0f) {
const float f = t / 300.0f;
return {0, static_cast<std::uint8_t>(255.0f * f), static_cast<std::uint8_t>(255.0f * (1.0f - f))};
}
const float f = (t - 300.0f) / 1700.0f;
return {static_cast<std::uint8_t>(255.0f * f), static_cast<std::uint8_t>(255.0f * (1.0f - f)), 0};
}
void draw_label(sf::RenderWindow& window, const sf::Font* font, const std::string& text,
sf::Vector2f position, sf::Color color = sf::Color::White, unsigned int size = 14) {
if (!font) return;
sf::Text label(*font);
label.setString(text);
label.setPosition(position);
label.setFillColor(color);
label.setCharacterSize(size);
window.draw(label);
}
std::optional<ScenarioKind> choose_scenario(sf::RenderWindow& window, const sf::Font* font) {
const std::array<sf::FloatRect, 5> cards{{{{32, 110}, {656, 72}}, {{32, 190}, {656, 72}}, {{32, 270}, {656, 72}}, {{32, 350}, {656, 72}}, {{32, 430}, {656, 72}}}};
const sf::FloatRect start_button{{32, 522}, {656, 52}};
const std::array<const char*, 5> titles{"600 cc parallel twin", "YZ125 two-stroke", "Saab B205 / 9-5 wagon", "600 cc twin + VZ21-size turbo", "1980s American 5.0 V8"};
const std::array<const char*, 5> descriptions{
"Four-stroke twin, motorcycle test and dyno",
"125 cc single, expansion chamber, motorcycle test and dyno",
"2.0 litre inline-four, GT17 turbo, ECU boost control and five-speed wagon",
"Coupled turbine/compressor, shaft inertia and pressure diagnostics",
"EFI cross-plane V8, heavy flywheel, independent exhaust banks"};
int selected = 0;
while (window.isOpen()) {
while (const auto event = window.pollEvent()) {
if (event->is<sf::Event::Closed>()) return std::nullopt;
if (const auto* key = event->getIf<sf::Event::KeyPressed>()) {
switch (key->code) {
case sf::Keyboard::Key::Escape: return std::nullopt;
case sf::Keyboard::Key::Up: selected = (selected + 4) % 5; break;
case sf::Keyboard::Key::Down:
case sf::Keyboard::Key::Tab: selected = (selected + 1) % 5; break;
case sf::Keyboard::Key::Num1: selected = 0; break;
case sf::Keyboard::Key::Num2: selected = 1; break;
case sf::Keyboard::Key::Num3: selected = 2; break;
case sf::Keyboard::Key::Num4: selected = 3; break;
case sf::Keyboard::Key::Num5: selected = 4; break;
case sf::Keyboard::Key::Enter: return static_cast<ScenarioKind>(selected);
default: break;
}
}
if (const auto* move = event->getIf<sf::Event::MouseMoved>()) {
const auto point = window.mapPixelToCoords(move->position);
for (int i = 0; i < 5; ++i)
if (cards[i].contains(point)) selected = i;
}
if (const auto* click = event->getIf<sf::Event::MouseButtonReleased>();
click && click->button == sf::Mouse::Button::Left) {
const auto point = window.mapPixelToCoords(click->position);
for (int i = 0; i < 5; ++i)
if (cards[i].contains(point)) return static_cast<ScenarioKind>(i);
if (start_button.contains(point)) return static_cast<ScenarioKind>(selected);
}
}
window.clear({9, 12, 17});
draw_label(window, font, "FluidSim", {32, 24}, sf::Color::White, 28);
draw_label(window, font, "Choose a scenario to start", {32, 66}, {180, 190, 205}, 16);
for (int i = 0; i < 5; ++i) {
sf::RectangleShape card(cards[i].size);
card.setPosition(cards[i].position);
card.setFillColor(i == selected ? sf::Color(24, 46, 60) : sf::Color(18, 24, 32));
card.setOutlineColor(i == selected ? sf::Color(90, 200, 210) : sf::Color(55, 65, 78));
card.setOutlineThickness(2);
window.draw(card);
draw_label(window, font, titles[i], cards[i].position + sf::Vector2f{20, 16},
sf::Color::White, 20);
draw_label(window, font, descriptions[i], cards[i].position + sf::Vector2f{20, 54},
{180, 190, 205}, 13);
}
sf::RectangleShape start(start_button.size);
start.setPosition(start_button.position);
start.setFillColor({35, 105, 115});
window.draw(start);
draw_label(window, font, "Start selected scenario [Enter]", {52, 537}, sf::Color::White, 17);
draw_label(window, font, "Click a scenario or use Up/Down. Esc: quit", {32, 574}, {150, 165, 180}, 12);
window.display();
}
return std::nullopt;
}
void draw_volume(sf::RenderWindow& window, float pressure, sf::FloatRect bounds) {
sf::RectangleShape body({bounds.size.x, bounds.size.y});
body.setPosition(bounds.position);
body.setFillColor(pressure_color(pressure));
body.setOutlineColor(sf::Color::White);
body.setOutlineThickness(1.0f);
window.draw(body);
}
void draw_pipe(sf::RenderWindow& window, const ScenarioSnapshot& snapshot, std::size_t pipe_index,
float center_y, float x0, float x1) {
const auto& cells = snapshot.pipe_states[pipe_index];
if (cells.empty()) return;
const auto& areas = snapshot.pipe_areas[pipe_index];
const auto& lengths = snapshot.pipe_lengths[pipe_index];
float total_length = 0;
for (float length : lengths) total_length += length;
float x = x0;
for (std::size_t i = 0; i < cells.size(); ++i) {
const auto& cell = cells[i];
const float cell_width = total_length > 0 ? (x1 - x0) * lengths[i] / total_length
: (x1 - x0) / cells.size();
const float thickness = areas.empty() ? 8 : 600 * std::sqrt(4 * areas[i] / pi);
sf::RectangleShape segment({cell_width + 1.0f, thickness});
segment.setPosition({x, center_y - thickness * .5f});
segment.setFillColor(temperature_color(cell.pressure / std::max(cell.rho * 287.0f, 1e-9f)));
window.draw(segment);
x += cell_width;
}
}
void draw_cylinder(sf::RenderWindow& window, const sf::Font* font, float pressure,
float piston_fraction, float top, const char* name, bool intake_open, bool exhaust_open, float x = 470.0f) {
constexpr float width = 74.0f, height = 142.0f;
const float fraction = std::clamp(piston_fraction, 0.0f, 1.0f);
const float gas_height = height * fraction;
sf::RectangleShape wall({width, height});
wall.setPosition({x, top});
wall.setFillColor({52, 59, 68});
window.draw(wall);
sf::RectangleShape gas({width, gas_height});
gas.setPosition({x, top});
gas.setFillColor(pressure_color(pressure));
window.draw(gas);
sf::RectangleShape piston({width + 8.0f, 5.0f});
piston.setPosition({x - 4.0f, top + gas_height});
piston.setFillColor(sf::Color::White);
window.draw(piston);
sf::RectangleShape intake_valve({7.0f, 12.0f});
intake_valve.setPosition({x - 12.0f, top + 5.0f});
intake_valve.setFillColor(intake_open ? sf::Color::Green : sf::Color::Red);
window.draw(intake_valve);
sf::RectangleShape exhaust_valve({7.0f, 12.0f});
exhaust_valve.setPosition({x + width + 5.0f, top + 5.0f});
exhaust_valve.setFillColor(exhaust_open ? sf::Color::Green : sf::Color::Red);
window.draw(exhaust_valve);
sf::RectangleShape border({width, height});
border.setPosition({x, top});
border.setFillColor(sf::Color::Transparent);
border.setOutlineColor(sf::Color(190, 200, 210));
border.setOutlineThickness(1.0f);
window.draw(border);
draw_label(window, font, name, {x + 13.0f, top - 22.0f}, sf::Color::White, 14);
}
void draw_graph_line(sf::RenderWindow& window, sf::Vector2f start, sf::Vector2f end,
sf::Color color, float thickness = 1.5f) {
const float dx = end.x - start.x;
const float dy = end.y - start.y;
const float length = std::sqrt(dx * dx + dy * dy);
if (length <= 0.0f) return;
sf::RectangleShape line({length, thickness});
line.setPosition(start);
line.setRotation(sf::degrees(std::atan2(dy, dx) * 180.0f / pi));
line.setFillColor(color);
window.draw(line);
}
void draw_dyno_curve(sf::RenderWindow& window, const sf::Font* font, const DynoCurve& curve,
float graph_x, float graph_y, float graph_width, float graph_height,
float current_rpm, float current_power_kw) {
const auto& bins = curve.bins();
if (bins.empty()) return;
float max_power = 0.01f, max_torque = 0.01f, max_rpm = 1000.0f;
for (std::size_t index = 0; index < bins.size(); ++index) {
if (bins[index].power_kw > 0.0f || bins[index].torque_nm > 0.0f) {
max_power = std::max(max_power, bins[index].power_kw);
max_torque = std::max(max_torque, bins[index].torque_nm);
max_rpm = std::max(max_rpm, static_cast<float>(index) * DynoCurve::rpm_bin_size +
DynoCurve::rpm_bin_size * 0.5f);
}
}
max_power *= 1.1f;
max_torque *= 1.1f;
max_rpm = std::max(max_rpm * 1.05f, 1000.0f);
sf::RectangleShape background({graph_width, graph_height});
background.setPosition({graph_x, graph_y});
background.setFillColor({20, 20, 20, 220});
background.setOutlineColor({70, 78, 90});
background.setOutlineThickness(1.0f);
window.draw(background);
constexpr float left_margin = 50.0f, right_margin = 48.0f;
constexpr float top_margin = 24.0f, bottom_margin = 38.0f;
const float plot_x = graph_x + left_margin;
const float plot_y = graph_y + top_margin;
const float plot_width = graph_width - left_margin - right_margin;
const float plot_height = graph_height - top_margin - bottom_margin;
const sf::Color grid_color(50, 50, 50);
for (int index = 0; index <= 9; ++index) {
const float x = plot_x + static_cast<float>(index) / 9.0f * plot_width;
sf::RectangleShape grid({1.0f, plot_height});
grid.setPosition({x, plot_y});
grid.setFillColor(grid_color);
window.draw(grid);
}
for (int index = 0; index <= 5; ++index) {
const float y = plot_y + (1.0f - static_cast<float>(index) / 5.0f) * plot_height;
sf::RectangleShape grid({plot_width, 1.0f});
grid.setPosition({plot_x, y});
grid.setFillColor(grid_color);
window.draw(grid);
draw_label(window, font, std::to_string(static_cast<int>(max_power * index / 5.0f)),
{graph_x + 3.0f, y - 7.0f}, sf::Color(240, 100, 90), 9);
draw_label(window, font, std::to_string(static_cast<int>(max_torque * index / 5.0f)),
{graph_x + graph_width - 42.0f, y - 7.0f}, sf::Color(100, 150, 255), 9);
}
for (int index = 0; index <= 9; ++index) {
const float rpm = max_rpm * static_cast<float>(index) / 9.0f;
const float x = plot_x + static_cast<float>(index) / 9.0f * plot_width;
std::ostringstream tick;
tick << std::fixed << std::setprecision(1) << rpm / 1000.0f << 'k';
draw_label(window, font, tick.str(), {x - 12.0f, graph_y + graph_height - 31.0f},
sf::Color(200, 205, 215), 9);
}
draw_label(window, font, "DYNO", {graph_x + 8.0f, graph_y + 4.0f}, sf::Color::White, 11);
draw_label(window, font, "kW", {graph_x + 8.0f, graph_y + 17.0f}, sf::Color(255, 100, 90), 9);
draw_label(window, font, "Nm", {graph_x + graph_width - 30.0f, graph_y + 17.0f},
sf::Color(110, 160, 255), 9);
draw_label(window, font, "RPM", {graph_x + graph_width * 0.5f - 12.0f,
graph_y + graph_height - 15.0f}, sf::Color::White, 10);
const sf::Color power_color(255, 27, 27), torque_color(9, 90, 255);
for (std::size_t index = 1; index < bins.size(); ++index) {
const float rpm0 = (static_cast<float>(index - 1) + 0.5f) * DynoCurve::rpm_bin_size;
const float rpm1 = (static_cast<float>(index) + 0.5f) * DynoCurve::rpm_bin_size;
if (rpm0 > max_rpm || rpm1 > max_rpm) break;
const float x0 = plot_x + rpm0 / max_rpm * plot_width;
const float x1 = plot_x + rpm1 / max_rpm * plot_width;
if (bins[index - 1].power_kw > 0.0f && bins[index].power_kw > 0.0f) {
const float y0 = plot_y + (1.0f - bins[index - 1].power_kw / max_power) * plot_height;
const float y1 = plot_y + (1.0f - bins[index].power_kw / max_power) * plot_height;
draw_graph_line(window, {x0, y0}, {x1, y1}, power_color, 2.0f);
}
if (bins[index - 1].torque_nm > 0.0f && bins[index].torque_nm > 0.0f) {
const float y0 = plot_y + (1.0f - bins[index - 1].torque_nm / max_torque) * plot_height;
const float y1 = plot_y + (1.0f - bins[index].torque_nm / max_torque) * plot_height;
draw_graph_line(window, {x0, y0}, {x1, y1}, torque_color, 2.0f);
}
}
if (current_rpm > 0.0f && current_rpm <= max_rpm && current_power_kw > 0.0f) {
const float x = plot_x + current_rpm / max_rpm * plot_width;
const float y = plot_y + (1.0f - current_power_kw / max_power) * plot_height;
sf::CircleShape dot(3.0f);
dot.setPosition({x - 3.0f, y - 3.0f});
dot.setFillColor(sf::Color::White);
window.draw(dot);
}
}
void draw_dashboard(sf::RenderWindow& window, const sf::Font* font, const ScenarioSnapshot& state,
float throttle_target, float brake, double playback_speed, std::size_t buffer_fill,
std::size_t buffer_capacity, bool time_warp, std::uint64_t underrun_blocks, bool auto_gain) {
std::ostringstream status;
status << (time_warp ? "[Space] normal speed" : "[Space] real time")
<< " Sim " << std::fixed << std::setprecision(3) << playback_speed << "x"
<< " Audio buffer " << buffer_fill << "/" << buffer_capacity
<< " Underruns " << underrun_blocks
<< (state.cylinder_count == 8 ? "\nCoupe " : state.cylinder_count == 4 ? "\nWagon " : "\nBike ") << std::setprecision(1) << state.speed_kmh << " km/h Gear "
<< (state.gear == 0 ? "N" : std::to_string(state.gear))
<< " Throttle " << state.throttle * 100.0f << "% / target " << throttle_target * 100.0f << "% [W, up/down]"
<< " Clutch " << state.clutch * 100.0f << "% [C] Engine load " << brake * 100.0f << "% [left/right]"
<< " Shift [A/D], Neutral [Z]"
<< "\nScene [R]: " << scene_name(state.audio_scene) << " " << (state.fuel_cut ? "FUEL CUT" : (state.idle_control ? "IDLE CONTROL" : "RUNNING"));
if (!state.two_stroke)
status << " Mechanical [M]: " << (state.mechanical_sound_enabled ? "ON" : "OFF");
status << " Auto gain [G]: " << (auto_gain ? "ON" : "OFF");
if (font) {
sf::RectangleShape panel({1260.0f, 92.0f});
panel.setPosition({10.0f, 10.0f});
panel.setFillColor({12, 18, 26, 236});
panel.setOutlineColor({80, 110, 130});
panel.setOutlineThickness(1.0f);
window.draw(panel);
draw_label(window, font, status.str(), {22.0f, 20.0f}, sf::Color::White, 14);
draw_label(window, font,
"RPM " + std::to_string(static_cast<int>(state.rpm)) +
" Power " + std::to_string(state.average_power / 1000.0f) + " kW" +
" Vehicle kinetic energy " + std::to_string(static_cast<int>(state.kinetic_energy)) + " J",
{24.0f, 122.0f}, {220, 230, 240}, 17);
draw_label(window, font, state.two_stroke ? "REV LIMIT DISABLED" :
((state.rev_limit1 || state.rev_limit2) ? std::string("REV CUT") : "REV LIMIT " + std::to_string(int(state.rev_limit_rpm)) + " rpm"),
{24.0f, 151.0f}, {120, 220, 120}, 14);
}
if (state.turbo_enabled) {
const auto& t = state.turbo;
std::ostringstream text;
text << std::fixed << std::setprecision(0) << (state.cylinder_count==4 ? "GT17-size: " : "VZ21-size: ") << t.rpm << " rpm "
<< std::setprecision(2) << "CPR " << t.compressor_pressure_ratio
<< " Charge " << t.charge_pressure/1000 << " kPa / " << std::setprecision(0) << t.charge_temperature << " K"
<< "\nTurbine inlet " << t.turbine_inlet_pressure/1000 << " kPa Outlet " << t.turbine_outlet_pressure/1000 << " kPa"
<< " Power " << t.turbine_power << " / " << t.compressor_power << " W";
draw_label(window, font, text.str(), {625, 125}, {130, 215, 220}, 12);
}
}
void draw_two_stroke_scene(sf::RenderWindow& window,const sf::Font* font,const ScenarioSnapshot& s) {
window.clear({9,12,17});
draw_label(window,font,"YZ125 baseline | piston-controlled ports",
{650,185},sf::Color::White,14);
draw_cylinder(window,font,s.cylinder_pressure1,s.cylinder_piston_fraction1,220,"CYLINDER",
s.valve1_intake_area>0,s.valve1_exhaust_area>0);
draw_volume(window,s.crankcase_pressure,{{455,410},{110,95}});
draw_label(window,font,"CRANKCASE",{455,385},sf::Color::White,14);
draw_pipe(window,s,0,460,30,170);
draw_volume(window,s.intake_pressure,{{180,425},{95,70}});
draw_label(window,font,"CARB / INTAKE",{180,400},sf::Color::White,14);
draw_pipe(window,s,1,460,285,445);
draw_label(window,font,s.reed_open?"REED OPEN":"REED CLOSED",{290,485},s.reed_open?sf::Color::Green:sf::Color::Red,12);
draw_pipe(window,s,2,350,315,450);
draw_graph_line(window,{510,505},{510,530},{180,190,200});
draw_graph_line(window,{510,530},{315,530},{180,190,200});
draw_graph_line(window,{315,530},{315,350},{180,190,200});
draw_graph_line(window,{450,350},{450,245},{180,190,200});
draw_graph_line(window,{450,245},{467,245},{180,190,200});
draw_label(window,font,"TRANSFER",{320,325},sf::Color::White,12);
draw_graph_line(window,{551,235},{590,235},{180,190,200});
draw_pipe(window,s,3,235,590,1230);
draw_label(window,font,s.racing_exhaust ? "HEADER / DIFFUSERS / BELLY / BAFFLE / STINGER" : "STRAIGHT EXHAUST",{670,205},sf::Color::White,13);
std::ostringstream pressures;
pressures<<std::fixed<<std::setprecision(1)<<"Cylinder "<<s.cylinder_pressure1/1000<<" kPa Crankcase "
<<s.crankcase_pressure/1000<<" kPa Intake "<<s.intake_pressure/1000<<" kPa Carb AFR "
<<s.target_afr<<":1";
draw_label(window,font,pressures.str(),{22,580},sf::Color::White,17);
draw_label(window,font,"54 x 54.5 mm | 8.2:1 trapped compression | estimated pipe/ports/CDI | fixed-open power valve",
{22,625},{180,190,200},11);
}
void draw_twin_scene(sf::RenderWindow& window, const sf::Font* font, const ScenarioSnapshot& state) {
constexpr float intake_y = 310.0f;
constexpr float upper_y = 152.0f;
constexpr float lower_y = 400.0f;
window.clear({9, 12, 17});
// Intake: ambient pipe → throttle → plenum → two cylinder runners.
draw_pipe(window, state, 0, intake_y, state.turbo_enabled ? 135.f : 36.f, 165.0f);
if (state.turbo_enabled) {
draw_pipe(window, state, 6, intake_y, 36, 90);
draw_volume(window, float(state.turbo.charge_pressure), {{95, intake_y-18}, {35, 36}});
draw_label(window, font, "COMP", {92, intake_y+30}, {130,215,220}, 10);
}
sf::RectangleShape throttle({8.0f, 28.0f});
throttle.setPosition({169.0f, intake_y - 14.0f});
throttle.setFillColor(sf::Color::Yellow);
window.draw(throttle);
draw_volume(window, state.intake_pressure, {{195.0f, intake_y - 45.0f}, {62.0f, 90.0f}});
draw_pipe(window, state, 1, upper_y + 71.0f, 263.0f, 458.0f);
draw_pipe(window, state, 2, lower_y + 71.0f, 263.0f, 458.0f);
draw_cylinder(window, font, state.cylinder_pressure1, state.cylinder_piston_fraction1,
upper_y, "CYL 1", state.valve1_intake_area > 1e-7f, state.valve1_exhaust_area > 1e-7f);
draw_cylinder(window, font, state.cylinder_pressure2, state.cylinder_piston_fraction2,
lower_y, "CYL 2", state.valve2_intake_area > 1e-7f, state.valve2_exhaust_area > 1e-7f);
// Two independent exhaust headers enter a shared collector and tailpipe.
draw_pipe(window, state, 3, upper_y + 71.0f, 556.0f, 820.0f);
draw_pipe(window, state, 4, lower_y + 71.0f, 556.0f, 820.0f);
draw_volume(window, state.exhaust_pressure, {{838.0f, 305.0f}, {70.0f, 82.0f}});
draw_pipe(window, state, 5, 346.0f, 915.0f, state.turbo_enabled ? 1040.f : 1236.f);
if (state.turbo_enabled) {
draw_volume(window, float(state.turbo.turbine_inlet_pressure), {{1045, 323}, {45, 46}});
draw_label(window, font, "TURBINE", {1040, 380}, {130,215,220}, 10);
draw_pipe(window, state, 7, 346, 1100, 1236);
}
draw_label(window, font, "AMBIENT", {37.0f, intake_y + 22.0f}, {180, 190, 200}, 12);
draw_label(window, font, "THROTTLE", {151.0f, intake_y + 24.0f}, {240, 210, 90}, 11);
draw_label(window, font, "INTAKE PLENUM", {188.0f, intake_y - 62.0f}, sf::Color::White, 11);
draw_label(window, font, "SHARED EXHAUST", {818.0f, 285.0f}, sf::Color::White, 11);
draw_label(window, font, "TAILPIPE", {1084.0f, 362.0f}, {180, 190, 200}, 11);
}
void draw_b205_scene(sf::RenderWindow& window, const sf::Font* font, const ScenarioSnapshot& s) {
window.clear({9, 12, 17});
draw_label(window, font, s.turbo_enabled ? "SAAB B205 | GT17 turbo | ECU wastegate control" : "SAAB B205 | naturally aspirated baseline",
{260, 181}, sf::Color::White, 16);
draw_pipe(window, s, 0, 215, 25, 110);
if (s.turbo_enabled) {
draw_pipe(window, s, 10, 320, 25, 70);
draw_volume(window, float(s.turbo.charge_pressure), {{75, 300}, {35, 38}});
draw_graph_line(window, {90, 300}, {25, 215}, {80,145,200});
draw_label(window, font, "COMP", {75, 345}, sf::Color::White, 11);
}
draw_volume(window, s.intake_pressure, {{120, 195}, {65, 65}});
draw_label(window, font, "PLENUM", {120, 270}, sf::Color::White, 12);
draw_graph_line(window, {190, 211}, {820, 211}, {80, 145, 200});
draw_graph_line(window, {220, 420}, {850, 420}, {200, 120, 80});
for (int i = 0; i < 4; ++i) {
const float x = 230 + i*150;
const std::string title = "CYL " + std::to_string(i+1);
draw_cylinder(window, font, s.cylinder_pressures[i], s.piston_fractions[i], 255, title.c_str(),
s.intake_areas[i] > 1e-7f, s.exhaust_areas[i] > 1e-7f, x);
draw_pipe(window, s, 1+i, 235, x-15, x+75);
draw_pipe(window, s, 5+i, 410, x-15, x+75);
draw_graph_line(window, {x-15, 211}, {x-15, 235}, {80,145,200});
draw_graph_line(window, {x+75, 410}, {x+75, 420}, {200,120,80});
draw_label(window, font, std::to_string(int(s.cylinder_pressures[i]/1000))+" kPa",
{x, 445}, {180,190,205}, 12);
}
draw_volume(window, s.exhaust_pressure, {{850, 393}, {65, 48}});
draw_pipe(window, s, 9, 420, 920, s.turbo_enabled ? 990 : 1240);
if (s.turbo_enabled) {
draw_volume(window, float(s.turbo.turbine_inlet_pressure), {{995, 397}, {55, 45}});
draw_pipe(window, s, 11, 420, 1055, 1240);
draw_label(window, font, "GT17", {1000, 450}, sf::Color::White, 12);
draw_label(window, font, "Boost target " + std::to_string(int(s.boost_target/1000)) + " kPa gauge | WG duty " + std::to_string(int(100*s.wastegate_duty)) + "%",
{25, 515}, sf::Color::White, 14);
}
draw_label(window, font, s.turbo_enabled ? "COLLECTOR / GT17 / EXHAUST" : "COLLECTOR / OPEN EXHAUST", {930, 375}, sf::Color::White, 12);
draw_label(window, font, "9-5 wagon baseline: 1675 kg with driver, five-speed manual, 4.05 final drive",
{25, 550}, sf::Color::White, 14);
draw_label(window, font, "Engine dimensions/firing from Saab WIS. Manifolds, cams, losses and ECU are estimates.",
{25, 580}, {180,190,205}, 12);
}
void draw_scene(sf::RenderWindow& window, const sf::Font* font, const ScenarioSnapshot& state,
float throttle_target, float brake, double playback_speed, std::size_t buffer_fill,
std::size_t buffer_capacity, bool time_warp, DynoCurve& dyno_curve, std::uint64_t underrun_blocks, bool auto_gain) {
const float power_kw = state.average_power / 1000.0f;
dyno_curve.update(state.rpm, power_kw, state.average_torque);
if (state.cylinder_count == 8) {
window.clear({9,12,17});
draw_label(window, font, "American 5.0 V8 | EFI / ECU | 7000 RPM | heavy flywheel", {25,185}, sf::Color::White, 16);
draw_pipe(window, state, 0, 205, 25, 115);
draw_volume(window, state.intake_pressure, {{120,200},{60,25}});
draw_label(window, font, "ONE THROTTLE / SHARED PLENUM", {850,225}, sf::Color::White, 12);
for (int i = 0; i < 8; ++i) {
const float x = 70 + (i%4)*185;
const float top = i < 4 ? 230.f : 455.f;
const std::string name = "CYL " + std::to_string(i+1);
draw_cylinder(window, font, state.cylinder_pressures[i], state.piston_fractions[i], top,
name.c_str(), state.intake_areas[i]>1e-7f, state.exhaust_areas[i]>1e-7f, x);
draw_pipe(window, state, 9+i, top+150, x-10, x+90);
draw_pipe(window, state, 1+i, top+125, x-10, x+90);
draw_graph_line(window, {x+90,top+150}, {830,top+150}, {200,120,80});
}
draw_graph_line(window, {830,380}, {850,300}, {200,120,80});
draw_graph_line(window, {830,605}, {850,375}, {200,120,80});
draw_pipe(window, state, 17, 300, 850, 1250);
draw_pipe(window, state, 18, 375, 850, 1250);
draw_label(window, font, "SEPARATE BANK COLLECTORS / DUAL TAILS", {850,410}, sf::Color::White, 11);
draw_label(window, font, "EFI AFR " + std::to_string(state.target_afr), {850,430}, sf::Color::White, 13);
draw_label(window, font, "302-class | shared 4 L intake, 12 cm runners | 7:1 compression | 750 rpm idle", {25,690}, {180,190,205}, 12);
}
else if (state.two_stroke) draw_two_stroke_scene(window, font, state);
else if (state.cylinder_count == 4) draw_b205_scene(window, font, state);
else draw_twin_scene(window, font, state);
draw_dashboard(window, font, state, throttle_target, brake, playback_speed, buffer_fill,
buffer_capacity, time_warp, underrun_blocks, auto_gain);
draw_dyno_curve(window, font, dyno_curve, 860, 455, 400, 250, state.rpm, power_kw);
window.display();
}
} // namespace } // namespace
int main(int argc, char** argv) { int main(int argc, char** argv) {
using namespace fluidsim::port; using namespace fluidsim::port;
ScenarioKind selected_kind=ScenarioKind::Twin; ScenarioKind selected_kind=ScenarioKind::Twin;
bool scenario_from_args=false; bool scenario_from_args=false;
bool turbo_from_args=false;
std::string turbo_name;
TwoStrokeConfig two_stroke_config; TwoStrokeConfig two_stroke_config;
ExhaustConfig exhaust_config; V8ExhaustConfig v8_config; EngineConfig engine_config;
const auto examples=scenario_examples();
std::string example_title;
PipeSolver selected_solver=PipeSolver::Rusanov;
OutdoorScene selected_scene=OutdoorScene::Wall; OutdoorScene selected_scene=OutdoorScene::Wall;
for(int i=1;i<argc;++i){ for(int i=1;i<argc;++i){
const std::string option=argv[i]; const std::string option=argv[i];
if((option=="--scenario"||option=="--scene"||option=="--exhaust"||option=="--turbo")&&i+1<argc){ if((option=="--example"||option=="--scenario"||option=="--scene"||option=="--exhaust"||option=="--solver")&&i+1<argc){
const std::string value=argv[++i]; const std::string value=argv[++i];
if(option=="--scenario"){ if(option=="--example") {
const auto found=std::find_if(examples.begin(),examples.end(),[&](const auto& e){return e.id==value;});
if(found==examples.end()){std::cerr<<"Unknown example: "<<value<<"\n";return 2;}
selected_kind=found->kind;two_stroke_config=found->two;exhaust_config=found->exhaust;v8_config=found->v8;engine_config=found->engine;example_title=found->title;scenario_from_args=true;
} else if(option=="--scenario"){
if(value!="twin"&&value!="two-stroke"&&value!="b205"&&value!="v8"){std::cerr<<"Choose twin, two-stroke, b205 or v8\n";return 2;} if(value!="twin"&&value!="two-stroke"&&value!="b205"&&value!="v8"){std::cerr<<"Choose twin, two-stroke, b205 or v8\n";return 2;}
selected_kind=value=="v8" ? ScenarioKind::AmericanV8 : value=="b205" ? ScenarioKind::SaabB205 : selected_kind=value=="v8" ? ScenarioKind::AmericanV8 : value=="b205" ? ScenarioKind::SaabB205 :
(value=="two-stroke" ? ScenarioKind::TwoStroke : ScenarioKind::Twin); (value=="two-stroke" ? ScenarioKind::TwoStroke : ScenarioKind::Twin);
scenario_from_args=true; scenario_from_args=true;
}else if(option=="--turbo"){ }else if(option=="--solver"){
if(value!="none"&&value!="vz21"&&value!="gt17"){std::cerr<<"Choose none, vz21 or gt17 turbo\n";return 2;} try { selected_solver=parse_pipe_solver(value); }
turbo_name=value; catch(const std::exception& e) { std::cerr<<e.what()<<"\n"; return 2; }
turbo_from_args=value!="none";
}else if(option=="--exhaust"){ }else if(option=="--exhaust"){
if(value=="racing") two_stroke_config.exhaust=TwoStrokeExhaust::Racing; if(value=="racing") two_stroke_config.exhaust=TwoStrokeExhaust::Racing;
else if(value=="straight") two_stroke_config.exhaust=TwoStrokeExhaust::Straight; else if(value=="straight") two_stroke_config.exhaust=TwoStrokeExhaust::Straight;
@@ -531,13 +63,16 @@ int main(int argc, char** argv) {
else if(value=="field")selected_scene=OutdoorScene::Field; else if(value=="field")selected_scene=OutdoorScene::Field;
else if(value=="wall")selected_scene=OutdoorScene::Wall; else if(value=="wall")selected_scene=OutdoorScene::Wall;
else if(value=="woodland")selected_scene=OutdoorScene::Woodland; else if(value=="woodland")selected_scene=OutdoorScene::Woodland;
else {std::cerr<<"Choose dry, field, wall or woodland\n";return 2;} else if(value=="tunnel")selected_scene=OutdoorScene::Tunnel;
else {std::cerr<<"Choose dry, field, wall, woodland or tunnel\n";return 2;}
} }
}else{std::cerr<<"Usage: fluidsim_desktop [--scenario twin|two-stroke|b205|v8] [--scene dry|field|wall|woodland] [--exhaust racing|straight] [--turbo none|vz21|gt17]\n";return 2;} }else{std::cerr<<"Usage: fluidsim_desktop [--example catalog-id] [--scenario twin|two-stroke|b205|v8] [--scene dry|field|wall|woodland|tunnel] [--exhaust racing|straight] [--solver rusanov|hlle|hllc]\n";return 2;}
} }
const sf::Vector2u initial_size = scenario_from_args ? sf::Vector2u{window_width, window_height} const sf::Vector2u initial_size = scenario_from_args ? sf::Vector2u{window_width, window_height}
: sf::Vector2u{720, 606}; : sf::Vector2u{720, 606};
sf::RenderWindow window(sf::VideoMode(initial_size), "FluidSim"); sf::ContextSettings context;
context.depthBits=24;context.antiAliasingLevel=4;context.majorVersion=2;context.minorVersion=1;
sf::RenderWindow window(sf::VideoMode(initial_size), "FluidSim",sf::Style::Default,sf::State::Windowed,context);
window.setVerticalSyncEnabled(false); window.setVerticalSyncEnabled(false);
window.setFramerateLimit(60); window.setFramerateLimit(60);
window.setKeyRepeatEnabled(false); window.setKeyRepeatEnabled(false);
@@ -555,25 +90,22 @@ int main(int argc, char** argv) {
const sf::Font* font_ptr=has_font?&font:nullptr; const sf::Font* font_ptr=has_font?&font:nullptr;
if (!scenario_from_args) { if (!scenario_from_args) {
const auto choice = choose_scenario(window, font_ptr); const auto choice = desktop::choose_scenario(window, font_ptr,examples);
if (!choice) return 0; if (!choice) return 0;
selected_kind = *choice; const auto& example=examples[*choice];
selected_kind=example.kind;two_stroke_config=example.two;exhaust_config=example.exhaust;v8_config=example.v8;engine_config=example.engine;example_title=example.title;
window.setSize({window_width, window_height}); window.setSize({window_width, window_height});
window.setView(sf::View(sf::FloatRect({0, 0}, {float(window_width), float(window_height)}))); window.setView(sf::View(sf::FloatRect({0, 0}, {float(window_width), float(window_height)})));
} }
if(turbo_from_args) { window.setTitle(selected_kind == ScenarioKind::AmericanV8 ? "FluidSim - 1980s American 5.0 V8" : selected_kind == ScenarioKind::SaabB205 ? "FluidSim - Saab B205 / 9-5 wagon" :
if(turbo_name=="gt17" && selected_kind==ScenarioKind::SaabB205) {
// Saab preset selects its GT17 calibration.
} else if(turbo_name=="vz21" && (selected_kind==ScenarioKind::Twin || selected_kind==ScenarioKind::TurboTwin)) {
selected_kind=ScenarioKind::TurboTwin;
} else {
std::cerr<<"Use gt17 with b205 or vz21 with twin\n";return 2;
}
}
window.setTitle(selected_kind == ScenarioKind::AmericanV8 ? "FluidSim - 1980s American 5.0 V8" : selected_kind == ScenarioKind::TurboTwin ? "FluidSim - twin + VZ21 turbo" : selected_kind == ScenarioKind::SaabB205 ? "FluidSim - Saab B205 / 9-5 wagon" :
(selected_kind == ScenarioKind::TwoStroke ? "FluidSim - YZ125 two-stroke" : "FluidSim - parallel twin")); (selected_kind == ScenarioKind::TwoStroke ? "FluidSim - YZ125 two-stroke" : "FluidSim - parallel twin"));
DesktopScenario scenario(selected_kind, two_stroke_config, turbo_name!="none"); window.setMinimumSize(sf::Vector2u{1040,800});
if(!example_title.empty())window.setTitle("FluidSim - "+example_title);
DesktopScenario scenario(selected_kind, two_stroke_config,exhaust_config,v8_config,engine_config,example_title);
scenario.set_pipe_solver(selected_solver);
scenario.set_audio_scene(selected_scene); scenario.set_audio_scene(selected_scene);
std::cout << "Desktop started: scenario=" << static_cast<int>(selected_kind)
<< " solver=" << pipe_solver_name(selected_solver) << " scene=" << scene_name(selected_scene) << std::endl;
SimulationRingBuffer ring(ring_capacity); SimulationRingBuffer ring(ring_capacity);
for (std::size_t i = 0; i < preroll_samples; ++i) { for (std::size_t i = 0; i < preroll_samples; ++i) {
const AudioFrame audio = scenario.process(); const AudioFrame audio = scenario.process();
@@ -598,15 +130,17 @@ int main(int argc, char** argv) {
double current_speed = desired_speed; double current_speed = desired_speed;
double last_normal_speed = 1.0; double last_normal_speed = 1.0;
bool time_warp = false; bool time_warp = false;
float throttle_target = 1.0f, throttle_current = 0.0f; float throttle_current = 0.0f;
float brake_target = 0.0f, brake_current = 0.0f; float brake_target = 0.0f, brake_current = 0.0f;
float clutch_current = 0.0f; float clutch_current = 0.0f;
bool throttle_held = false, clutch_held = false; bool throttle_held = false, throttle_down = false, clutch_held = false;
DynoCurve dyno_curve; desktop::DesktopUi ui;
auto previous_frame = std::chrono::steady_clock::now(); auto previous_frame = std::chrono::steady_clock::now();
while (window.isOpen()) { while (window.isOpen()) {
while (const std::optional event = window.pollEvent()) { while (const std::optional event = window.pollEvent()) {
if(ui.handle_event(*event,window.getSize())) continue;
if(event->is<sf::Event::FocusLost>()) { throttle_held=false; throttle_down=false; clutch_held=false; scenario.set_starter(false); }
if (event->is<sf::Event::Closed>()) window.close(); if (event->is<sf::Event::Closed>()) window.close();
if (const auto* key = event->getIf<sf::Event::KeyPressed>()) { if (const auto* key = event->getIf<sf::Event::KeyPressed>()) {
switch (key->code) { switch (key->code) {
@@ -614,33 +148,41 @@ int main(int argc, char** argv) {
time_warp = !time_warp; time_warp = !time_warp;
if (!time_warp) { desired_speed = last_normal_speed; } if (!time_warp) { desired_speed = last_normal_speed; }
break; break;
case sf::Keyboard::Key::PageUp:
if(!time_warp) {desired_speed=std::min(1.,desired_speed*1.1);last_normal_speed=desired_speed;} break;
case sf::Keyboard::Key::PageDown:
if(!time_warp) {desired_speed=std::max(.0001,desired_speed/1.1);last_normal_speed=desired_speed;} break;
case sf::Keyboard::Key::W: throttle_held = true; break; case sf::Keyboard::Key::W: throttle_held = true; break;
case sf::Keyboard::Key::V: scenario.set_starter(true); break;
case sf::Keyboard::Key::C: clutch_held = true; break; case sf::Keyboard::Key::C: clutch_held = true; break;
case sf::Keyboard::Key::Up: throttle_target = std::min(1.0f, throttle_target + 0.05f); break; case sf::Keyboard::Key::Up: ui.nudge_pedal(.05f); break;
case sf::Keyboard::Key::Down: throttle_target = std::max(0.0f, throttle_target - 0.05f); break; case sf::Keyboard::Key::Down: ui.nudge_pedal(-.05f); break;
case sf::Keyboard::Key::Left: brake_target = std::max(0.0f, brake_target - 0.05f); break; case sf::Keyboard::Key::Left: brake_target = std::max(0.0f, brake_target - 0.05f); break;
case sf::Keyboard::Key::Right: brake_target = std::min(1.0f, brake_target + 0.05f); break; case sf::Keyboard::Key::Right: brake_target = std::min(1.0f, brake_target + 0.05f); break;
case sf::Keyboard::Key::D: scenario.shift_up(); break; case sf::Keyboard::Key::D: scenario.shift_up(); break;
case sf::Keyboard::Key::A: scenario.shift_down(); break; case sf::Keyboard::Key::A: scenario.shift_down(); break;
case sf::Keyboard::Key::Z: scenario.set_neutral(); break; case sf::Keyboard::Key::Z: scenario.set_neutral(); break;
case sf::Keyboard::Key::S: throttle_down=true; break;
case sf::Keyboard::Key::F3:
selected_solver=static_cast<PipeSolver>((static_cast<int>(selected_solver)+1)%3);
scenario.set_pipe_solver(selected_solver);
std::cout << "Pipe solver: " << pipe_solver_name(selected_solver) << std::endl;
break;
case sf::Keyboard::Key::R: case sf::Keyboard::Key::R:
selected_scene=static_cast<OutdoorScene>((static_cast<int>(selected_scene)+1)%4); selected_scene=static_cast<OutdoorScene>((static_cast<int>(selected_scene)+1)%5);
scenario.set_audio_scene(selected_scene); scenario.set_audio_scene(selected_scene);
break; break;
case sf::Keyboard::Key::M: scenario.toggle_mechanical_sound(); break;
case sf::Keyboard::Key::G: audio.set_auto_gain(!audio.auto_gain()); break; case sf::Keyboard::Key::G: audio.set_auto_gain(!audio.auto_gain()); break;
default: break; default: break;
} }
} }
if (const auto* key = event->getIf<sf::Event::KeyReleased>()) { if (const auto* key = event->getIf<sf::Event::KeyReleased>()) {
if (key->code == sf::Keyboard::Key::V) scenario.set_starter(false);
if (key->code == sf::Keyboard::Key::W) throttle_held = false; if (key->code == sf::Keyboard::Key::W) throttle_held = false;
if (key->code == sf::Keyboard::Key::S) throttle_down = false;
if (key->code == sf::Keyboard::Key::C) clutch_held = false; if (key->code == sf::Keyboard::Key::C) clutch_held = false;
} }
if (const auto* wheel = event->getIf<sf::Event::MouseWheelScrolled>(); wheel && !time_warp) {
desired_speed *= wheel->delta > 0.0f ? 1.1 : (wheel->delta < 0.0f ? 1.0 / 1.1 : 1.0);
desired_speed = std::clamp(desired_speed, 0.0001, 1.0);
last_normal_speed = desired_speed;
}
} }
const auto now = std::chrono::steady_clock::now(); const auto now = std::chrono::steady_clock::now();
@@ -650,15 +192,18 @@ int main(int argc, char** argv) {
current_speed += (target_speed - current_speed) * (1.0 - std::exp(-8.0 * frame_dt)); current_speed += (target_speed - current_speed) * (1.0 - std::exp(-8.0 * frame_dt));
audio.set_speed(current_speed); audio.set_speed(current_speed);
const float wanted_throttle = throttle_held ? throttle_target : 0.0f; ui.update_pedal(frame_dt,throttle_held,throttle_down);
const float wanted_throttle = ui.pedal();
throttle_current = wanted_throttle; // ECU actuator evolves at the simulation sample rate. throttle_current = wanted_throttle; // ECU actuator evolves at the simulation sample rate.
brake_current += (brake_target - brake_current) * (1.0f - std::exp(-throttle_lerp_rate * frame_dt)); brake_current += (brake_target - brake_current) * (1.0f - std::exp(-throttle_lerp_rate * frame_dt));
const float wanted_clutch = clutch_held ? 1.0f : 0.0f; const float wanted_clutch = clutch_held ? 1.0f : 0.0f;
clutch_current += (wanted_clutch - clutch_current) * (1.0f - std::exp(-throttle_lerp_rate * frame_dt)); clutch_current += (wanted_clutch - clutch_current) * (1.0f - std::exp(-throttle_lerp_rate * frame_dt));
scenario.set_controls(throttle_current, brake_current, clutch_current); scenario.set_controls(throttle_current, brake_current, clutch_current);
draw_scene(window, font_ptr, scenario.snapshot(), throttle_target, brake_current, const desktop::PlaybackInfo playback{throttle_current,brake_current,current_speed,
current_speed, ring.available_samples(), ring_capacity, time_warp, dyno_curve, audio.underrun_blocks(), audio.auto_gain()); ring.available_samples(),ring_capacity,audio.underrun_blocks(),audio.auto_gain(),time_warp};
ui.draw(window,font_ptr,scenario.snapshot(),playback);
} }
simulation.request_stop(); simulation.request_stop();

119
src/desktop_ui.cpp Normal file
View File

@@ -0,0 +1,119 @@
#include "FluidSim/Desktop/DesktopUi.hpp"
#include "FluidSim/Desktop/EngineSchematic.hpp"
#include "FluidSim/Desktop/DynoRenderer.hpp"
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <sstream>
namespace fluidsim::port::desktop {
namespace {
const sf::Color background{8,8,8},panel{22,22,22},border{52,52,52},muted{160,160,160},accent{235,235,235};
void label(sf::RenderWindow& w,const sf::Font* f,const std::string& text,float x,float y,unsigned size=13,sf::Color color=sf::Color::White) {
if(!f)return;
sf::Text t(*f,text,size);t.setPosition({x,y});t.setFillColor(color);w.draw(t);
}
void box(sf::RenderWindow& w,sf::FloatRect r,sf::Color fill=panel) {
sf::RectangleShape s(r.size);s.setPosition(r.position);s.setFillColor(fill);s.setOutlineColor(border);s.setOutlineThickness(1);w.draw(s);
}
std::string number(float v,int precision=0) {std::ostringstream s;s<<std::fixed<<std::setprecision(precision)<<v;return s.str();}
const char* title(const ScenarioSnapshot& s) {return !s.example_name.empty()?s.example_name.c_str():s.two_stroke?"YZ125 / TWO STROKE":s.cylinder_count==8?"AMERICAN 5.0 / V8":s.cylinder_count==4?"SAAB B205 / INLINE FOUR":"600 CC / PARALLEL TWIN";}
}
WorkspaceLayout WorkspaceLayout::calculate(sf::Vector2u size) {
const float w=std::max(640.f,float(size.x)),h=std::max(640.f,float(size.y));
const float side=std::clamp(w*.25f,230.f,272.f);
const float left=side+32,content=w-left-16,bottom=h-226;
return {{{16,80},{side,h-128}},{{left,80},{content,bottom-96}},
{{left,bottom},{content*.60f-8,178}},{{left+content*.60f+8,bottom},{content*.40f-8,178}},{{32,h<800?382.f:414.f},{side-32,26}}};
}
bool DesktopUi::handle_event(const sf::Event& event,sf::Vector2u size) {
const auto layout=WorkspaceLayout::calculate(size);
const auto set_pedal=[&](int x) {pedal_.set((x-layout.pedal.position.x)/layout.pedal.size.x);};
if(const auto* p=event.getIf<sf::Event::MouseButtonPressed>()) {
if(p->button==sf::Mouse::Button::Left && layout.pedal.contains(sf::Vector2f(p->position))) {
pedal_drag_=true;set_pedal(p->position.x);return true;
}
if(layout.viewport.contains(sf::Vector2f(p->position))) {
dragging_=true;pan_drag_=p->button!=sf::Mouse::Button::Left;previous_mouse_=p->position;return true;
}
}
if(event.is<sf::Event::MouseButtonReleased>() || event.is<sf::Event::FocusLost>()) {
if(pedal_drag_ || event.is<sf::Event::FocusLost>())pedal_.set(0);
dragging_=false;pedal_drag_=false;
}
if(const auto* p=event.getIf<sf::Event::MouseMoved>()) {
if(pedal_drag_) {set_pedal(p->position.x);return true;}
if(dragging_) {
const auto delta=p->position-previous_mouse_;previous_mouse_=p->position;
if(three_d_) {if(pan_drag_)camera_.pan(delta,layout.viewport.size.y-56);else camera_.orbit(delta);}
else pan_-=sf::Vector2f(delta)*(1280.f/layout.viewport.size.x)*zoom_;
return true;
}
}
if(const auto* p=event.getIf<sf::Event::MouseWheelScrolled>();p && layout.viewport.contains(sf::Vector2f(p->position))) {
if(three_d_)camera_.zoom(p->delta);else zoom_=std::clamp(zoom_*std::exp(-p->delta*.12f),.3f,3.f);
return true;
}
if(const auto* p=event.getIf<sf::Event::KeyPressed>()) {
if(p->code==sf::Keyboard::Key::Home) {pan_={};zoom_=1;camera_fitted_=false;return true;}
if(p->code==sf::Keyboard::Key::End) {pedal_.set(0);return true;}
if(p->code==sf::Keyboard::Key::Tab) {three_d_=!three_d_;return true;}
}
return false;
}
void DesktopUi::draw(sf::RenderWindow& w,const sf::Font* font,const ScenarioSnapshot& s,const PlaybackInfo& p) {
const auto size=w.getSize();const float width=float(size.x),height=float(size.y);
w.setView(sf::View(sf::FloatRect({0,0},{width,height})));w.clear(background);
const auto l=WorkspaceLayout::calculate(size);
label(w,font,"FLUIDSIM",20,15,24,accent);label(w,font,"ENGINE WORKSPACE",20,48,10,muted);
label(w,font,title(s),l.viewport.position.x+2,22,19);label(w,font,"LIVE / "+number(float(p.speed),3)+"x",width-160,25,14,accent);
box(w,l.sidebar);box(w,l.viewport);box(w,l.chart);box(w,l.inspector);
const float x=32;float y=97;
label(w,font,"ENGINE",x,y,11,accent);y+=25;
label(w,font,number(s.rpm)+" RPM",x,y,30);y+=47;
auto row=[&](const std::string& name,const std::string& value) {label(w,font,name,x,y,12,muted);label(w,font,value,x+136,y,12);y+=height<800?19.f:23.f;};
row("Power",number(s.average_power/1000,1)+" kW");row("Torque",number(s.average_torque,1)+" Nm");
row("Vehicle",number(s.speed_kmh,1)+" km/h");row("Gear",s.gear?std::to_string(s.gear):"N");
y+=12;label(w,font,"CONTROLS",x,y,11,accent);y+=28;
row("Throttle",number(s.throttle*100,1)+" %");
row("Clutch",number(s.clutch*100)+" %");row("Engine load",number(p.load*100)+" %");row("AFR",number(s.target_afr,1));
label(w,font,"Pedal lever",x,l.pedal.position.y-22,11,muted);
label(w,font,number(p.pedal_target*100)+" %",x+180,l.pedal.position.y-22,11,accent);
box(w,{{l.pedal.position.x,l.pedal.position.y+10},{l.pedal.size.x,6}},background);
box(w,{{l.pedal.position.x,l.pedal.position.y+10},{l.pedal.size.x*p.pedal_target,6}},accent);
box(w,{{l.pedal.position.x+l.pedal.size.x*p.pedal_target-5,l.pedal.position.y+2},{10,22}},sf::Color(225,225,225));
y=l.pedal.position.y+40;
y+=12;label(w,font,"AUDIO & SIMULATION",x,y,11,accent);y+=28;
row("Solver [F3]",pipe_solver_name(s.pipe_solver));row("Scene [R]",scene_name(s.audio_scene));row("Auto gain [G]",p.auto_gain?"On":"Off");
row("Buffer",std::to_string(p.buffered)+" / "+std::to_string(p.capacity));row("Underruns",std::to_string(p.underruns));
label(w,font,s.fuel_cut?"FUEL CUT":s.idle_control?"IDLE CONTROL":"RUNNING",x,y+8,12,s.fuel_cut?sf::Color(255,171,96):accent);
if(height>=800) label(w,font,"W/S Raise / lower pedal\nA/D Shift Z Neutral\nC Clutch V Starter\nArrows Load End Release\nPgUp/Dn Speed Space 1x",x,height-141,11,muted);
label(w,font,three_d_?"ENGINE VIEW / 3D":"ENGINE VIEW / SCHEMATIC",l.viewport.position.x+16,93,11,accent);
label(w,font,three_d_?"Drag: orbit Right: pan Scroll: zoom Home: fit Tab: 2D":"Drag: pan Scroll: zoom Home: reset Tab: 3D",l.viewport.position.x+16,117,11,muted);
const sf::FloatRect drawing{{l.viewport.position.x,l.viewport.position.y+56},{l.viewport.size.x,l.viewport.size.y-56}};
if(!camera_fitted_) {camera_.reset(drawing.size.x/drawing.size.y);camera_fitted_=true;}
if(three_d_) renderer_.draw(w,s,drawing,camera_);
else {
sf::View view(sf::FloatRect({0,0},{1280,600}));
const float ratio=drawing.size.x/drawing.size.y;
view.setSize(ratio>1280.f/600?sf::Vector2f{600*ratio,600}:sf::Vector2f{1280,1280/ratio});
view.setCenter({640+pan_.x,400+pan_.y});view.zoom(zoom_);
view.setViewport({{drawing.position.x/width,drawing.position.y/height},{drawing.size.x/width,drawing.size.y/height}});
w.setView(view);draw_schematic(w,font,s);
}
w.setView(sf::View(sf::FloatRect({0,0},{width,height})));
dyno_.update(s.rpm,s.average_power/1000,s.average_torque);
draw_dyno_curve(w,font,dyno_,l.chart.position.x+8,l.chart.position.y+8,l.chart.size.x-16,l.chart.size.y-16,s.rpm,s.average_power/1000);
const float ix=l.inspector.position.x+14,iy=l.inspector.position.y+12;
label(w,font,"CYLINDERS",ix,iy,11,accent);label(w,font,"# kPa % I / E",ix,iy+22,10,muted);
for(int i=0;i<s.cylinder_count;++i) {
const float pressure=s.two_stroke?s.cylinder_pressure1:s.cylinder_pressures[i];
const float stroke=s.two_stroke?s.cylinder_piston_fraction1:s.piston_fractions[i];
const bool intake=s.two_stroke?s.valve1_intake_area>0:s.intake_areas[i]>0;
const bool exhaust=s.two_stroke?s.valve1_exhaust_area>0:s.exhaust_areas[i]>0;
label(w,font,std::to_string(i+1)+" "+number(pressure/1000)+" "+number(stroke*100)+"% "+(intake?"O":"-")+" / "+(exhaust?"O":"-"),ix+(s.cylinder_count>8?i/6*(l.inspector.size.x/2-4):0),iy+42+(s.cylinder_count>8?i%6:i)*15,10);
}
label(w,font,"End: release pedal",20,height-29,11,muted);
label(w,font,"Pressure: blue vacuum / green atmosphere / red 1 MPa",l.viewport.position.x+2,height-29,11,muted);
w.display();
}
}

128
src/dyno_renderer.cpp Normal file
View File

@@ -0,0 +1,128 @@
#include "FluidSim/Desktop/DynoRenderer.hpp"
#include "FluidSim/Constants.hpp"
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <sstream>
namespace fluidsim::port::desktop {
namespace {
void draw_label(sf::RenderWindow& window, const sf::Font* font, const std::string& text,
sf::Vector2f position, sf::Color color = sf::Color::White, unsigned int size = 14) {
if (!font) return;
sf::Text label(*font);
label.setString(text);
label.setPosition(position);
label.setFillColor(color);
label.setCharacterSize(size);
window.draw(label);
}
void draw_graph_line(sf::RenderWindow& window, sf::Vector2f start, sf::Vector2f end,
sf::Color color, float thickness = 1.5f) {
const float dx = end.x - start.x;
const float dy = end.y - start.y;
const float length = std::sqrt(dx * dx + dy * dy);
if (length <= 0.0f) return;
sf::RectangleShape line({length, thickness});
line.setPosition(start);
line.setRotation(sf::degrees(std::atan2(dy, dx) * 180.0f / pi));
line.setFillColor(color);
window.draw(line);
}
}
void draw_dyno_curve(sf::RenderWindow& window, const sf::Font* font, const DynoCurve& curve,
float graph_x, float graph_y, float graph_width, float graph_height,
float current_rpm, float current_power_kw) {
const auto& bins = curve.bins();
if (bins.empty()) return;
float max_power = 0.01f, max_torque = 0.01f, max_rpm = 1000.0f;
for (std::size_t index = 0; index < bins.size(); ++index) {
if (bins[index].power_kw > 0.0f || bins[index].torque_nm > 0.0f) {
max_power = std::max(max_power, bins[index].power_kw);
max_torque = std::max(max_torque, bins[index].torque_nm);
max_rpm = std::max(max_rpm, static_cast<float>(index) * DynoCurve::rpm_bin_size +
DynoCurve::rpm_bin_size * 0.5f);
}
}
max_power *= 1.1f;
max_torque *= 1.1f;
max_rpm = std::max(max_rpm * 1.05f, 1000.0f);
sf::RectangleShape background({graph_width, graph_height});
background.setPosition({graph_x, graph_y});
background.setFillColor({20, 20, 20, 220});
background.setOutlineColor({70,70,70});
background.setOutlineThickness(1.0f);
window.draw(background);
constexpr float left_margin = 50.0f, right_margin = 48.0f;
constexpr float top_margin = 24.0f, bottom_margin = 38.0f;
const float plot_x = graph_x + left_margin;
const float plot_y = graph_y + top_margin;
const float plot_width = graph_width - left_margin - right_margin;
const float plot_height = graph_height - top_margin - bottom_margin;
const sf::Color grid_color(50, 50, 50);
for (int index = 0; index <= 9; ++index) {
const float x = plot_x + static_cast<float>(index) / 9.0f * plot_width;
sf::RectangleShape grid({1.0f, plot_height});
grid.setPosition({x, plot_y});
grid.setFillColor(grid_color);
window.draw(grid);
}
for (int index = 0; index <= 5; ++index) {
const float y = plot_y + (1.0f - static_cast<float>(index) / 5.0f) * plot_height;
sf::RectangleShape grid({plot_width, 1.0f});
grid.setPosition({plot_x, y});
grid.setFillColor(grid_color);
window.draw(grid);
draw_label(window, font, std::to_string(static_cast<int>(max_power * index / 5.0f)),
{graph_x + 3.0f, y - 7.0f}, sf::Color(245,245,245), 9);
draw_label(window, font, std::to_string(static_cast<int>(max_torque * index / 5.0f)),
{graph_x + graph_width - 42.0f, y - 7.0f}, sf::Color(165,165,165), 9);
}
for (int index = 0; index <= 9; ++index) {
const float rpm = max_rpm * static_cast<float>(index) / 9.0f;
const float x = plot_x + static_cast<float>(index) / 9.0f * plot_width;
std::ostringstream tick;
tick << std::fixed << std::setprecision(1) << rpm / 1000.0f << 'k';
draw_label(window, font, tick.str(), {x - 12.0f, graph_y + graph_height - 31.0f},
sf::Color(180,180,180), 9);
}
draw_label(window, font, "DYNO", {graph_x + 8.0f, graph_y + 4.0f}, sf::Color::White, 11);
draw_label(window, font, "kW", {graph_x + 8.0f, graph_y + 17.0f}, sf::Color(245,245,245), 9);
draw_label(window, font, "Nm", {graph_x + graph_width - 30.0f, graph_y + 17.0f},
sf::Color(165,165,165), 9);
draw_label(window, font, "RPM", {graph_x + graph_width * 0.5f - 12.0f,
graph_y + graph_height - 15.0f}, sf::Color::White, 10);
const sf::Color power_color(245,245,245), torque_color(140,140,140);
for (std::size_t index = 1; index < bins.size(); ++index) {
const float rpm0 = (static_cast<float>(index - 1) + 0.5f) * DynoCurve::rpm_bin_size;
const float rpm1 = (static_cast<float>(index) + 0.5f) * DynoCurve::rpm_bin_size;
if (rpm0 > max_rpm || rpm1 > max_rpm) break;
const float x0 = plot_x + rpm0 / max_rpm * plot_width;
const float x1 = plot_x + rpm1 / max_rpm * plot_width;
if (bins[index - 1].power_kw > 0.0f && bins[index].power_kw > 0.0f) {
const float y0 = plot_y + (1.0f - bins[index - 1].power_kw / max_power) * plot_height;
const float y1 = plot_y + (1.0f - bins[index].power_kw / max_power) * plot_height;
draw_graph_line(window, {x0, y0}, {x1, y1}, power_color, 2.0f);
}
if (bins[index - 1].torque_nm > 0.0f && bins[index].torque_nm > 0.0f) {
const float y0 = plot_y + (1.0f - bins[index - 1].torque_nm / max_torque) * plot_height;
const float y1 = plot_y + (1.0f - bins[index].torque_nm / max_torque) * plot_height;
draw_graph_line(window, {x0, y0}, {x1, y1}, torque_color, 2.0f);
}
}
if (current_rpm > 0.0f && current_rpm <= max_rpm && current_power_kw > 0.0f) {
const float x = plot_x + current_rpm / max_rpm * plot_width;
const float y = plot_y + (1.0f - current_power_kw / max_power) * plot_height;
sf::CircleShape dot(3.0f);
dot.setPosition({x - 3.0f, y - 3.0f});
dot.setFillColor(sf::Color::White);
window.draw(dot);
}
}
}

View File

@@ -3,27 +3,6 @@
#include <cmath> #include <cmath>
namespace fluidsim::port { namespace fluidsim::port {
void Ecu::control_boost(float dt, float pedal, float rpm, float manifold, float charge, float ambient) {
boost_target_ = boost_duty_ = 0;
if (!boost_control_enabled) { boost_integral_ = 0; boost_cut_ = false; return; }
if (!std::isfinite(dt) || dt <= 0 || !std::isfinite(pedal) || !std::isfinite(rpm) ||
!std::isfinite(manifold) || !std::isfinite(charge) || !std::isfinite(ambient) ||
manifold <= 0 || charge <= 0 || ambient <= 0) {
boost_integral_ = 0; boost_cut_ = true; return;
}
const float boost = manifold-ambient;
filtered_boost_ += (boost-filtered_boost_)*(-std::expm1(-dt/.05f));
if (std::max(manifold, charge)-ambient > maximum_boost+30000) boost_cut_ = true;
else if (std::max(manifold, charge)-ambient < maximum_boost+10000) boost_cut_ = false;
boost_target_ = maximum_boost*std::clamp((pedal-.5f)/.5f, 0.f, 1.f)*std::clamp((rpm-1500)/1500, 0.f, 1.f);
if (boost_target_ <= 0 || fuel_cut()) { boost_integral_ = 0; return; }
const float error = boost_target_-filtered_boost_;
const float proposed = boost_integral_+boost_ki*error*dt;
const float command = .3f+boost_kp*error+proposed;
if ((command >= 0 && command <= .85f) || (command > .85f && error < 0) || (command < 0 && error > 0))
boost_integral_ = proposed;
boost_duty_ = std::clamp(.3f+boost_kp*error+boost_integral_, 0.f, .85f);
}
void Ecu::step(float dt, float pedal, float rpm) { 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; fault_ = !std::isfinite(dt) || dt <= 0.0f || !std::isfinite(pedal) || !std::isfinite(rpm) || rpm < 0.0f;
if (fault_) { target_ = 0.0f; idle_ = false; return; } if (fault_) { target_ = 0.0f; idle_ = false; return; }
@@ -38,7 +17,8 @@ void Ecu::step(float dt, float pedal, float rpm) {
filtered_rpm_ += (rpm - filtered_rpm_) * (-std::expm1(-dt / 0.05f)); filtered_rpm_ += (rpm - filtered_rpm_) * (-std::expm1(-dt / 0.05f));
idle_ = pedal <= 0.005f && !overrun_; idle_ = pedal <= 0.005f && !overrun_;
const float error = idle_rpm - filtered_rpm_; const float error = idle_rpm - filtered_rpm_;
if (idle_) { const bool fixed_idle = idle_control == IdleControl::FixedOpening;
if (idle_ && !fixed_idle) {
const float proposed = integral_ + idle_ki * error * dt; const float proposed = integral_ + idle_ki * error * dt;
const float command = idle_base_opening + idle_kp * error + proposed; const float command = idle_base_opening + idle_kp * error + proposed;
// Conditional integration prevents windup at either actuator limit. // Conditional integration prevents windup at either actuator limit.
@@ -48,7 +28,7 @@ void Ecu::step(float dt, float pedal, float rpm) {
} else { } else {
integral_ *= std::exp(-dt / 0.5f); integral_ *= std::exp(-dt / 0.5f);
} }
const float idle_command = idle_ ? std::clamp(idle_base_opening + idle_kp * error + integral_, const float idle_command = fixed_idle ? std::clamp(idle_base_opening,0.f,1.f) : idle_ ? std::clamp(idle_base_opening + idle_kp * error + integral_,
0.0f, idle_max_opening) : 0.0f; 0.0f, idle_max_opening) : 0.0f;
target_ = std::max(pedal, idle_command); target_ = std::max(pedal, idle_command);
const bool closing = target_ < opening_; const bool closing = target_ < opening_;
@@ -71,7 +51,7 @@ void Ecu::step(float dt, float pedal, float rpm) {
if (rpm <= b.rpm) break; if (rpm <= b.rpm) break;
} }
} }
if(idle_ && idle_spark_gain>0) spark_=std::clamp(spark_+idle_spark_gain*error,idle_minimum_advance,35.0f); if(idle_ && !fixed_idle && idle_spark_gain>0) spark_=std::clamp(spark_+idle_spark_gain*error,idle_minimum_advance,35.0f);
} }
float Ecu::fuel_for_air(float air) const { float Ecu::fuel_for_air(float air) const {

254
src/engine_renderer_3d.cpp Normal file
View File

@@ -0,0 +1,254 @@
#include "FluidSim/Desktop/EngineRenderer3D.hpp"
#include "FluidSim/Desktop/PressurePalette.hpp"
#include "FluidSim/Desktop/PipePath.hpp"
#include <SFML/OpenGL.hpp>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <vector>
namespace fluidsim::port::desktop {
namespace {
constexpr float pi3=3.14159265359f;
struct Vertex {float x,y,z,nx,ny,nz;};
struct Mesh {
std::vector<Vertex> vertices;
std::vector<unsigned> indices;
void draw(const std::vector<Vec3>* colors=nullptr) const {
if(colors) {glEnableClientState(GL_COLOR_ARRAY);glColorPointer(3,GL_FLOAT,sizeof(Vec3),colors->data());}
else glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
glEnableClientState(GL_VERTEX_ARRAY);glEnableClientState(GL_NORMAL_ARRAY);
glVertexPointer(3,GL_FLOAT,sizeof(Vertex),vertices.data());
glNormalPointer(GL_FLOAT,sizeof(Vertex),reinterpret_cast<const char*>(vertices.data())+offsetof(Vertex,nx));
glDrawElements(GL_TRIANGLES,static_cast<GLsizei>(indices.size()),GL_UNSIGNED_INT,indices.data());
glDisableClientState(GL_COLOR_ARRAY);glDisableClientState(GL_NORMAL_ARRAY);glDisableClientState(GL_VERTEX_ARRAY);
}
};
Mesh cylinder(bool half=false) {
Mesh m;constexpr unsigned sides=24;
for(unsigned i=0;i<=sides;++i) {
float a=(half?pi3:0)+(half?pi3:2*pi3)*i/sides,c=std::cos(a),s=std::sin(a);
m.vertices.push_back({c,0,s,c,0,s});m.vertices.push_back({c,1,s,c,0,s});
if(i<sides) {unsigned k=2*i;m.indices.insert(m.indices.end(),{k,k+1,k+2,k+2,k+1,k+3});}
}
if(!half) for(unsigned cap=0;cap<2;++cap) {
unsigned center=static_cast<unsigned>(m.vertices.size());float y=float(cap),normal=cap?1.f:-1.f;
m.vertices.push_back({0,y,0,0,normal,0});
for(unsigned i=0;i<=sides;++i) {float a=2*pi3*i/sides;m.vertices.push_back({std::cos(a),y,std::sin(a),0,normal,0});}
for(unsigned i=0;i<sides;++i) m.indices.insert(m.indices.end(),{center,center+1+i,center+2+i});
}
return m;
}
Mesh cube() {
Mesh m;
const std::array<Vec3,6> normals{{{1,0,0},{-1,0,0},{0,1,0},{0,-1,0},{0,0,1},{0,0,-1}}};
const std::array<std::array<Vec3,4>,6> faces{{
{{{.5f,-.5f,-.5f},{.5f,.5f,-.5f},{.5f,.5f,.5f},{.5f,-.5f,.5f}}},
{{{-.5f,-.5f,.5f},{-.5f,.5f,.5f},{-.5f,.5f,-.5f},{-.5f,-.5f,-.5f}}},
{{{-.5f,.5f,-.5f},{-.5f,.5f,.5f},{.5f,.5f,.5f},{.5f,.5f,-.5f}}},
{{{-.5f,-.5f,.5f},{-.5f,-.5f,-.5f},{.5f,-.5f,-.5f},{.5f,-.5f,.5f}}},
{{{-.5f,-.5f,.5f},{.5f,-.5f,.5f},{.5f,.5f,.5f},{-.5f,.5f,.5f}}},
{{{.5f,-.5f,-.5f},{-.5f,-.5f,-.5f},{-.5f,.5f,-.5f},{.5f,.5f,-.5f}}}}};
for(unsigned f=0;f<6;++f) {
unsigned start=static_cast<unsigned>(m.vertices.size());auto n=normals[f];
for(auto p:faces[f])m.vertices.push_back({p.x,p.y,p.z,n.x,n.y,n.z});
m.indices.insert(m.indices.end(),{start,start+1,start+2,start,start+2,start+3});
}
return m;
}
Vec3 sub(Vec3 a,Vec3 b) {return {a.x-b.x,a.y-b.y,a.z-b.z};}
float length(Vec3 a) {return std::sqrt(a.x*a.x+a.y*a.y+a.z*a.z);}
Vec3 pressure_color(float pressure) {return pressure_tint(pressure);}
void color(Vec3 c) {glColor3f(c.x,c.y,c.z);}
struct Route {std::array<Vec3,5> points{};std::array<float,5> fractions{};unsigned count{};};
// Scene points specify a fraction of the pipe's physical length, converted
// to metre-based PipeAnchor distances when constructing the cached surface.
Route route(std::initializer_list<PipeAnchor> p) {
Route r;for(auto v:p) {r.points[r.count]=v.position;r.fractions[r.count++]=v.distance;}return r;
}
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 dot(Vec3 a,Vec3 b) {return a.x*b.x+a.y*b.y+a.z*b.z;}
Vec3 unit(Vec3 a) {float l=length(a);return l>1e-7f?Vec3{a.x/l,a.y/l,a.z/l}:Vec3{1,0,0};}
struct PipeVisual {
struct Ring {unsigned first;std::size_t a,b;float weight;};
Mesh mesh;
std::vector<Vec3> colors;
std::vector<Ring> rings;
std::vector<float> lengths,areas;
Route route;
bool valid{};
bool matches(const ScenarioSnapshot& s,std::size_t index,const Route& r) const {
if(!valid || lengths!=s.pipe_lengths[index] || areas!=s.pipe_areas[index] || r.count!=route.count)return false;
for(unsigned i=0;i<r.count;++i)if(length(sub(r.points[i],route.points[i]))>1e-6f || r.fractions[i]!=route.fractions[i])return false;
return true;
}
void build(const ScenarioSnapshot& s,std::size_t index,const Route& r) {
lengths=s.pipe_lengths[index];areas=s.pipe_areas[index];route=r;
mesh={};rings.clear();
std::vector<float> centers,samples{0};float total=0;
for(float l:lengths) {centers.push_back(total+l*.5f);samples.push_back(total+l*.5f);total+=l;samples.push_back(total);}
std::vector<PipeAnchor> anchors;
for(unsigned i=0;i<r.count;++i) {
const float distance=total*r.fractions[i];
anchors.push_back({distance,r.points[i]});samples.push_back(distance);
}
PipePath path(std::move(anchors));
std::sort(samples.begin(),samples.end());samples.erase(std::unique(samples.begin(),samples.end(),[](float a,float b){return std::abs(a-b)<1e-7f;}),samples.end());
constexpr unsigned sides=20;
Vec3 normal{};
for(float d:samples) {
const auto position=path.position(d),tangent=path.tangent(d);
if(rings.empty())normal=unit(cross(tangent,std::abs(tangent.y)<.9f?Vec3{0,1,0}:Vec3{1,0,0}));
else {float v=dot(normal,tangent);normal=unit({normal.x-v*tangent.x,normal.y-v*tangent.y,normal.z-v*tangent.z});}
const auto binormal=unit(cross(tangent,normal));
std::size_t b=std::min<std::size_t>(std::upper_bound(centers.begin(),centers.end(),d)-centers.begin(),centers.size()-1);
std::size_t a=b?b-1:0;
const float blend=a==b?0:std::clamp((d-centers[a])/(centers[b]-centers[a]),0.f,1.f);
const float radius=std::sqrt((areas[a]+(areas[b]-areas[a])*blend)/pi3);
const unsigned first=static_cast<unsigned>(mesh.vertices.size());
if(!rings.empty())for(unsigned j=0;j<sides;++j) {
unsigned prev=first-sides,k=(j+1)%sides;
mesh.indices.insert(mesh.indices.end(),{prev+j,first+j,prev+k,prev+k,first+j,first+k});
}
rings.push_back({first,a,b,blend});
for(unsigned j=0;j<sides;++j) {
float angle=2*pi3*j/sides,c=std::cos(angle),sn=std::sin(angle);
Vec3 n{normal.x*c+binormal.x*sn,normal.y*c+binormal.y*sn,normal.z*c+binormal.z*sn};
mesh.vertices.push_back({position.x+radius*n.x,position.y+radius*n.y,position.z+radius*n.z,n.x,n.y,n.z});
}
}
colors.resize(mesh.vertices.size());valid=true;
}
void update(const std::vector<GasState>& states) {
constexpr unsigned sides=20;
for(auto ring:rings) {
const float p=states[ring.a].pressure+(states[ring.b].pressure-states[ring.a].pressure)*ring.weight;
std::fill_n(colors.begin()+ring.first,sides,pressure_tint(p));
}
}
};
}
struct EngineRenderer3D::Impl {
Mesh tube=cylinder(),shell=cylinder(true),block=cube();
std::array<PipeVisual,32> pipes;
void rod(Vec3 a,Vec3 b,float radius,Vec3 tint) const {
auto d=sub(b,a);float len=length(d);if(len<1e-6)return;
glPushMatrix();glTranslatef(a.x,a.y,a.z);
const float angle=std::acos(std::clamp(d.y/len,-1.f,1.f))*180/pi3;
if(std::abs(d.x)+std::abs(d.z)>.00001f)glRotatef(angle,d.z,0,-d.x);
else if(d.y<0)glRotatef(180,1,0,0);
glScalef(radius,len,radius);color(tint);tube.draw();glPopMatrix();
}
void box(Vec3 center,Vec3 scale,Vec3 tint) const {glPushMatrix();glTranslatef(center.x,center.y,center.z);glScalef(scale.x,scale.y,scale.z);color(tint);block.draw();glPopMatrix();}
void pipe(const ScenarioSnapshot& s,std::size_t index,const Route& r) {
if(s.pipe_states[index].empty())return;
auto& visual=pipes[index];
if(!visual.matches(s,index,r))visual.build(s,index,r);
visual.update(s.pipe_states[index]);
visual.mesh.draw(&visual.colors);
}
void cylinder_part(Vec3 center,float fraction,float pressure,float angle,float tilt=0) const {
glPushMatrix();glTranslatef(center.x,.1f,center.z);glRotatef(tilt,1,0,0);glTranslatef(-center.x,-.1f,-center.z);
const float piston=.43f-.18f*std::clamp(fraction,0.f,1.f);
glPushMatrix();glTranslatef(center.x,.19f,center.z);glScalef(.059f,.29f,.059f);color({.4f,.4f,.4f});shell.draw();glPopMatrix();
rod({center.x,piston+.025f,center.z},{center.x,.47f,center.z},.047f,pressure_color(pressure));
rod({center.x,piston,center.z},{center.x,piston+.025f,center.z},.05f,{.8f,.8f,.8f});
Vec3 pin{center.x,.1f+.055f*std::cos(angle),center.z+.055f*std::sin(angle)};
rod(pin,{center.x,piston,center.z},.012f,{.65f,.65f,.65f});
rod({center.x,.1f,center.z},pin,.019f,{.42f,.42f,.42f});
box({center.x,.487f,center.z},{.12f,.028f,.12f},{.48f,.48f,.48f});
glPopMatrix();
}
void engine(const ScenarioSnapshot& s) {
if(s.two_stroke) {
cylinder_part({0,0,0},s.cylinder_piston_fraction1,s.cylinder_pressure1,s.cylinder_angles[0]);
box({0,.07f,0},{.21f,.14f,.19f},pressure_color(s.crankcase_pressure));
box({-.55f,.35f,0},{.18f,.12f,.15f},pressure_color(s.intake_pressure));
pipe(s,0,route({{0.0f,{-1.05f,.35f,0}},{1.0f,{-.64f,.35f,0}}}));
pipe(s,1,route({{0.0f,{-.46f,.35f,0}},{0.3333333333333333f,{-.28f,.35f,0}},{0.6666666666666666f,{-.28f,.1f,0}},{1.0f,{-.105f,.1f,0}}}));
pipe(s,2,route({{0.0f,{.105f,.09f,0}},{0.3333333333333333f,{.18f,.09f,-.12f}},{0.6666666666666666f,{.18f,.35f,-.12f}},{1.0f,{.05f,.35f,0}}}));
pipe(s,3,route({{0.0f,{.055f,.40f,0}},{0.3333333333333333f,{.3f,.40f,0}},{0.6666666666666666f,{.58f,.23f,0}},{1.0f,{1.45f,.23f,0}}}));
} else {
const int n=s.cylinder_count;const bool v8=s.v8_headers;
const bool banked=s.layout!=EngineLayout::Inline;const int per_bank=banked?n/2:n;
const float spacing=.21f,first=-(per_bank-1)*spacing*.5f;
box({0,.64f,0},{per_bank*.21f,.12f,.15f},pressure_color(s.intake_pressure));
pipe(s,0,route({{0.0f,{-1.05f,.64f,0}},{0.25f,{-.5f,.64f,0}},{0.5f,{-.5f,.64f,-.1f}},{0.75f,{0,.64f,-.1f}},{1.0f,{0,.64f,0}}}));
for(int i=0;i<n;++i) {
const float x=first+(i%per_bank)*spacing,z=0;
const float bank_tilt=s.layout==EngineLayout::Boxer?90.f:s.bank_angle*.5f;
const float tilt=banked?(i<per_bank?-bank_tilt:bank_tilt):0;
const auto port=[&](float y,float dx) {float a=tilt*pi3/180;return Vec3{x+dx,.1f+(y-.1f)*std::cos(a),z+(y-.1f)*std::sin(a)};};
cylinder_part({x,0,z},s.piston_fractions[i],s.cylinder_pressures[i],s.cylinder_angles[i],tilt);
const float cz=v8?(i<4?-.39f:.39f):.26f;
const auto intake=port(.45f,-.055f),exhaust=port(.43f,.055f);
pipe(s,1+i,route({{0,{x,.58f,0}},{.3f,{x,.55f,intake.z*.5f}},{.7f,{intake.x,.52f,intake.z}},{1,intake}}));
if(!v8) pipe(s,1+n+i,route({{0,exhaust},{.3f,{x+.12f,exhaust.y,cz}},{.6f,{x+.12f,.17f,cz}},{1,{.68f,.17f,cz}}}));
}
if(v8) {
const std::array<int,8> pair{0,0,1,1,2,3,2,3};
for(int i=0;i<8;++i) {
const int bank=i/4, group=pair[i]%2;
const float x=first+(i%4)*spacing, z=bank? .39f:-.39f;
const float a=(bank?28.f:-28.f)*pi3/180;
const Vec3 port{x+.055f,.1f+.33f*std::cos(a),.33f*std::sin(a)};
const float end_z=z+(group? .09f:-.09f);
pipe(s,9+i,route({{0,port},{.3f,{x+.12f,.34f,z}},{.65f,{.35f,.20f,end_z}},{1,{.74f,.17f,end_z}}}));
}
for(int i=0;i<4;++i) {
const float z=i<2?-.39f:.39f;
pipe(s,17+i,route({{0,{.74f,.17f,z+(i%2?.09f:-.09f)}},{.5f,{.94f,.17f,z+(i%2?.06f:-.06f)}},{1,{1.14f,.17f,z}}}));
}
if(s.h_pipe) pipe(s,25,route({{0,{1.69f,.17f,-.39f}},{1,{1.69f,.17f,.39f}}}));
}
for(int bank=0;bank<(v8?2:1);++bank) {
float z=v8?(bank==0?-.39f:.39f):.26f;
if(!v8) box({.68f,.17f,z},{.12f,.10f,.12f},pressure_color(bank==0?s.exhaust_pressure:s.second_bank_pressure));
if(s.silencers) {
const auto& c=s.exhaust_config;
const float total=c.inlet_length+c.resonator.length+c.connecting_length+c.muffler.length+c.outlet_length;
const float start=.74f;
pipe(s,1+2*n,route({{0,{start,.17f,z}},{1,{start+total,.17f,z}}}));
float x=start+c.inlet_length;
for(int index=0;index<2;++index) {
if(index) x+=c.connecting_length;
const auto& silencer=index?c.muffler:c.resonator;
rod({x,.17f,z},{x+silencer.length,.17f,z},silencer.diameter()*.505f,{.45f,.45f,.45f});
x+=silencer.length;
}
} else if(v8) {
pipe(s,21+bank,route({{0,{1.14f,.17f,z}},{1,{1.69f,.17f,z}}}));
pipe(s,23+bank,route({{0,{1.69f,.17f,z}},{1,{2.94f,.17f,z}}}));
} else pipe(s,1+2*n,route({{0,{.74f,.17f,z}},{1,{1.45f,.17f,z}}}));
if(bank==0)rod({first-.07f,.1f,0},{first+(per_bank-1)*spacing+.07f,.1f,0},.022f,{.56f,.56f,.56f});
}
}
}
};
EngineRenderer3D::EngineRenderer3D():impl_(std::make_unique<Impl>()){}
EngineRenderer3D::~EngineRenderer3D()=default;
void EngineRenderer3D::draw(sf::RenderWindow& window,const ScenarioSnapshot& state,sf::FloatRect viewport,const OrbitCamera& camera) {
window.pushGLStates();
const auto size=window.getSize();
const int x=int(viewport.position.x),y=int(float(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);glDepthFunc(GL_LEQUAL);glDisable(GL_BLEND);glDisable(GL_TEXTURE_2D);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=.03,top=near*.41421356;
glFrustum(-top*w/h,top*w/h,-top,top,near,50);
glMatrixMode(GL_MODELVIEW);glLoadIdentity();
const GLfloat light[]{-.4f,1,1,0},ambient[]{.35f,.35f,.35f,1},diffuse[]{1,1,1,1};
glLightfv(GL_LIGHT0,GL_POSITION,light);glLightfv(GL_LIGHT0,GL_DIFFUSE,diffuse);glLightModelfv(GL_LIGHT_MODEL_AMBIENT,ambient);
glTranslatef(0,0,-camera.distance*(state.silencers?1.8f:state.cylinder_count==8?1.4f:1.f));glRotatef(camera.pitch,1,0,0);glRotatef(camera.yaw,0,1,0);glTranslatef(-camera.target.x-(state.silencers?1.1f:state.cylinder_count==8?.7f:0.f),-camera.target.y,-camera.target.z);
// Grid is indexed triangles too; no per-frame geometry allocation.
for(int i=-8;i<=8;++i) {
float p=i*.2f;
impl_->box({p,-.025f,0},{.0015f,.001f,3.2f},{.16f,.16f,.16f});
impl_->box({0,-.025f,p},{3.2f,.001f,.0015f},{.16f,.16f,.16f});
}
impl_->engine(state);
glDisable(GL_SCISSOR_TEST);window.popGLStates();
}
}

227
src/engine_schematic.cpp Normal file
View File

@@ -0,0 +1,227 @@
#include "FluidSim/Desktop/EngineSchematic.hpp"
#include "FluidSim/Desktop/PressurePalette.hpp"
#include <cmath>
#include <algorithm>
#include <iomanip>
#include <sstream>
namespace fluidsim::port::desktop {
sf::Color pressure_color(float pressure) {
auto c=pressure_tint(pressure);
return {static_cast<std::uint8_t>(255*c.x),static_cast<std::uint8_t>(255*c.y),static_cast<std::uint8_t>(255*c.z)};
}
void draw_label(sf::RenderWindow& window, const sf::Font* font, const std::string& text,
sf::Vector2f position, sf::Color color = sf::Color::White, unsigned int size = 14) {
if (!font) return;
sf::Text label(*font);
label.setString(text);
label.setPosition(position);
label.setFillColor(color);
label.setCharacterSize(size);
window.draw(label);
}
void draw_volume(sf::RenderWindow& window, float pressure, sf::FloatRect bounds) {
sf::RectangleShape body({bounds.size.x, bounds.size.y});
body.setPosition(bounds.position);
body.setFillColor(pressure_color(pressure));
body.setOutlineColor(sf::Color::White);
body.setOutlineThickness(1.0f);
window.draw(body);
}
void draw_pipe(sf::RenderWindow& window, const ScenarioSnapshot& snapshot, std::size_t pipe_index,
float center_y, float x0, float x1) {
const auto& cells = snapshot.pipe_states[pipe_index];
if (cells.empty()) return;
const auto& areas = snapshot.pipe_areas[pipe_index];
const auto& lengths = snapshot.pipe_lengths[pipe_index];
float total_length = 0;
for (float length : lengths) total_length += length;
float x = x0;
for (std::size_t i = 0; i < cells.size(); ++i) {
const auto& cell = cells[i];
const float cell_width = total_length > 0 ? (x1 - x0) * lengths[i] / total_length
: (x1 - x0) / cells.size();
const float thickness = areas.empty() ? 8 : 600 * std::sqrt(4 * areas[i] / pi);
sf::RectangleShape segment({cell_width + 1.0f, thickness});
segment.setPosition({x, center_y - thickness * .5f});
segment.setFillColor(pressure_color(cell.pressure));
window.draw(segment);
x += cell_width;
}
}
void draw_cylinder(sf::RenderWindow& window, const sf::Font* font, float pressure,
float piston_fraction, float top, const char* name, bool intake_open, bool exhaust_open, float x = 470.0f) {
constexpr float width = 74.0f, height = 142.0f;
const float fraction = std::clamp(piston_fraction, 0.0f, 1.0f);
const float gas_height = height * fraction;
sf::RectangleShape wall({width, height});
wall.setPosition({x, top});
wall.setFillColor({52, 59, 68});
window.draw(wall);
sf::RectangleShape gas({width, gas_height});
gas.setPosition({x, top});
gas.setFillColor(pressure_color(pressure));
window.draw(gas);
sf::RectangleShape piston({width + 8.0f, 5.0f});
piston.setPosition({x - 4.0f, top + gas_height});
piston.setFillColor(sf::Color::White);
window.draw(piston);
sf::RectangleShape intake_valve({7.0f, 12.0f});
intake_valve.setPosition({x - 12.0f, top + 5.0f});
intake_valve.setFillColor(intake_open ? sf::Color::Green : sf::Color::Red);
window.draw(intake_valve);
sf::RectangleShape exhaust_valve({7.0f, 12.0f});
exhaust_valve.setPosition({x + width + 5.0f, top + 5.0f});
exhaust_valve.setFillColor(exhaust_open ? sf::Color::Green : sf::Color::Red);
window.draw(exhaust_valve);
sf::RectangleShape border({width, height});
border.setPosition({x, top});
border.setFillColor(sf::Color::Transparent);
border.setOutlineColor(sf::Color(190, 200, 210));
border.setOutlineThickness(1.0f);
window.draw(border);
draw_label(window, font, name, {x + 13.0f, top - 22.0f}, sf::Color::White, 14);
}
void draw_graph_line(sf::RenderWindow& window, sf::Vector2f start, sf::Vector2f end,
sf::Color color, float thickness = 1.5f) {
const float dx = end.x - start.x;
const float dy = end.y - start.y;
const float length = std::sqrt(dx * dx + dy * dy);
if (length <= 0.0f) return;
sf::RectangleShape line({length, thickness});
line.setPosition(start);
line.setRotation(sf::degrees(std::atan2(dy, dx) * 180.0f / pi));
line.setFillColor(color);
window.draw(line);
}
void draw_two_stroke_scene(sf::RenderWindow& window,const sf::Font* font,const ScenarioSnapshot& s) {
draw_label(window,font,"YZ125 baseline | piston-controlled ports",
{650,185},sf::Color::White,14);
draw_cylinder(window,font,s.cylinder_pressure1,s.cylinder_piston_fraction1,220,"CYLINDER",
s.valve1_intake_area>0,s.valve1_exhaust_area>0);
draw_volume(window,s.crankcase_pressure,{{455,410},{110,95}});
draw_label(window,font,"CRANKCASE",{455,385},sf::Color::White,14);
draw_pipe(window,s,0,460,30,170);
draw_volume(window,s.intake_pressure,{{180,425},{95,70}});
draw_label(window,font,"CARB / INTAKE",{180,400},sf::Color::White,14);
draw_pipe(window,s,1,460,285,445);
draw_label(window,font,s.reed_open?"REED OPEN":"REED CLOSED",{290,485},s.reed_open?sf::Color::Green:sf::Color::Red,12);
draw_pipe(window,s,2,350,315,450);
draw_graph_line(window,{510,505},{510,530},{180,190,200});
draw_graph_line(window,{510,530},{315,530},{180,190,200});
draw_graph_line(window,{315,530},{315,350},{180,190,200});
draw_graph_line(window,{450,350},{450,245},{180,190,200});
draw_graph_line(window,{450,245},{467,245},{180,190,200});
draw_label(window,font,"TRANSFER",{320,325},sf::Color::White,12);
draw_graph_line(window,{551,235},{590,235},{180,190,200});
draw_pipe(window,s,3,235,590,1230);
draw_label(window,font,s.racing_exhaust ? "HEADER / DIFFUSERS / BELLY / BAFFLE / STINGER" : "STRAIGHT EXHAUST",{670,205},sf::Color::White,13);
std::ostringstream pressures;
pressures<<std::fixed<<std::setprecision(1)<<"Cylinder "<<s.cylinder_pressure1/1000<<" kPa Crankcase "
<<s.crankcase_pressure/1000<<" kPa Intake "<<s.intake_pressure/1000<<" kPa Carb AFR "
<<s.target_afr<<":1";
draw_label(window,font,pressures.str(),{22,580},sf::Color::White,17);
draw_label(window,font,"54 x 54.5 mm | 8.2:1 trapped compression | estimated pipe/ports/CDI | fixed-open power valve",
{22,625},{180,190,200},11);
}
void draw_twin_scene(sf::RenderWindow& window, const sf::Font* font, const ScenarioSnapshot& state) {
constexpr float intake_y = 310.0f;
constexpr float upper_y = 152.0f;
constexpr float lower_y = 400.0f;
// Intake: ambient pipe → throttle → plenum → two cylinder runners.
draw_pipe(window, state, 0, intake_y, 36.f, 165.0f);
sf::RectangleShape throttle({8.0f, 28.0f});
throttle.setPosition({169.0f, intake_y - 14.0f});
throttle.setFillColor(sf::Color::Yellow);
window.draw(throttle);
draw_volume(window, state.intake_pressure, {{195.0f, intake_y - 45.0f}, {62.0f, 90.0f}});
draw_pipe(window, state, 1, upper_y + 71.0f, 263.0f, 458.0f);
draw_pipe(window, state, 2, lower_y + 71.0f, 263.0f, 458.0f);
draw_cylinder(window, font, state.cylinder_pressure1, state.cylinder_piston_fraction1,
upper_y, "CYL 1", state.valve1_intake_area > 1e-7f, state.valve1_exhaust_area > 1e-7f);
draw_cylinder(window, font, state.cylinder_pressure2, state.cylinder_piston_fraction2,
lower_y, "CYL 2", state.valve2_intake_area > 1e-7f, state.valve2_exhaust_area > 1e-7f);
// Two independent exhaust headers enter a shared collector and tailpipe.
draw_pipe(window, state, 3, upper_y + 71.0f, 556.0f, 820.0f);
draw_pipe(window, state, 4, lower_y + 71.0f, 556.0f, 820.0f);
draw_volume(window, state.exhaust_pressure, {{838.0f, 305.0f}, {70.0f, 82.0f}});
draw_pipe(window, state, 5, 346.0f, 915.0f, 1236.f);
draw_label(window, font, "AMBIENT", {37.0f, intake_y + 22.0f}, {180, 190, 200}, 12);
draw_label(window, font, "THROTTLE", {151.0f, intake_y + 24.0f}, {240, 210, 90}, 11);
draw_label(window, font, "INTAKE PLENUM", {188.0f, intake_y - 62.0f}, sf::Color::White, 11);
draw_label(window, font, "SHARED EXHAUST", {818.0f, 285.0f}, sf::Color::White, 11);
draw_label(window, font, "TAILPIPE", {1084.0f, 362.0f}, {180, 190, 200}, 11);
}
void draw_b205_scene(sf::RenderWindow& window, const sf::Font* font, const ScenarioSnapshot& s) {
draw_label(window, font, "SAAB B205 | naturally aspirated baseline",
{260, 181}, sf::Color::White, 16);
draw_pipe(window, s, 0, 215, 25, 110);
draw_volume(window, s.intake_pressure, {{120, 195}, {65, 65}});
draw_label(window, font, "PLENUM", {120, 270}, sf::Color::White, 12);
draw_graph_line(window, {190, 211}, {820, 211}, {80, 145, 200});
draw_graph_line(window, {220, 420}, {850, 420}, {200, 120, 80});
for (int i = 0; i < 4; ++i) {
const float x = 230 + i*150;
const std::string title = "CYL " + std::to_string(i+1);
draw_cylinder(window, font, s.cylinder_pressures[i], s.piston_fractions[i], 255, title.c_str(),
s.intake_areas[i] > 1e-7f, s.exhaust_areas[i] > 1e-7f, x);
draw_pipe(window, s, 1+i, 235, x-15, x+75);
draw_pipe(window, s, 5+i, 410, x-15, x+75);
draw_graph_line(window, {x-15, 211}, {x-15, 235}, {80,145,200});
draw_graph_line(window, {x+75, 410}, {x+75, 420}, {200,120,80});
draw_label(window, font, std::to_string(int(s.cylinder_pressures[i]/1000))+" kPa",
{x, 445}, {180,190,205}, 12);
}
draw_volume(window, s.exhaust_pressure, {{850, 393}, {65, 48}});
draw_pipe(window, s, 9, 420, 920, 1240);
draw_label(window, font, "COLLECTOR / OPEN EXHAUST", {930, 375}, sf::Color::White, 12);
draw_label(window, font, "9-5 wagon baseline: 1675 kg with driver, five-speed manual, 4.05 final drive",
{25, 550}, sf::Color::White, 14);
draw_label(window, font, "Engine dimensions/firing from Saab WIS. Manifolds, cams, losses and ECU are estimates.",
{25, 580}, {180,190,205}, 12);
}
void draw_schematic(sf::RenderWindow& window, const sf::Font* font, const ScenarioSnapshot& state) {
if (state.cylinder_count == 8) {
draw_label(window, font, "American 5.0 V8 | EFI / ECU | 7000 RPM | heavy flywheel", {25,185}, sf::Color::White, 16);
draw_pipe(window, state, 0, 205, 25, 115);
draw_volume(window, state.intake_pressure, {{120,200},{60,25}});
draw_label(window, font, "ONE THROTTLE / SHARED PLENUM", {850,225}, sf::Color::White, 12);
for (int i = 0; i < 8; ++i) {
const float x = 70 + (i%4)*185;
const float top = i < 4 ? 230.f : 455.f;
const std::string name = "CYL " + std::to_string(i+1);
draw_cylinder(window, font, state.cylinder_pressures[i], state.piston_fractions[i], top,
name.c_str(), state.intake_areas[i]>1e-7f, state.exhaust_areas[i]>1e-7f, x);
draw_pipe(window, state, 9+i, top+150, x-10, x+90);
draw_pipe(window, state, 1+i, top+125, x-10, x+90);
draw_graph_line(window, {x+90,top+150}, {830,top+150}, {200,120,80});
}
draw_graph_line(window, {830,380}, {850,300}, {200,120,80});
draw_graph_line(window, {830,605}, {850,375}, {200,120,80});
draw_pipe(window, state, 17, 300, 850, 1250);
draw_pipe(window, state, 18, 375, 850, 1250);
draw_label(window, font, "SEPARATE BANK COLLECTORS / DUAL TAILS", {850,410}, sf::Color::White, 11);
draw_label(window, font, "EFI AFR " + std::to_string(state.target_afr), {850,430}, sf::Color::White, 13);
draw_label(window, font, "302-class | shared 4 L intake, 12 cm runners | 7:1 compression | 750 rpm idle", {25,690}, {180,190,205}, 12);
}
else if (state.two_stroke) draw_two_stroke_scene(window, font, state);
else if (state.cylinder_count == 4) draw_b205_scene(window, font, state);
else draw_twin_scene(window, font, state);
}
}

View File

@@ -11,7 +11,7 @@ HelmholtzScenario::HelmholtzScenario(int sample_rate)
pipes_.emplace_back(neck_area_, 0.050f, 20); pipes_.emplace_back(neck_area_, 0.050f, 20);
cavity_port_ = &cavity_.create_port(); cavity_port_ = &cavity_.create_port();
boundaries_.add_orifice(*cavity_port_, 0, true, cavity_orifice_area_, 0.9f); boundaries_.add_orifice(*cavity_port_, 0, true, cavity_orifice_area_, 0.9f);
boundaries_.add_open_end(0, false, 101325.0f, neck_area_); boundaries_.add_radiating_open_end(0, false, 101325.0f, neck_area_);
solver_.minimum_substeps = 8; solver_.minimum_substeps = 8;
solver_.add_component(cavity_); solver_.add_component(cavity_);
} }

View File

@@ -16,18 +16,15 @@ int main(int argc, char** argv) {
float throttle = 0.35f; float throttle = 0.35f;
std::string scenario_name = "twin"; std::string scenario_name = "twin";
TwoStrokeConfig two_stroke_config; TwoStrokeConfig two_stroke_config;
bool turbo_enabled = false; PipeSolver pipe_solver=PipeSolver::Rusanov;
std::string turbo_name;
for (int i = 1; i + 1 < argc; ++i) { for (int i = 1; i + 1 < argc; ++i) {
const std::string option = argv[i]; const std::string option = argv[i];
if (option == "--seconds") duration_seconds = std::max(0.0f, std::strtof(argv[++i], nullptr)); if (option == "--seconds") duration_seconds = std::max(0.0f, std::strtof(argv[++i], nullptr));
else if (option == "--throttle") throttle = std::clamp(std::strtof(argv[++i], nullptr), 0.0f, 1.0f); else if (option == "--throttle") throttle = std::clamp(std::strtof(argv[++i], nullptr), 0.0f, 1.0f);
else if (option == "--scenario") scenario_name = argv[++i]; else if (option == "--scenario") scenario_name = argv[++i];
else if (option == "--turbo") { else if (option == "--solver") {
const std::string value = argv[++i]; try { pipe_solver=parse_pipe_solver(argv[++i]); }
if (value != "none" && value != "vz21" && value != "gt17") { std::cerr << "Choose none, vz21 or gt17 turbo\n"; return 2; } catch(const std::exception& e) { std::cerr<<e.what()<<"\n"; return 2; }
turbo_name = value;
turbo_enabled = value != "none";
} }
else if (option == "--exhaust") { else if (option == "--exhaust") {
const std::string value = argv[++i]; const std::string value = argv[++i];
@@ -37,17 +34,12 @@ int main(int argc, char** argv) {
} }
} }
if (turbo_name.empty() && scenario_name == "b205") turbo_enabled = true;
if ((!turbo_name.empty() && turbo_name != "none" &&
!((turbo_name == "gt17" && scenario_name == "b205") || (turbo_name == "vz21" && scenario_name == "twin"))) ||
(turbo_enabled && scenario_name != "twin" && scenario_name != "b205")) {
std::cerr << "Use gt17 with b205 or vz21 with twin\n"; return 2;
}
constexpr int sample_rate = 44100; constexpr int sample_rate = 44100;
const auto total_samples = static_cast<std::uint64_t>(duration_seconds * sample_rate); const auto total_samples = static_cast<std::uint64_t>(duration_seconds * sample_rate);
const auto report_interval = static_cast<std::uint64_t>(sample_rate / 10); const auto report_interval = static_cast<std::uint64_t>(sample_rate / 10);
if (scenario_name == "twin" || scenario_name == "b205" || scenario_name == "v8") { if (scenario_name == "twin" || scenario_name == "b205" || scenario_name == "v8") {
FourStrokeScenario scenario(sample_rate, scenario_name == "v8" ? FourStrokePreset::AmericanV8 : scenario_name == "b205" ? FourStrokePreset::SaabB205 : FourStrokePreset::Twin, turbo_enabled); FourStrokeScenario scenario(sample_rate, scenario_name == "v8" ? FourStrokePreset::AmericanV8 : scenario_name == "b205" ? FourStrokePreset::SaabB205 : FourStrokePreset::Twin);
scenario.set_pipe_solver(pipe_solver);
scenario.throttle = throttle; scenario.throttle = throttle;
std::cout << (scenario_name == "v8" ? "FluidSimCpp | American 5.0 V8 | throttle " : scenario_name == "b205" ? "FluidSimCpp | Saab B205 / 9-5 wagon | throttle " : "FluidSimCpp | 600 cc parallel twin | throttle ") << std::fixed << std::setprecision(0) std::cout << (scenario_name == "v8" ? "FluidSimCpp | American 5.0 V8 | throttle " : scenario_name == "b205" ? "FluidSimCpp | Saab B205 / 9-5 wagon | throttle " : "FluidSimCpp | 600 cc parallel twin | throttle ") << std::fixed << std::setprecision(0)
<< throttle * 100.0f << "%\n"; << throttle * 100.0f << "%\n";
@@ -63,17 +55,12 @@ int main(int argc, char** argv) {
<< scenario.cylinder2.pressure / 1000.0f << " kPa" << scenario.cylinder2.pressure / 1000.0f << " kPa"
<< " intake=" << scenario.intake_plenum.pressure() / 1000.0f << " kPa" << " intake=" << scenario.intake_plenum.pressure() / 1000.0f << " kPa"
<< " exhaust=" << scenario.exhaust_collector.pressure() / 1000.0f << " kPa"; << " exhaust=" << scenario.exhaust_collector.pressure() / 1000.0f << " kPa";
if (scenario.turbo) {
const auto turbo = scenario.turbo->snapshot();
std::cout << " turbo_rpm=" << turbo.rpm << " CPR=" << turbo.compressor_pressure_ratio
<< " turbine_in=" << turbo.turbine_inlet_pressure/1000 << " kPa"
<< " charge_T=" << turbo.charge_temperature << " K";
}
std::cout << '\n'; std::cout << '\n';
} }
} }
} else if (scenario_name == "two-stroke") { } else if (scenario_name == "two-stroke") {
TwoStrokeScenario scenario(sample_rate, two_stroke_config); TwoStrokeScenario scenario(sample_rate, two_stroke_config);
scenario.set_pipe_solver(pipe_solver);
scenario.set_throttle(throttle); scenario.set_throttle(throttle);
std::cout << "FluidSimCpp | 125 cc crankcase-scavenged two-stroke | throttle " std::cout << "FluidSimCpp | 125 cc crankcase-scavenged two-stroke | throttle "
<< std::fixed << std::setprecision(0) << throttle * 100.0f << "%\n"; << std::fixed << std::setprecision(0) << throttle * 100.0f << "%\n";

View File

@@ -1,132 +0,0 @@
#include "FluidSim/MechanicalSoundProcessor.hpp"
#include <algorithm>
#include <cmath>
namespace fluidsim::port {
namespace {
constexpr float two_pi = 2.0f * pi;
constexpr float closed_threshold = 1e-7f;
}
MechanicalSoundProcessor::Resonator::Resonator(float frequency_hz, float q, float sample_rate) {
const float omega = two_pi * frequency_hz / sample_rate;
const float radius = std::exp(-omega / (2.0f * q));
b1_ = 2.0f * radius * std::cos(omega);
b2_ = -radius * radius;
gain_ = std::sin(omega);
}
float MechanicalSoundProcessor::Resonator::process(float input) {
const float output = input * gain_ + b1_ * y1_ + b2_ * y2_;
y2_ = y1_;
y1_ = output;
return output;
}
MechanicalSoundProcessor::OnePoleLowPass::OnePoleLowPass(float cutoff_hz, float sample_rate) {
alpha_ = 1.0f - std::exp(-(two_pi * cutoff_hz / sample_rate));
}
float MechanicalSoundProcessor::OnePoleLowPass::process(float input) {
state_ += alpha_ * (input - state_);
return state_;
}
MechanicalSoundProcessor::MechanicalSoundProcessor(int sample_rate)
: dt_(1.0f / static_cast<float>(std::max(sample_rate, 1))),
intake_tick_(4200.0f, 20.0f, static_cast<float>(std::max(sample_rate, 1))),
exhaust_tick_(3400.0f, 20.0f, static_cast<float>(std::max(sample_rate, 1))),
slap_filter_(450.0f, static_cast<float>(std::max(sample_rate, 1))),
combustion_filter_(600.0f, static_cast<float>(std::max(sample_rate, 1))),
chain_filter_(1500.0f, static_cast<float>(std::max(sample_rate, 1))) {
slap_decay_ = std::exp(-dt_ / 0.018f);
combustion_decay_ = std::exp(-dt_ / 0.025f);
chain_decay_ = std::exp(-dt_ / 0.005f);
}
float MechanicalSoundProcessor::process(const Crankshaft& crank,
const EngineCylinder& cylinder, float load) {
const float rpm = crank.omega * 60.0f / two_pi;
const float rpm_norm = std::clamp(std::abs(rpm) / 7000.0f, 0.0f, 1.5f);
const float normalized_load = std::clamp(load, 0.0f, 1.0f);
const float degrees = std::fmod((crank.angle + cylinder.phase) * 180.0f / pi, 720.0f);
const float intake_area = cylinder.valve_area(degrees, true);
const float exhaust_area = cylinder.valve_area(degrees, false);
const float pressure = cylinder.pressure;
const float pressure_rate = (pressure - previous_pressure_) / dt_;
previous_pressure_ = pressure;
float cycle_position = (crank.angle + cylinder.phase) / (4.0f * pi);
cycle_position -= std::floor(cycle_position);
const float previous_cycle = previous_cycle_position_ < 0.0f ? cycle_position : previous_cycle_position_;
float crank_fraction = crank.angle / two_pi;
crank_fraction -= std::floor(crank_fraction);
const float previous_crank = previous_crank_fraction_ < 0.0f ? crank_fraction : previous_crank_fraction_;
float sample = 0.0f;
float intake_impulse = 0.0f, exhaust_impulse = 0.0f;
const bool intake_opened = previous_intake_area_ <= closed_threshold && intake_area > closed_threshold;
const bool intake_closed = previous_intake_area_ > closed_threshold && intake_area <= closed_threshold;
const bool exhaust_opened = previous_exhaust_area_ <= closed_threshold && exhaust_area > closed_threshold;
const bool exhaust_closed = previous_exhaust_area_ > closed_threshold && exhaust_area <= closed_threshold;
const float open_amplitude = 0.20f + 0.30f * rpm_norm;
const float close_amplitude = 0.45f + 0.85f * rpm_norm;
if (intake_opened) intake_impulse += random_sample() * open_amplitude;
if (intake_closed) intake_impulse += random_sample() * close_amplitude * 1.15f;
if (exhaust_opened) exhaust_impulse += random_sample() * open_amplitude * 1.10f;
if (exhaust_closed) exhaust_impulse += random_sample() * close_amplitude * 1.30f;
sample += intake_tick_.process(intake_impulse) * valve_tick_gain;
sample += exhaust_tick_.process(exhaust_impulse) * valve_tick_gain;
if (previous_cycle_position_ >= 0.0f) {
float amplitude = 0.0f;
if (crossed_fraction(previous_cycle, cycle_position, 0.0f))
amplitude = (0.35f + 0.65f * rpm_norm) * (0.4f + 0.6f * normalized_load);
else if (crossed_fraction(previous_cycle, cycle_position, 0.5f))
amplitude = (0.10f + 0.20f * rpm_norm) * 0.7f;
else if (crossed_fraction(previous_cycle, cycle_position, 0.25f) ||
crossed_fraction(previous_cycle, cycle_position, 0.75f))
amplitude = (0.08f + 0.18f * rpm_norm) * 0.8f;
slap_envelope_ = std::max(slap_envelope_, amplitude);
}
sample += slap_filter_.process(random_sample() * slap_envelope_) * piston_slap_gain;
slap_envelope_ *= slap_decay_;
const bool power_stroke = cycle_position < 0.25f || cycle_position > 0.95f;
if (power_stroke && pressure_rate > 2e5f) {
const float drive = std::clamp(pressure_rate * 1.2e-7f, 0.0f, 3.0f);
combustion_envelope_ = std::max(combustion_envelope_, drive * (0.3f + 0.7f * normalized_load));
}
sample += combustion_filter_.process(random_sample() * combustion_envelope_) * combustion_gain;
combustion_envelope_ *= combustion_decay_;
if (previous_crank_fraction_ >= 0.0f &&
(crossed_fraction(previous_crank, crank_fraction, 0.0f) ||
crossed_fraction(previous_crank, crank_fraction, 0.5f)))
chain_envelope_ += 0.15f + 0.25f * rpm_norm;
sample += chain_filter_.process(random_sample() * chain_envelope_) * cam_chain_gain;
chain_envelope_ *= chain_decay_;
const float crank_hz = std::abs(rpm) / 60.0f;
gear_phase1_ = std::fmod(gear_phase1_ + two_pi * crank_hz * 15.0f * dt_, two_pi);
gear_phase2_ = std::fmod(gear_phase2_ + two_pi * crank_hz * 30.2f * dt_, two_pi);
gear_phase3_ = std::fmod(gear_phase3_ + two_pi * crank_hz * 45.4f * dt_, two_pi);
const float gear_tone = std::sin(gear_phase1_) * 0.5f +
std::sin(gear_phase2_) * 0.3f + std::sin(gear_phase3_) * 0.2f;
sample += gear_tone * gear_whine_gain * rpm_norm;
previous_intake_area_ = intake_area;
previous_exhaust_area_ = exhaust_area;
previous_cycle_position_ = cycle_position;
previous_crank_fraction_ = crank_fraction;
return std::tanh(sample * master_gain);
}
float MechanicalSoundProcessor::random_sample() { return unit_(random_); }
bool MechanicalSoundProcessor::crossed_fraction(float previous, float current, float target) {
if (current == previous) return false;
return current > previous ? previous < target && target <= current
: previous < target || target <= current;
}
} // namespace fluidsim::port

View File

@@ -22,6 +22,8 @@ const char *scene_name(OutdoorScene scene) {
return "Nearby wall"; return "Nearby wall";
case OutdoorScene::Woodland: case OutdoorScene::Woodland:
return "Woodland"; return "Woodland";
case OutdoorScene::Tunnel:
return "Tunnel";
} }
return "Unknown"; return "Unknown";
} }
@@ -35,7 +37,7 @@ float OutdoorAudio::LowPass::process(float x) {
z2 = 0; z2 = 0;
return y; return y;
} }
OutdoorAudio::OutdoorAudio(int rate, OutdoorAudioConfig config) : rate_(rate), config_(config) { OutdoorAudio::OutdoorAudio(int rate, OutdoorAudioConfig config) : rate_(rate), config_(config), late_reverb_(rate) {
if (rate < 8000 || !std::isfinite(config.sound_speed) || config.sound_speed <= 0 || if (rate < 8000 || !std::isfinite(config.sound_speed) || config.sound_speed <= 0 ||
!std::isfinite(config.monitor_cutoff_hz) || config.monitor_cutoff_hz <= 0 || !std::isfinite(config.monitor_cutoff_hz) || config.monitor_cutoff_hz <= 0 ||
!std::isfinite(config.pressure_gain) || config.pressure_gain < 0) !std::isfinite(config.pressure_gain) || config.pressure_gain < 0)
@@ -48,14 +50,11 @@ OutdoorAudio::OutdoorAudio(int rate, OutdoorAudioConfig config) : rate_(rate), c
configure_emitter(emitters_[0], config_.intake_position); configure_emitter(emitters_[0], config_.intake_position);
configure_emitter(emitters_[1], config_.exhaust_position); configure_emitter(emitters_[1], config_.exhaust_position);
if (config_.dual_exhaust) configure_emitter(emitters_[2], config_.second_exhaust_position); if (config_.dual_exhaust) configure_emitter(emitters_[2], config_.second_exhaust_position);
constexpr std::array<int, 4> lengths{1499, 2111, 2633, 3251}; late_reverb_.dry_mix = 0;
for (std::size_t i = 0; i < tail_.size(); ++i) { late_reverb_.early_mix = 0;
auto &line = tail_[i]; late_reverb_.tail_mix = 1;
line.buffer.resize(std::max(1, int(std::lround(lengths[i] * rate_ / 44100.0)))); late_reverb_.set_decay_times(.75f, .45f, .22f);
line.gain = std::pow(10.0f, -3.0f * (float(line.buffer.size()) / rate_) / 0.35f);
}
scene_alpha_ = 1 - std::exp(-1.0f / (rate_ * .02f)); scene_alpha_ = 1 - std::exp(-1.0f / (rate_ * .02f));
tail_alpha_ = 1 - std::exp(-2 * pi * 2500 / rate_);
} }
void OutdoorAudio::configure_emitter(Emitter &emitter, Vec3 source) { void OutdoorAudio::configure_emitter(Emitter &emitter, Vec3 source) {
emitter.filtered.resize(std::bit_ceil(std::size_t(rate_))); emitter.filtered.resize(std::bit_ceil(std::size_t(rate_)));
@@ -78,8 +77,14 @@ void OutdoorAudio::configure_emitter(Emitter &emitter, Vec3 source) {
p.delay_right = std::size_t(sr); p.delay_right = std::size_t(sr);
p.fraction_left = sl - p.delay_left; p.fraction_left = sl - p.delay_left;
p.fraction_right = sr - p.delay_right; p.fraction_right = sr - p.delay_right;
p.gain_left = coefficient / (dl * source_distance); // A scatterer adds path length before the listener leg. Use spreading
p.gain_right = coefficient / (dr * source_distance); // over the complete reflected path rather than multiplying two
// inverse-distance losses, which made the woodland paths nearly
// inaudible.
const float path_left = source_distance > 1.0f ? dl + source_distance : dl;
const float path_right = source_distance > 1.0f ? dr + source_distance : dr;
p.gain_left = coefficient / path_left;
p.gain_right = coefficient / path_right;
p.alpha = cutoff > 0 ? 1 - std::exp(-2 * pi * cutoff / rate_) : 1; p.alpha = cutoff > 0 ? 1 - std::exp(-2 * pi * cutoff / rate_) : 1;
emitter.paths.push_back(p); emitter.paths.push_back(p);
}; };
@@ -92,6 +97,21 @@ void OutdoorAudio::configure_emitter(Emitter &emitter, Vec3 source) {
const float first = std::max(distance(source, tree), .1f); const float first = std::max(distance(source, tree), .1f);
path(tree, .35f, 3, 2400, first, first); path(tree, .35f, 3, 2400, first, first);
} }
// Image sources for an approximate 6 m wide, 4 m high hard-surfaced
// tunnel. Single and combined ceiling/side-wall bounces make the profile
// distinct before the diffuse FDN tail builds up.
constexpr float tunnel_width = 6.0f;
constexpr float tunnel_height = 4.0f;
const Vec3 ceiling{source.x, 2 * tunnel_height - source.y, source.z};
const Vec3 left_wall{source.x, source.y, -tunnel_width - source.z};
const Vec3 right_wall{source.x, source.y, tunnel_width - source.z};
path(ceiling, .82f, 4, 7200);
path(left_wall, .82f, 4, 6200);
path(right_wall, .82f, 4, 6200);
path({ceiling.x, ceiling.y, left_wall.z}, .68f, 4, 5600);
path({ceiling.x, ceiling.y, right_wall.z}, .68f, 4, 5600);
path({source.x, source.y, -2 * tunnel_width + source.z}, .56f, 4, 4800);
path({source.x, source.y, 2 * tunnel_width + source.z}, .56f, 4, 4800);
const float cutoff = std::min(config_.monitor_cutoff_hz, rate_ * .45f); const float cutoff = std::min(config_.monitor_cutoff_hz, rate_ * .45f);
const float w = 2 * pi * cutoff / rate_, cs = std::cos(w), alpha = std::sin(w) / std::sqrt(2.0f), const float w = 2 * pi * cutoff / rate_, cs = std::cos(w), alpha = std::sin(w) / std::sqrt(2.0f),
a0 = 1 + alpha; a0 = 1 + alpha;
@@ -101,9 +121,8 @@ void OutdoorAudio::configure_emitter(Emitter &emitter, Vec3 source) {
AudioFrame OutdoorAudio::render(Emitter &e, float flow, AudioFrame &dry, AudioFrame &raw) { AudioFrame OutdoorAudio::render(Emitter &e, float flow, AudioFrame &dry, AudioFrame &raw) {
const float pressure = (flow - e.previous_flow) * rate_ * config_.pressure_gain / (4 * pi); const float pressure = (flow - e.previous_flow) * rate_ * config_.pressure_gain / (4 * pi);
e.previous_flow = flow; e.previous_flow = flow;
const float filtered = e.filter.process(pressure);
e.raw[e.position] = pressure; e.raw[e.position] = pressure;
e.filtered[e.position] = monitor_filter_ ? filtered : pressure; e.filtered[e.position] = monitor_filter_ ? e.filter.process(pressure) : pressure;
const auto read = [&](const std::vector<float> &data, std::size_t delay, float fraction) { const auto read = [&](const std::vector<float> &data, std::size_t delay, float fraction) {
const auto i = (e.position + data.size() - delay) & (data.size() - 1); const auto i = (e.position + data.size() - delay) & (data.size() - 1);
const auto j = (i + data.size() - 1) & (data.size() - 1); const auto j = (i + data.size() - 1) & (data.size() - 1);
@@ -130,44 +149,52 @@ AudioFrame OutdoorAudio::render(Emitter &e, float flow, AudioFrame &dry, AudioFr
e.position = (e.position + 1) & (e.filtered.size() - 1); e.position = (e.position + 1) & (e.filtered.size() - 1);
return result; return result;
} }
AudioFrame OutdoorAudio::diffuse(float input) {
std::array<float, 4> values{};
float sum = 0;
for (std::size_t i = 0; i < 4; ++i) {
auto &line = tail_[i];
values[i] = line.buffer[line.position];
sum += values[i];
}
for (std::size_t i = 0; i < 4; ++i) {
auto &line = tail_[i];
const float scattered = values[i] - .5f * sum;
line.state += tail_alpha_ * (scattered - line.state);
if (std::abs(line.state) < 1e-25f)
line.state = 0;
line.buffer[line.position] = line.gain * line.state + .5f * input;
line.position = (line.position + 1) % line.buffer.size();
}
return {(values[0] + values[2]) * .35f, (values[1] + values[3]) * .35f};
}
AudioFrame OutdoorAudio::process(float intake, float exhaust, float second_exhaust) { AudioFrame OutdoorAudio::process(float intake, float exhaust, float second_exhaust) {
if (!std::isfinite(intake) || !std::isfinite(exhaust) || !std::isfinite(second_exhaust)) if (!std::isfinite(intake) || !std::isfinite(exhaust) || !std::isfinite(second_exhaust))
throw std::runtime_error("nonfinite acoustic source flow"); throw std::runtime_error("nonfinite acoustic source flow");
const bool wet = scene_ != OutdoorScene::Dry; const OutdoorScene scene = requested_scene_.load(std::memory_order_relaxed);
const std::array<float, 4> target{1, wet ? 1.f : 0.f, scene_ == OutdoorScene::Wall ? 1.f : 0.f, if (scene != active_scene_) {
scene_ == OutdoorScene::Woodland ? 1.f : 0.f}; active_scene_ = scene;
for (std::size_t i = 0; i < 4; ++i) switch (scene) {
case OutdoorScene::Dry:
late_reverb_.set_decay_times(.20f, .12f, .06f);
break;
case OutdoorScene::Field:
late_reverb_.set_decay_times(.28f, .17f, .08f);
break;
case OutdoorScene::Wall:
late_reverb_.set_decay_times(.75f, .45f, .22f);
break;
case OutdoorScene::Woodland:
late_reverb_.set_decay_times(.42f, .22f, .09f);
break;
case OutdoorScene::Tunnel:
late_reverb_.set_decay_times(1.8f, 1.35f, .75f);
break;
}
}
const bool wet = scene != OutdoorScene::Dry;
const std::array<float, 5> target{1, wet && scene != OutdoorScene::Tunnel ? 1.f : 0.f,
scene == OutdoorScene::Wall ? 1.f : 0.f,
scene == OutdoorScene::Woodland ? 1.f : 0.f,
scene == OutdoorScene::Tunnel ? 1.f : 0.f};
for (std::size_t i = 0; i < weights_.size(); ++i)
weights_[i] += (target[i] - weights_[i]) * scene_alpha_; weights_[i] += (target[i] - weights_[i]) * scene_alpha_;
const float target_tail = // Keep the diffuse tail present between sharp events; the prior mix values
scene_ == OutdoorScene::Wall // made it perceptible mainly after large transients.
? .14f const float target_tail = scene == OutdoorScene::Wall ? .75f
: (scene_ == OutdoorScene::Woodland ? .25f : (scene_ == OutdoorScene::Field ? .025f : 0)); : scene == OutdoorScene::Woodland ? 1.0f
: scene == OutdoorScene::Tunnel ? 1.15f
: scene == OutdoorScene::Field ? .30f : 0.f;
tail_mix_ += (target_tail - tail_mix_) * scene_alpha_; tail_mix_ += (target_tail - tail_mix_) * scene_alpha_;
last_dry_ = {}; last_dry_ = {};
last_raw_ = {}; last_raw_ = {};
const auto a = render(emitters_[0], intake, last_dry_, last_raw_); const auto a = render(emitters_[0], intake, last_dry_, last_raw_);
const auto b = render(emitters_[1], exhaust, last_dry_, last_raw_); const auto b = render(emitters_[1], exhaust, last_dry_, last_raw_);
const auto c = config_.dual_exhaust ? render(emitters_[2], second_exhaust, last_dry_, last_raw_) : AudioFrame{}; const auto c = config_.dual_exhaust
const auto tail = diffuse((last_dry_.left + last_dry_.right) * .5f); ? render(emitters_[2], second_exhaust, last_dry_, last_raw_)
: AudioFrame{};
const auto tail = late_reverb_.process_stereo((last_dry_.left + last_dry_.right) * .5f);
return {a.left + b.left + c.left + tail.left * tail_mix_, a.right + b.right + c.right + tail.right * tail_mix_}; return {a.left + b.left + c.left + tail.left * tail_mix_, a.right + b.right + c.right + tail.right * tail_mix_};
} }
} // namespace fluidsim::port } // namespace fluidsim::port

View File

@@ -4,6 +4,7 @@
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <cmath> #include <cmath>
#include <stdexcept>
namespace fluidsim::port { namespace fluidsim::port {
@@ -38,23 +39,26 @@ OutdoorExhaustReverb::OutdoorExhaustReverb(int sample_rate)
wall_right_(delay_samples(sample_rate_, wall_delay_ms + 2.2f)), wall_right_(delay_samples(sample_rate_, wall_delay_ms + 2.2f)),
ground_filter_left_(1400.0f, sample_rate_), ground_filter_right_(1400.0f, sample_rate_), ground_filter_left_(1400.0f, sample_rate_), ground_filter_right_(1400.0f, sample_rate_),
wall_filter_left_(3800.0f, sample_rate_), wall_filter_right_(3800.0f, sample_rate_) { wall_filter_left_(3800.0f, sample_rate_), wall_filter_right_(3800.0f, sample_rate_) {
constexpr std::array<int, 8> lengths{3203, 4027, 5521, 7027, 8521, 10007, 11503, 13009}; // Distinct prime-ish delays below the 250 ms cap at 44.1 kHz. Scaling
// keeps them distinct at other sample rates instead of clipping both long
// lines to the same delay.
constexpr std::array<int, 8> lengths{3203, 4027, 5521, 7027, 8521, 10007, 10427, 10949};
const int maximum_delay = std::max(4, sample_rate_ / 4); const int maximum_delay = std::max(4, sample_rate_ / 4);
float average_delay_seconds = 0.0f; float average_delay_seconds = 0.0f;
fdn_.reserve(lengths.size()); fdn_.reserve(lengths.size());
low_filters_.reserve(lengths.size()); low_filters_.reserve(lengths.size());
mid_filters_.reserve(lengths.size()); mid_filters_.reserve(lengths.size());
for (const int requested : lengths) { for (const int requested : lengths) {
const int size = std::min(requested, maximum_delay); const int scaled = std::max(1, int(std::lround(requested * double(sample_rate_) / 44100.0)));
const int size = std::min(scaled, maximum_delay);
fdn_.emplace_back(static_cast<std::size_t>(size)); fdn_.emplace_back(static_cast<std::size_t>(size));
low_filters_.emplace_back(band_low_hz, sample_rate_); low_filters_.emplace_back(band_low_hz, sample_rate_);
mid_filters_.emplace_back(band_high_hz, sample_rate_); mid_filters_.emplace_back(band_high_hz, sample_rate_);
average_delay_seconds += static_cast<float>(size) / static_cast<float>(sample_rate_); average_delay_seconds += static_cast<float>(size) / static_cast<float>(sample_rate_);
} }
average_delay_seconds /= static_cast<float>(lengths.size()); average_delay_seconds /= static_cast<float>(lengths.size());
gain_low_ = std::pow(10.0f, -3.0f * average_delay_seconds / low_decay_seconds); average_delay_seconds_ = average_delay_seconds;
gain_mid_ = std::pow(10.0f, -3.0f * average_delay_seconds / mid_decay_seconds); update_feedback_gains();
gain_high_ = std::pow(10.0f, -3.0f * average_delay_seconds / high_decay_seconds);
read_buffer_.resize(lengths.size()); read_buffer_.resize(lengths.size());
mix_buffer_.resize(lengths.size()); mix_buffer_.resize(lengths.size());
@@ -65,6 +69,22 @@ OutdoorExhaustReverb::OutdoorExhaustReverb(int sample_rate)
bloom_decay_ = std::exp(-dt_ / (transient_decay_ms * 0.001f)); bloom_decay_ = std::exp(-dt_ / (transient_decay_ms * 0.001f));
} }
void OutdoorExhaustReverb::set_decay_times(float low_seconds, float mid_seconds, float high_seconds) {
if (!std::isfinite(low_seconds) || !std::isfinite(mid_seconds) || !std::isfinite(high_seconds) ||
low_seconds <= 0 || mid_seconds <= 0 || high_seconds <= 0)
throw std::invalid_argument("reverb decay times must be finite and positive");
low_decay_seconds = low_seconds;
mid_decay_seconds = mid_seconds;
high_decay_seconds = high_seconds;
update_feedback_gains();
}
void OutdoorExhaustReverb::update_feedback_gains() {
gain_low_ = std::pow(10.0f, -3.0f * average_delay_seconds_ / low_decay_seconds);
gain_mid_ = std::pow(10.0f, -3.0f * average_delay_seconds_ / mid_decay_seconds);
gain_high_ = std::pow(10.0f, -3.0f * average_delay_seconds_ / high_decay_seconds);
}
AudioFrame OutdoorExhaustReverb::process_stereo(float input) { AudioFrame OutdoorExhaustReverb::process_stereo(float input) {
const float magnitude = std::abs(input); const float magnitude = std::abs(input);
fast_envelope_ += (magnitude > fast_envelope_ ? fast_attack_ : fast_release_) * (magnitude - fast_envelope_); fast_envelope_ += (magnitude > fast_envelope_ ? fast_attack_ : fast_release_) * (magnitude - fast_envelope_);

View File

@@ -22,7 +22,7 @@ PulseTestScenario::PulseTestScenario(int sample_rate)
{bubble_density, 0.0f, bubble_pressure / (gamma - 1.0f), 1.0f}; {bubble_density, 0.0f, bubble_pressure / (gamma - 1.0f), 1.0f};
boundaries_.add_closed_end(0, true); boundaries_.add_closed_end(0, true);
boundaries_.add_open_end(0, false, 101325.0f, pipe_area_); boundaries_.add_radiating_open_end(0, false, 101325.0f, pipe_area_);
solver_.minimum_substeps = 3; solver_.minimum_substeps = 3;
} }

View File

@@ -1,29 +1,32 @@
#include "FluidSim/Scenario.hpp" #include "FluidSim/Scenario.hpp"
#include "FluidSim/Constants.hpp"
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
namespace fluidsim::port { namespace fluidsim::port {
FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selected_preset, bool turbo_enabled) FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selected_preset, ExhaustConfig exhaust, V8ExhaustConfig v8_exhaust, EngineConfig engine)
: preset(selected_preset), : preset(selected_preset), exhaust_config(exhaust), v8_exhaust_config(v8_exhaust), engine_config(engine),
intake_plenum(selected_preset == FourStrokePreset::AmericanV8 ? 4e-3f : selected_preset == FourStrokePreset::SaabB205 ? 3e-3f : 1e-3f, 101325, 300), intake_plenum(engine.cylinders ? .001f*engine.cylinders : selected_preset == FourStrokePreset::AmericanV8 ? 4e-3f : selected_preset == FourStrokePreset::SaabB205 ? 3e-3f : 1e-3f, 101325, 300),
exhaust_collector(selected_preset == FourStrokePreset::AmericanV8 ? 1.5e-3f : selected_preset == FourStrokePreset::SaabB205 ? .6e-3f : .15e-3f, 101325, 700), exhaust_collector(engine.cylinders ? .00025f*engine.cylinders : selected_preset == FourStrokePreset::AmericanV8 ? 1.5e-3f : selected_preset == FourStrokePreset::SaabB205 ? .6e-3f : .15e-3f, 101325, 700),
pipes(create_pipes(selected_preset, turbo_enabled)), dt_(1.f / std::max(sample_rate, 1)), pipes(create_pipes(selected_preset, exhaust, v8_exhaust, engine)), dt_(1.f / std::max(sample_rate, 1)),
boundaries_(pipes), solver_(pipes, boundaries_), audio_(sample_rate, [&] { boundaries_(pipes), solver_(pipes, boundaries_), audio_(sample_rate, [&] {
OutdoorAudioConfig config; OutdoorAudioConfig config;
config.dual_exhaust = selected_preset == FourStrokePreset::AmericanV8; config.dual_exhaust = selected_preset == FourStrokePreset::AmericanV8;
return config; return config;
}()), mechanical_sound_(sample_rate) { }()) {
const bool saab = preset == FourStrokePreset::SaabB205; const bool saab = preset == FourStrokePreset::SaabB205;
const bool v8 = preset == FourStrokePreset::AmericanV8; const bool v8 = preset == FourStrokePreset::AmericanV8;
if (v8 && turbo_enabled) throw std::invalid_argument("American V8 preset is naturally aspirated");
const int count = cylinder_count(); const int count = cylinder_count();
if(engine.cylinders<0 || engine.cylinders>12 || (engine.layout!=EngineLayout::Inline && engine.cylinders%2)) throw std::invalid_argument("invalid engine cylinder layout");
starter_torque = v8 ? 180.f : saab ? 65.f : 18.f;
crankshaft.omega = (saab ? 1200 : 2000) * 2 * pi / 60; crankshaft.omega = (saab ? 1200 : 2000) * 2 * pi / 60;
crankshaft.inertia = saab ? .18f : .04f; crankshaft.inertia = saab ? .18f : .04f;
crankshaft.friction_constant = saab ? 12.f : 5.f; crankshaft.friction_constant = saab ? 12.f : 5.f;
crankshaft.friction_viscous = saab ? .012f : .003f; crankshaft.friction_viscous = saab ? .012f : .003f;
if (saab) { if (saab) {
solver_.minimum_substeps = 2; // Acoustic CFL may subdivide further.
// Rough 9-5 wagon with driver; FM55 B05 overall reductions from WIS. // Rough 9-5 wagon with driver; FM55 B05 overall reductions from WIS.
vehicle.mass = 1600 + 75; vehicle.mass = 1600 + 75;
vehicle.primary_reduction = 1; vehicle.primary_reduction = 1;
@@ -57,10 +60,6 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selecte
vehicle.drag_coefficient = .4f; vehicle.drag_coefficient = .4f;
vehicle.frontal_area = 2.f; vehicle.frontal_area = 2.f;
ecu.idle_rpm = 750; ecu.idle_rpm = 750;
ecu.throttle_time_constant = .065f;
ecu.throttle_rate = 2.3f;
ecu.throttle_closing_time_constant = .16f;
ecu.throttle_closing_rate = 1.4f;
ecu.idle_base_opening = .08f; ecu.idle_base_opening = .08f;
ecu.idle_spark_gain = .02f; ecu.idle_spark_gain = .02f;
ecu.ignition_map = {{0, 10}, {750, 12}, {2500, 28}, {4000, 32}, {6000, 30}}; ecu.ignition_map = {{0, 10}, {750, 12}, {2500, 28}, {4000, 32}, {6000, 30}};
@@ -68,9 +67,27 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selecte
ecu.rev_resume_rpm = 6750; ecu.rev_resume_rpm = 6750;
ecu.overrun_cut_rpm = 2000; ecu.overrun_cut_rpm = 2000;
ecu.overrun_resume_rpm = 1100; ecu.overrun_resume_rpm = 1100;
// One minimum substep; acoustic CFL still subdivides when needed. // Port-coupled fittings use one minimum substep; acoustic CFL may subdivide further.
solver_.minimum_substeps = 1; solver_.minimum_substeps = 1;
} }
if(engine.cylinders) {
solver_.minimum_substeps=1;
const float litres=count*pi*.25f*engine.bore*engine.bore*engine.stroke*1000;
crankshaft.omega=engine.idle_rpm*1.4f*2*pi/60;crankshaft.inertia=std::max(.04f,litres*.12f);
crankshaft.friction_constant=litres*4;crankshaft.friction_viscous=litres*.004f;
starter_torque=litres*25;vehicle.mass=count<=2?240:1400;vehicle.clutch_capacity=litres*220;
ecu.idle_rpm=engine.idle_rpm;ecu.rev_limit_rpm=engine.rev_limit;ecu.rev_resume_rpm=engine.rev_limit-250;
ecu.idle_spark_gain=.02f;ecu.idle_base_opening=.05f;
if(engine.carbureted) {
ecu.idle_control=Ecu::IdleControl::FixedOpening;ecu.idle_base_opening=engine.idle_opening;
ecu.overrun_fuel_cut_enabled=false;
carb_=std::make_unique<Carburetor>(.085f);
const float jet_scale=engine.carb_jet_scale*std::sqrt(litres/.5f);
carb_->pilot_jet_diameter*=jet_scale*engine.carb_pilot_scale;carb_->main_jet_diameter*=jet_scale;carb_->needle_jet_diameter*=jet_scale;
carb_->bowl_capacity=.1f;carb_->bowl_refill_rate=.1f;
intake_plenum.add_fuel_mass(intake_plenum.mass()/14.7f);
}
}
vehicle.set_neutral(); vehicle.set_neutral();
vehicle.clutch_input = clutch; vehicle.clutch_input = clutch;
const auto engines = cylinders(); const auto engines = cylinders();
@@ -78,7 +95,7 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selecte
const std::array<int, 8> firing_order{1, 5, 4, 2, 6, 3, 7, 8}; const std::array<int, 8> firing_order{1, 5, 4, 2, 6, 3, 7, 8};
for (int i = 0; i < count; ++i) { for (int i = 0; i < count; ++i) {
auto& c = *engines[i]; auto& c = *engines[i];
c.phase = phases[i]; c.phase = phases[i%4];
if (v8) { if (v8) {
const auto position = std::find(firing_order.begin(), firing_order.end(), i+1) - firing_order.begin(); const auto position = std::find(firing_order.begin(), firing_order.end(), i+1) - firing_order.begin();
c.phase = std::fmod(4*pi - float(position)*pi*.5f, 4*pi); c.phase = std::fmod(4*pi - float(position)*pi*.5f, 4*pi);
@@ -105,21 +122,28 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selecte
c.intake_valve_diameter = .045f; c.exhaust_valve_diameter = .037f; c.intake_valve_diameter = .045f; c.exhaust_valve_diameter = .037f;
c.intake_valve_lift = .010f; c.exhaust_valve_lift = .010f; c.intake_valve_lift = .010f; c.exhaust_valve_lift = .010f;
} }
if(engine.cylinders) {
c.phase=engine.custom_phases?engine.phase_degrees[i]*pi/180:std::fmod(4*pi-i*4*pi/count,4*pi);
c.bore=engine.bore;c.stroke=engine.stroke;c.rod=engine.rod;c.compression=engine.compression;
c.intake_valve_diameter=engine.bore*.38f;c.exhaust_valve_diameter=engine.bore*.32f;
c.intake_valve_lift=engine.bore*.10f;c.exhaust_valve_lift=engine.bore*.09f;
c.transported_fuel=engine.carbureted;
}
c.rev_limit_enabled = true; c.rev_limit_enabled = true;
c.rev_limit_rpm = ecu.rev_limit_rpm; c.rev_limit_rpm = ecu.rev_limit_rpm;
c.rev_limit_hysteresis = 250; c.rev_limit_hysteresis = 250;
c.rev_limit_cut_time = .020f; c.rev_limit_cut_time = .020f;
c.ecu = &ecu; c.ecu = &ecu;
c.initialize(crankshaft); c.initialize(crankshaft);
if (saab || v8) { if (saab || v8 || engine.cylinders) {
// Initialize each phased cylinder at its actual piston position. // Initialize each phased cylinder at its actual piston position.
c.volume = c.compute_volume(c.phase); c.volume = c.compute_volume(c.phase);
c.air_mass = ambient_density * c.volume; c.air_mass = ambient_density * c.volume;
c.energy = 101325 * c.volume / (gamma - 1); c.energy = 101325 * c.volume / (gamma - 1);
if(engine.carbureted) {c.fuel_mass=c.air_mass/15.7f;c.air_mass-=c.fuel_mass;}
c.update_state(0); c.update_state(0);
} }
} }
intake_boundary_area_ = pipes.front().area;
runner_area_ = pipes[1].area; runner_area_ = pipes[1].area;
header_area_ = pipes[1+count].area; header_area_ = pipes[1+count].area;
tail_area_ = pipes[1+2*count].area; tail_area_ = pipes[1+2*count].area;
@@ -128,50 +152,56 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selecte
pipe.energy_relaxation_rate = 0; pipe.energy_relaxation_rate = 0;
} }
auto& throttle_port = intake_plenum.create_port(); auto& throttle_port = intake_plenum.create_port();
solver_.add_component(intake_atmosphere); throttle_port_=&throttle_port;
solver_.add_component(intake_plenum); solver_.add_component(intake_plenum);
solver_.add_component(exhaust_collector); if(!v8) solver_.add_component(exhaust_collector);
if (v8) solver_.add_component(second_bank_collector);
if (turbo_enabled) {
turbo = std::make_unique<Turbocharger>(saab ? TurboConfig::gt17() : TurboConfig{});
ecu.boost_control_enabled = saab;
const int fresh = 2+2*count, exhaust_exit = fresh+1;
boundaries_.add_orifice(intake_atmosphere.port, fresh, true, intake_boundary_area_);
turbo->connect(boundaries_, pipes, fresh, false, 0, true, 1+2*count, false, exhaust_exit, true);
solver_.add_component(*turbo);
} else {
boundaries_.add_orifice(intake_atmosphere.port, 0, true, intake_boundary_area_);
}
boundaries_.add_orifice(throttle_port, 0, false, throttle_area_, .7f); boundaries_.add_orifice(throttle_port, 0, false, throttle_area_, .7f);
for (int i = 0; i < count; ++i) { for (int i = 0; i < count; ++i) {
auto& feed = intake_plenum.create_port(); auto& feed = intake_plenum.create_port();
// 1--4 and 5--8 are separate physical banks for this firing order. // 1--4 and 5--8 are separate physical banks for this firing order.
auto& collector_in = (v8 && i >= 4 ? second_bank_collector : exhaust_collector).create_port();
solver_.add_component(*engines[i]); solver_.add_component(*engines[i]);
boundaries_.add_orifice(feed, 1+i, true, runner_area_); boundaries_.add_orifice(feed, 1+i, true, runner_area_);
boundaries_.add_orifice(engines[i]->intake, 1+i, false, intake_valve_areas_[i]); boundaries_.add_orifice(engines[i]->intake, 1+i, false, intake_valve_areas_[i]);
boundaries_.add_orifice(engines[i]->exhaust, 1+count+i, true, exhaust_valve_areas_[i]); boundaries_.add_orifice(engines[i]->exhaust, 1+count+i, true, exhaust_valve_areas_[i]);
boundaries_.add_orifice(collector_in, 1+count+i, false, header_area_); if (!v8) boundaries_.add_orifice(exhaust_collector.create_port(), 1+count+i, false, header_area_);
} }
auto& collector_out = exhaust_collector.create_port(); if(v8) {
boundaries_.add_orifice(collector_out, 1+2*count, true, tail_area_); auto junction=[&](std::initializer_list<JunctionEnd> ends) {
const int exhaust_exit = turbo_enabled ? 3+2*count : 1+2*count; auto fitting=std::make_unique<Junction>(v8_exhaust.junction_volume,101325,700);
boundaries_.add_open_end(exhaust_exit, false, 101325, pipes[exhaust_exit].area); fitting->connect(boundaries_,pipes,std::span(ends.begin(),ends.size()));
if (v8) { solver_.add_component(*fitting);exhaust_junctions_.push_back(std::move(fitting));
auto& outlet = second_bank_collector.create_port(); };
boundaries_.add_orifice(outlet, 18, true, tail_area_); // Within each bank pair cylinders with the widest available firing gaps.
boundaries_.add_open_end(18, false, 101325, pipes[18].area); 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(v8_exhaust.h_pipe) {
junction({{21,false},{23,true},{25,true}});
junction({{22,false},{24,true},{25,false}});
} else {
boundaries_.add_connection(21,false,23,true);
boundaries_.add_connection(22,false,24,true);
boundaries_.add_closed_end(25,true);boundaries_.add_closed_end(25,false);
}
boundaries_.add_radiating_open_end(23,false,101325,pipes[23].area);
boundaries_.add_radiating_open_end(24,false,101325,pipes[24].area);
} else {
auto& collector_out = exhaust_collector.create_port();
const int exit=1+2*count;
boundaries_.add_orifice(collector_out,exit,true,tail_area_);
boundaries_.add_radiating_open_end(exit,false,101325,pipes[exit].area);
} }
boundaries_.add_radiating_open_end(0, true, 101325, pipes[0].area);
} }
AudioFrame FourStrokeScenario::process() { AudioFrame FourStrokeScenario::process() {
const std::lock_guard guard(mutex_); const std::lock_guard guard(mutex_);
step_motorcycle(crankshaft, dt_, preset == FourStrokePreset::AmericanV8 ? 300.f : preset == FourStrokePreset::SaabB205 ? 200.f : 30.f); step_motorcycle(crankshaft, dt_, preset == FourStrokePreset::AmericanV8 ? 300.f : preset == FourStrokePreset::SaabB205 ? 200.f : 30.f);
ecu.step(dt_, throttle, crankshaft.omega * 60 / (2*pi)); ecu.step(dt_, throttle, crankshaft.omega * 60 / (2*pi));
if (turbo) {
ecu.control_boost(dt_, throttle, crankshaft.omega * 60/(2*pi), intake_plenum.pressure(), turbo->compressor_outlet.pressure());
turbo->set_wastegate_duty(ecu.wastegate_duty());
}
const auto engines = cylinders(); const auto engines = cylinders();
for (int i = 0; i < cylinder_count(); ++i) { for (int i = 0; i < cylinder_count(); ++i) {
auto& c = *engines[i]; auto& c = *engines[i];
@@ -182,19 +212,30 @@ AudioFrame FourStrokeScenario::process() {
} }
const float opening = ecu.throttle_opening(); const float opening = ecu.throttle_opening();
const float angle = opening * pi * .5f; const float angle = opening * pi * .5f;
throttle_area_ = area_for_diameter(preset == FourStrokePreset::AmericanV8 ? .065f : preset == FourStrokePreset::SaabB205 ? .052f : .026f) * (1-std::cos(angle)); throttle_area_ = area_for_diameter(engine_config.cylinders ? (engine_config.carbureted?.085f:.065f) : preset == FourStrokePreset::AmericanV8 ? .065f : preset == FourStrokePreset::SaabB205 ? .052f : .026f) * (1-std::cos(angle));
if(carb_) {
const auto inlet=pipes[0].state(int(pipes[0].cells.size())-1);
carb_->step(dt_,throttle,opening,crankshaft.omega*60/(2*pi),std::max(throttle_port_->mass_flow,0.f),inlet.rho,inlet.pressure,intake_plenum.pressure());
intake_plenum.add_fuel_mass(carb_->fuel_flow()*dt_);
}
solver_.step(dt_); solver_.step(dt_);
intake_flow = intake_atmosphere.port.mass_flow; intake_flow = boundaries_.open_end_mass_flow(preset == FourStrokePreset::AmericanV8 ? 2 : 1);
exhaust_flow = boundaries_.open_end_mass_flow(0); exhaust_flow = boundaries_.open_end_mass_flow(0);
const float other_exhaust = preset == FourStrokePreset::AmericanV8 ? boundaries_.open_end_mass_flow(1) : 0.f; const float other_exhaust = preset == FourStrokePreset::AmericanV8 ? boundaries_.open_end_mass_flow(1) : 0.f;
const auto rendered = audio_.process(intake_flow, exhaust_flow, other_exhaust); // Compact mouth radiation responds to volume velocity. Use ambient-equivalent
// mass flow so hot/cold donor-density changes do not enter the derivative.
const float intake_source = ambient_density * boundaries_.open_end_volume_flow(preset == FourStrokePreset::AmericanV8 ? 2 : 1);
const float exhaust_source = ambient_density * boundaries_.open_end_volume_flow(0);
const float other_source = preset == FourStrokePreset::AmericanV8 ? ambient_density * boundaries_.open_end_volume_flow(1) : 0.f;
const auto rendered = audio_.process(intake_source, exhaust_source, other_source);
exhaust_flow += other_exhaust; exhaust_flow += other_exhaust;
const float mechanical = mechanical_sound_enabled
? mechanical_sound_.process(crankshaft, cylinder1, load) * mechanical_mix : 0;
last_mix_l = rendered.left + mechanical;
last_mix_r = rendered.right + mechanical;
++sample_count; ++sample_count;
return {last_mix_l, last_mix_r}; return rendered;
}
void FourStrokeScenario::set_pipe_solver(PipeSolver solver) {
const std::lock_guard guard(mutex_);
for(auto& pipe : pipes) pipe.solver=solver;
} }
void FourStrokeScenario::set_audio_scene(OutdoorScene scene) { void FourStrokeScenario::set_audio_scene(OutdoorScene scene) {
@@ -210,6 +251,11 @@ AudioFrame FourStrokeScenario::unfiltered_audio() const {
const std::lock_guard<std::mutex> guard(mutex_); return audio_.unfiltered_frame(); const std::lock_guard<std::mutex> guard(mutex_); return audio_.unfiltered_frame();
} }
void FourStrokeScenario::set_starter(bool engaged) {
const std::lock_guard guard(mutex_);
starter_engaged=engaged;
}
void FourStrokeScenario::set_controls(float throttle_value, float load_value, float clutch_value) { void FourStrokeScenario::set_controls(float throttle_value, float load_value, float clutch_value) {
const std::lock_guard<std::mutex> guard(mutex_); const std::lock_guard<std::mutex> guard(mutex_);
throttle = std::clamp(throttle_value, 0.0f, 1.0f); throttle = std::clamp(throttle_value, 0.0f, 1.0f);
@@ -217,11 +263,6 @@ void FourStrokeScenario::set_controls(float throttle_value, float load_value, fl
clutch = std::clamp(clutch_value, 0.0f, 1.0f); clutch = std::clamp(clutch_value, 0.0f, 1.0f);
} }
void FourStrokeScenario::toggle_mechanical_sound() {
const std::lock_guard<std::mutex> guard(mutex_);
mechanical_sound_enabled = !mechanical_sound_enabled;
}
void FourStrokeScenario::shift_up() { void FourStrokeScenario::shift_up() {
const std::lock_guard<std::mutex> guard(mutex_); const std::lock_guard<std::mutex> guard(mutex_);
vehicle.shift_up(); vehicle.shift_up();
@@ -245,10 +286,14 @@ void FourStrokeScenario::set_first_gear() {
ScenarioSnapshot FourStrokeScenario::snapshot() const { ScenarioSnapshot FourStrokeScenario::snapshot() const {
const std::lock_guard<std::mutex> guard(mutex_); const std::lock_guard<std::mutex> guard(mutex_);
ScenarioSnapshot result; ScenarioSnapshot result;
result.turbo_enabled = bool(turbo); result.layout=engine_config.cylinders?engine_config.layout:preset==FourStrokePreset::AmericanV8?EngineLayout::V:EngineLayout::Inline;
result.boost_target = ecu.boost_target(); result.bank_angle=engine_config.cylinders?engine_config.bank_angle:56;
result.wastegate_duty = ecu.wastegate_duty(); result.v8_headers=preset==FourStrokePreset::AmericanV8;
if (turbo) result.turbo = turbo->snapshot(); result.silencers = preset == FourStrokePreset::SaabB205 && exhaust_config.enabled;
result.exhaust_config = exhaust_config;
result.h_pipe = preset==FourStrokePreset::AmericanV8 && v8_exhaust_config.h_pipe;
result.crossover_pressures = preset==FourStrokePreset::AmericanV8 ? std::array<float,2>{pipes[21].state(0).pressure,pipes[22].state(0).pressure} : std::array<float,2>{};
result.pipe_solver = pipes.front().solver;
result.solver_substeps = solver_.substep_count(); result.solver_substeps = solver_.substep_count();
result.boundary_iterations = boundaries_.nonlinear_iterations; result.boundary_iterations = boundaries_.nonlinear_iterations;
result.rpm = crankshaft.omega * 60.0f / (2.0f * pi); result.rpm = crankshaft.omega * 60.0f / (2.0f * pi);
@@ -267,10 +312,11 @@ ScenarioSnapshot FourStrokeScenario::snapshot() const {
result.valve2_intake_area = intake_valve_areas_[1]; result.valve2_intake_area = intake_valve_areas_[1];
result.valve2_exhaust_area = exhaust_valve_areas_[1]; result.valve2_exhaust_area = exhaust_valve_areas_[1];
result.intake_pressure = intake_plenum.pressure(); result.intake_pressure = intake_plenum.pressure();
result.exhaust_pressure = exhaust_collector.pressure(); result.exhaust_pressure = preset==FourStrokePreset::AmericanV8 ? pipes[21].state(0).pressure : exhaust_collector.pressure();
result.second_bank_pressure = preset==FourStrokePreset::AmericanV8 ? pipes[22].state(0).pressure : 101325;
result.throttle = ecu.throttle_opening(); result.throttle = ecu.throttle_opening();
result.pedal = throttle; result.pedal = throttle;
result.target_afr = ecu.target_afr(); result.target_afr = carb_?carb_->indicated_afr():ecu.target_afr();
result.idle_control = ecu.idle_active(); result.idle_control = ecu.idle_active();
result.fuel_cut = ecu.fuel_cut(); result.fuel_cut = ecu.fuel_cut();
result.audio_scene = audio_.scene(); result.audio_scene = audio_.scene();
@@ -280,12 +326,12 @@ ScenarioSnapshot FourStrokeScenario::snapshot() const {
result.gear = vehicle.current_gear; result.gear = vehicle.current_gear;
result.rev_limit1 = cylinder1.rev_limit_active; result.rev_limit1 = cylinder1.rev_limit_active;
result.rev_limit2 = cylinder2.rev_limit_active; result.rev_limit2 = cylinder2.rev_limit_active;
result.mechanical_sound_enabled = mechanical_sound_enabled;
result.cylinder_count = cylinder_count(); result.cylinder_count = cylinder_count();
result.rev_limit_rpm = ecu.rev_limit_rpm; result.rev_limit_rpm = ecu.rev_limit_rpm;
const std::array<const FourStrokeCylinder*, 8> engines{&cylinder1, &cylinder2, &cylinder3, &cylinder4, const std::array<const FourStrokeCylinder*, 12> engines{&cylinder1, &cylinder2, &cylinder3, &cylinder4,
&cylinder5, &cylinder6, &cylinder7, &cylinder8}; &cylinder5, &cylinder6, &cylinder7, &cylinder8,&cylinder9,&cylinder10,&cylinder11,&cylinder12};
for (int i = 0; i < cylinder_count(); ++i) { for (int i = 0; i < cylinder_count(); ++i) {
result.cylinder_angles[i] = crankshaft.angle + engines[i]->phase;
result.cylinder_pressures[i] = engines[i]->pressure; result.cylinder_pressures[i] = engines[i]->pressure;
result.piston_fractions[i] = (engines[i]->volume-engines[i]->clearance_volume())/engines[i]->swept_volume(); result.piston_fractions[i] = (engines[i]->volume-engines[i]->clearance_volume())/engines[i]->swept_volume();
result.intake_areas[i] = intake_valve_areas_[i]; result.intake_areas[i] = intake_valve_areas_[i];
@@ -299,20 +345,32 @@ float FourStrokeScenario::area_for_diameter(float diameter) {
return pi * diameter * diameter * 0.25f; return pi * diameter * diameter * 0.25f;
} }
std::vector<Pipe1D> FourStrokeScenario::create_pipes(FourStrokePreset preset, bool turbo_enabled) { std::vector<Pipe1D> FourStrokeScenario::create_pipes(FourStrokePreset preset, const ExhaustConfig& exhaust, const V8ExhaustConfig& v8_exhaust, const EngineConfig& engine) {
if(engine.cylinders) {
if(engine.cylinders<1||engine.cylinders>12||engine.bore<=0||engine.stroke<=0||engine.rod<=engine.stroke*.5f)throw std::invalid_argument("invalid custom engine dimensions");
std::vector<Pipe1D> result;
result.emplace_back(area_for_diameter(.075f),.20f,4);
for(int i=0;i<engine.cylinders;++i)result.emplace_back(area_for_diameter(.035f),.25f,5);
for(int i=0;i<engine.cylinders;++i)result.emplace_back(area_for_diameter(.038f),.65f,13);
result.emplace_back(area_for_diameter(.060f),1.5f,30);
return result;
}
const bool saab = preset == FourStrokePreset::SaabB205; const bool saab = preset == FourStrokePreset::SaabB205;
if (preset == FourStrokePreset::AmericanV8) { if (preset == FourStrokePreset::AmericanV8) {
// One inlet/throttle -> shared plenum -> short individual runners. // One inlet/throttle -> shared plenum -> short individual runners.
// Independent exhaust banks. Cells stay long enough to avoid routine // Bank collectors feed twin pipes joined by a finite-length H crossover.
// Cells stay long enough to avoid routine
// extra CFL substeps during high-vacuum coast-down. // extra CFL substeps during high-vacuum coast-down.
std::vector<Pipe1D> result; std::vector<Pipe1D> result;
result.emplace_back(area_for_diameter(.070f), .18f, 3); result.emplace_back(area_for_diameter(.070f), .18f, 3);
for (int i = 0; i < 8; ++i) for (int i = 0; i < 8; ++i)
result.emplace_back(area_for_diameter(.038f), .12f, 2); result.emplace_back(area_for_diameter(.038f), .12f, 2);
for (int i = 0; i < 8; ++i) for (int i = 0; i < 8; ++i)
result.emplace_back(area_for_diameter(.040f), .60f + .06f*(i%4), 12); result.emplace_back(area_for_diameter(v8_exhaust.primary_diameter),v8_exhaust.primary_length,int(std::ceil(v8_exhaust.primary_length/.05f)));
result.emplace_back(area_for_diameter(.050f), 1.8f, 36); for(int i=0;i<4;++i) result.emplace_back(area_for_diameter(.045f),.40f,8);
result.emplace_back(area_for_diameter(.050f), 1.95f, 39); for(int i=0;i<2;++i) result.emplace_back(area_for_diameter(.050f),.55f,11);
for(int i=0;i<2;++i) result.emplace_back(area_for_diameter(.050f),1.25f,25);
result.emplace_back(area_for_diameter(.038f),.78f,16);
return result; return result;
} }
const int count = saab ? 4 : 2; const int count = saab ? 4 : 2;
@@ -323,11 +381,8 @@ std::vector<Pipe1D> FourStrokeScenario::create_pipes(FourStrokePreset preset, bo
result.emplace_back(area_for_diameter(saab ? .032f : .0275f), saab ? .25f : .12f, 10); result.emplace_back(area_for_diameter(saab ? .032f : .0275f), saab ? .25f : .12f, 10);
for (int i = 0; i < count; ++i) for (int i = 0; i < count; ++i)
result.emplace_back(area_for_diameter(saab ? .034f : .028f), saab ? .28f : .20f, saab ? 10 : 6); result.emplace_back(area_for_diameter(saab ? .034f : .028f), saab ? .28f : .20f, saab ? 10 : 6);
result.emplace_back(area_for_diameter(saab ? .060f : .028f), turbo_enabled ? .12f : (saab ? 1.4f : .40f), saab ? (turbo_enabled ? 8 : 40) : 10); result.emplace_back(area_for_diameter(saab ? .060f : .028f), saab ? 1.4f : .40f, saab ? 40 : 10);
if (turbo_enabled) { if (saab) result.back() = make_silenced_exhaust(exhaust);
result.emplace_back(area_for_diameter(saab ? .060f : .0275f), .10f, 8);
result.emplace_back(area_for_diameter(saab ? .060f : .032f), .60f, 24);
}
return result; return result;
} }

83
src/scenario_menu.cpp Normal file
View File

@@ -0,0 +1,83 @@
#include "FluidSim/Desktop/ScenarioMenu.hpp"
#include <algorithm>
namespace fluidsim::port::desktop {
std::optional<std::size_t> choose_scenario(sf::RenderWindow& window,const sf::Font* font,const std::vector<ScenarioExample>& examples) {
if(examples.empty())return std::nullopt;
int selected=0,first=0;
std::vector<std::string> 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; };
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<Item> items;
if(category.empty()) for(std::size_t i=0;i<categories.size();++i) {
const auto count=std::count_if(examples.begin(),examples.end(),[&](const auto& e){return e.category==categories[i];});
items.push_back({categories[i],std::to_string(count)+" variants / click to browse",i});
} else for(std::size_t i=0;i<examples.size();++i) if(examples[i].category==category)items.push_back({examples[i].title,examples[i].description,i});
const int count=int(items.size()), rows=std::max(1,int((h-210)/86));
first=std::clamp(first,0,std::max(0,count-rows));
const sf::FloatRect start{{24,h-85},{w-48,46}};
bool navigating=false;
auto activate=[&]()->std::optional<std::size_t> {
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)first=selected;if(selected>=first+rows)first=selected-rows+1;};
while(auto event=window.pollEvent()) {
if(event->is<sf::Event::Closed>())return std::nullopt;
if(auto key=event->getIf<sf::Event::KeyPressed>()) {
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<sf::Event::MouseWheelScrolled>()) {
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<sf::Event::MouseButtonReleased>();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;row<rows && first+row<count;++row)
if(!navigating && sf::FloatRect({24,105.f+row*86},{w-58,76}).contains(point)) {selected=first+row;if(auto choice=activate())return choice;break;}
}
if(navigating)break;
}
if(navigating)continue;
window.setView(sf::View(sf::FloatRect({0,0},{w,h})));window.clear({8,8,8});
label("FLUIDSIM / EXAMPLES",24,20,24);label(category.empty()?"Choose an engine category":"< Categories "+category,24,61,15,{170,170,170});
for(int row=0;row<rows && first+row<count;++row) {
const int index=first+row;const float y=105.f+row*86;
sf::RectangleShape card({w-58,76});card.setPosition({24,y});
card.setFillColor(index==selected?sf::Color(48,48,48):sf::Color(23,23,23));
card.setOutlineThickness(1);card.setOutlineColor(index==selected?sf::Color::White:sf::Color(60,60,60));window.draw(card);
label(items[index].title,38,y+10,17);label(items[index].description,38,y+44,12,{170,170,170});
}
if(count>rows) {
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();
}
return std::nullopt;
}
}

View File

@@ -1,200 +0,0 @@
#include "FluidSim/Turbocharger.hpp"
#include <algorithm>
#include <cmath>
#include <stdexcept>
namespace fluidsim::port {
namespace {
constexpr double cp = double(gamma)*gas_constant/(gamma-1);
constexpr double exponent = double(gamma-1)/gamma;
TurboConfig checked(TurboConfig c) {
for (double x : {c.shaft_inertia, c.compressor_radius, c.turbine_radius,
c.compressor_head_coefficient, c.turbine_optimum_speed_ratio,
c.compressor_effective_area, c.turbine_effective_area, c.gate_diaphragm_area,
c.gate_spring_stiffness, c.gate_damping, c.gate_diameter, c.reference_pressure})
if (!std::isfinite(x) || x <= 0) throw std::invalid_argument("invalid turbo geometry/inertia");
for (double x : {c.compressor_efficiency, c.turbine_peak_efficiency})
if (!std::isfinite(x) || x <= 0 || x > 1) throw std::invalid_argument("invalid turbo efficiency");
for (double x : {c.bearing_viscous, c.windage, c.gate_spring_preload, c.gate_maximum_lift})
if (!std::isfinite(x) || x < 0) throw std::invalid_argument("invalid turbo loss coefficient");
for (float x : c.connection_diameters)
if (!std::isfinite(x) || x <= 0) throw std::invalid_argument("invalid turbo connection diameter");
return c;
}
}
TurboConfig TurboConfig::gt17() {
TurboConfig c;
c.shaft_inertia = 2.0e-5;
c.compressor_radius = .026;
c.turbine_radius = .019;
c.compressor_efficiency = .70;
c.turbine_peak_efficiency = .70;
c.compressor_effective_area = 420e-6;
c.turbine_effective_area = 350e-6;
c.bearing_viscous = 2e-7;
c.windage = 5e-11;
c.gate_diaphragm_area = .001;
c.gate_spring_preload = 25;
c.gate_diameter = .024;
c.gate_maximum_lift = .007;
c.compressor_inlet_volume = c.compressor_outlet_volume = 100e-6f;
c.turbine_inlet_volume = c.turbine_outlet_volume = 100e-6f;
c.connection_diameters = {.045f, .045f, .040f, .050f};
return c;
}
Turbocharger::Turbocharger(TurboConfig configuration)
: config(checked(configuration)),
compressor_inlet(config.compressor_inlet_volume), compressor_outlet(config.compressor_outlet_volume),
turbine_inlet(config.turbine_inlet_volume), turbine_outlet(config.turbine_outlet_volume),
compressor_inlet_port(compressor_inlet.create_port()),
compressor_outlet_port(compressor_outlet.create_port()),
turbine_inlet_port(turbine_inlet.create_port()), turbine_outlet_port(turbine_outlet.create_port()),
internal_{&compressor_inlet.create_port(), &compressor_outlet.create_port(),
&turbine_inlet.create_port(), &turbine_outlet.create_port()} {}
void Turbocharger::connect(BoundarySystem& b, const std::vector<Pipe1D>& pipes,
int ci, bool cil, int co, bool col, int ti, bool til, int to, bool tol) {
if (connected_) throw std::logic_error("turbo already connected");
const std::array<int, 4> indices{ci, co, ti, to};
const std::array<bool, 4> left{cil, col, til, tol};
auto external = ports();
for (std::size_t i = 0; i < 4; ++i) {
const auto& p = pipes.at(indices[i]);
const float diameter = config.connection_diameters[i];
connection_areas_[i] = std::min(p.cell_area(left[i] ? 0 : int(p.cells.size())-1), pi*diameter*diameter*.25f);
b.add_orifice(*external[i], indices[i], left[i], connection_areas_[i]);
}
connected_ = true;
}
std::vector<Port*> Turbocharger::ports() {
return {&compressor_inlet_port, &compressor_outlet_port, &turbine_inlet_port, &turbine_outlet_port};
}
double Turbocharger::fluid_energy() const {
return double(compressor_inlet.internal_energy()) + compressor_outlet.internal_energy() +
turbine_inlet.internal_energy() + turbine_outlet.internal_energy();
}
void Turbocharger::set_initial_rpm(double rpm) {
if (!std::isfinite(rpm) || rpm < 0) throw std::invalid_argument("invalid initial turbo speed");
omega_ = rpm*2*pi/60;
}
double Turbocharger::nozzle(double p, double t, double back, double area) {
if (back >= p) return 0;
const double critical = std::pow(2/(gamma+1), gamma/(gamma-1));
const double ratio = std::max(back/p, critical);
const double factor = 2*gamma/(gas_constant*(gamma-1)) *
(std::pow(ratio, 2/gamma)-std::pow(ratio, (gamma+1)/gamma));
return area*p/std::sqrt(t)*std::sqrt(std::max(0., factor));
}
double Turbocharger::transfer_flow(Volume0D& in, Volume0D& out, Port& in_ext,
Port& out_ext, double requested, double dt) {
const bool forward = requested >= 0;
const auto& donor = forward ? in : out;
const auto& external = forward ? in_ext : out_ext;
const double h = cp*donor.temperature();
// Reserve the mass/energy already promised to the external pipe boundary.
const double mass = donor.mass()+std::min(0.f, external.mass_flow)*dt;
const double energy = donor.internal_energy()+std::min(0.f, external.mass_flow)*external.enthalpy*dt;
const double bound = .25*std::max(0., std::min(mass, energy/h))/dt;
const double actual = std::clamp(requested, -bound, bound);
if (actual != requested) ++limited_transfers_;
return actual;
}
void Turbocharger::set_transfer(Volume0D& in, Volume0D& out, Port& ip, Port& op,
double flow, double work, double dt) {
const auto& donor = flow >= 0 ? in : out;
const double h = cp*donor.temperature();
ip.mass_flow = float(-flow);
op.mass_flow = float(flow);
ip.air_fraction = op.air_fraction = donor.air_fraction();
ip.fuel_fraction = op.fuel_fraction = donor.fuel_fraction();
ip.enthalpy = op.enthalpy = float(h);
// Positive work means energy delivered to the shaft. Only the receiving
// fluid enthalpy changes; the donor loses precisely its stagnation enthalpy.
if (std::abs(flow) > 0) {
auto& receiver = flow >= 0 ? op : ip;
receiver.enthalpy = float(h-work/(std::abs(flow)*dt));
}
}
void Turbocharger::update_state(float timestep) {
if (!std::isfinite(timestep) || timestep < 0) throw std::invalid_argument("invalid turbo timestep");
if (timestep == 0) return;
const double dt = timestep, initial_fluid = fluid_energy(), initial_shaft = shaft_energy();
const std::array<const Port*, 4> external{&compressor_inlet_port, &compressor_outlet_port,
&turbine_inlet_port, &turbine_outlet_port};
double external_work = 0;
for (const auto* p : external) external_work += double(p->mass_flow)*p->enthalpy*dt;
const double pin = compressor_inlet.pressure(), tin = compressor_inlet.temperature();
const double head = config.compressor_head_coefficient*std::pow(omega_*config.compressor_radius, 2);
const double pump_pressure = pin*std::pow(1+config.compressor_efficiency*head/(cp*tin), 1/exponent);
double mc = compressor_outlet.pressure() <= pump_pressure
? nozzle(pump_pressure, tin+head/cp, compressor_outlet.pressure(), config.compressor_effective_area)
: -nozzle(compressor_outlet.pressure(), compressor_outlet.temperature(), pump_pressure,
config.compressor_effective_area);
mc = transfer_flow(compressor_inlet, compressor_outlet, compressor_inlet_port, compressor_outlet_port, mc, dt);
const double tp = turbine_inlet.pressure(), tout = turbine_outlet.pressure();
const double can_force = std::max(0., double(compressor_outlet.pressure())-config.reference_pressure)*
config.gate_diaphragm_area*(1-gate_duty_);
const double equilibrium_lift = (can_force-config.gate_spring_preload)/config.gate_spring_stiffness;
// Overdamped spring-can linkage: b*dx/dt + k*x = F-Fpreload, with seats.
wastegate_lift_ = std::clamp(equilibrium_lift+(wastegate_lift_-equilibrium_lift)*
std::exp(-dt*config.gate_spring_stiffness/config.gate_damping), 0., config.gate_maximum_lift);
const double gate_area = pi*config.gate_diameter*wastegate_lift_;
double mt = tp >= tout ? nozzle(tp, turbine_inlet.temperature(), tout, config.turbine_effective_area)
: -nozzle(tout, turbine_outlet.temperature(), tp, config.turbine_effective_area);
double mw = tp >= tout ? nozzle(tp, turbine_inlet.temperature(), tout, gate_area)
: -nozzle(tout, turbine_outlet.temperature(), tp, gate_area);
const double requested = mt+mw;
const double actual = transfer_flow(turbine_inlet, turbine_outlet, turbine_inlet_port, turbine_outlet_port, requested, dt);
if (requested != 0) { mt *= actual/requested; mw *= actual/requested; }
const double ideal_drop = tp > tout ? cp*turbine_inlet.temperature()*(1-std::pow(tout/tp, exponent)) : 0;
const double jet = std::sqrt(2*ideal_drop), radius = config.turbine_radius;
const double ratio = config.turbine_optimum_speed_ratio, eta = config.turbine_peak_efficiency;
const double drive = std::max(0., mt)*eta*jet*radius/ratio;
const double turbine_drag = std::max(0., mt)*eta*.5*radius*radius/(ratio*ratio);
const double compressor_drag = std::max(0., mc)*config.compressor_head_coefficient*
config.compressor_radius*config.compressor_radius;
// Implicit midpoint solves I*domega/dt = drive - b*omega - c*omega^2.
// Evaluating work at midpoint conserves rotor kinetic energy, even at rest.
double wt = 0, wc = 0, loss = 0, remaining = dt;
int steps = 0;
while (remaining > dt*1e-12) {
const double drag = turbine_drag+compressor_drag+config.bearing_viscous+config.windage*omega_;
const double h = std::min(remaining, config.shaft_inertia/std::max(drag, 1e-300));
if (!(h > 0) || ++steps > 1000000) throw std::runtime_error("turbo shaft timestep failed");
const double b = 2*config.shaft_inertia/h+turbine_drag+compressor_drag+config.bearing_viscous;
const double rhs = 2*config.shaft_inertia/h*omega_+drive;
const double mid = 2*rhs/(b+std::sqrt(b*b+4*config.windage*rhs));
const double next = 2*mid-omega_;
if (next < 0) throw std::runtime_error("turbo shaft integration reversed unexpectedly");
wt += (drive-turbine_drag*mid)*mid*h;
wc += compressor_drag*mid*mid*h;
loss += (config.bearing_viscous+config.windage*mid)*mid*mid*h;
omega_ = next;
angle_ = std::fmod(angle_+mid*h, 2*double(pi));
remaining -= h;
}
bearing_heat_ += loss;
turbine_power_ = wt/dt;
compressor_power_ = wc/dt;
bearing_power_ = loss/dt;
turbine_flow_ = mt;
wastegate_flow_ = mw;
compressor_flow_ = mc;
set_transfer(compressor_inlet, compressor_outlet, *internal_[0], *internal_[1], mc, -wc, dt);
set_transfer(turbine_inlet, turbine_outlet, *internal_[2], *internal_[3], mt+mw, wt, dt);
compressor_inlet.update_state(timestep);
compressor_outlet.update_state(timestep);
turbine_inlet.update_state(timestep);
turbine_outlet.update_state(timestep);
energy_residual_ += fluid_energy()-initial_fluid + shaft_energy()-initial_shaft+loss-external_work;
}
TurboSnapshot Turbocharger::snapshot() const {
return {omega_*60/(2*pi), angle_, shaft_energy(), turbine_power_, compressor_power_, bearing_power_,
compressor_flow_, turbine_flow_, compressor_outlet.pressure()/compressor_inlet.pressure(),
turbine_inlet.pressure()/turbine_outlet.pressure(), turbine_inlet.pressure(),
turbine_outlet.pressure(), compressor_outlet.pressure(), compressor_outlet.temperature(),
bearing_heat_, energy_residual_, limited_transfers_, wastegate_flow_, wastegate_lift_};
}
} // namespace fluidsim::port

View File

@@ -1,4 +1,5 @@
#include "FluidSim/TwoStrokeScenario.hpp" #include "FluidSim/TwoStrokeScenario.hpp"
#include "FluidSim/Constants.hpp"
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
@@ -10,13 +11,22 @@ TwoStrokeScenario::TwoStrokeScenario(int sample_rate, TwoStrokeConfig config)
carburetor(config.carburetor_diameter), carburetor(config.carburetor_diameter),
cylinder(config.bore, config.stroke, config.rod, config.geometric_compression(), cylinder(config.bore, config.stroke, config.rod, config.geometric_compression(),
config.transfer_duration, config.exhaust_duration), config.transfer_duration, config.exhaust_duration),
crankcase(crankshaft, config.stroke * .5f, config.rod, config.bore, 150e-6f), pipes(create_pipes(config)), crankcase(crankshaft, config.stroke * .5f, config.rod, config.bore, config.crankcase_clearance), pipes(create_pipes(config)),
dt_(1.0f / static_cast<float>(std::max(sample_rate, 1))), max_throttle_area_(area_for_diameter(config.carburetor_diameter)), dt_(1.0f / static_cast<float>(std::max(sample_rate, 1))), max_throttle_area_(area_for_diameter(config.carburetor_diameter)),
reed_in_area_(area_for_diameter(0.040f)), reed_out_area_(0.0f), reed_in_area_(area_for_diameter(0.040f)), reed_out_area_(0.0f),
transfer_pipe_area_(area_for_diameter(0.032f)), transfer_valve_area_(0.0f), exhaust_valve_area_(0.0f), transfer_pipe_area_(area_for_diameter(0.032f)), transfer_valve_area_(0.0f), exhaust_valve_area_(0.0f),
boundaries_(pipes), solver_(pipes, boundaries_), audio_(sample_rate) { boundaries_(pipes), solver_(pipes, boundaries_), audio_(sample_rate) {
if (!std::isfinite(config.idle_slide_opening) || config.idle_slide_opening<0 || config.idle_slide_opening>1)
throw std::invalid_argument("carb idle slide opening must be in [0,1]");
ecu.idle_control = Ecu::IdleControl::FixedOpening;
carburetor.pilot_jet_diameter*=config.jet_scale;
carburetor.main_jet_diameter*=config.jet_scale;
carburetor.needle_jet_diameter*=config.jet_scale;
ecu.throttle_time_constant = .0002f;
ecu.throttle_rate = 5000;
crankshaft.omega = 3000.0f * 2.0f * pi / 60.0f; crankshaft.omega = 3000.0f * 2.0f * pi / 60.0f;
crankshaft.inertia = 0.01f; crankshaft.inertia = 0.01f*(config.bore*config.bore*config.stroke)/(.054f*.054f*.0545f);
starter_torque = 8.f;
const float pressure_to_torque = cylinder.swept_volume() * 1e5f / (2 * pi); const float pressure_to_torque = cylinder.swept_volume() * 1e5f / (2 * pi);
const float piston_speed_per_omega = config.stroke / pi; const float piston_speed_per_omega = config.stroke / pi;
crankshaft.friction_constant = pressure_to_torque * config.friction_bar; crankshaft.friction_constant = pressure_to_torque * config.friction_bar;
@@ -39,9 +49,9 @@ TwoStrokeScenario::TwoStrokeScenario(int sample_rate, TwoStrokeConfig config)
ecu.idle_rpm = 3000; ecu.idle_rpm = 3000;
ecu.rev_limit_enabled = false; ecu.rev_limit_enabled = false;
cylinder.rev_limit_enabled = false; cylinder.rev_limit_enabled = false;
ecu.idle_base_opening = .08f; ecu.idle_base_opening = config.idle_slide_opening;
ecu.idle_max_opening = .3f; ecu.idle_max_opening = .3f;
ecu.idle_spark_gain=.04f; ecu.idle_spark_gain=0;
ecu.overrun_cut_rpm = 5000; ecu.overrun_cut_rpm = 5000;
ecu.overrun_resume_rpm = 3500; ecu.overrun_resume_rpm = 3500;
ecu.overrun_fuel_cut_enabled = false; ecu.overrun_fuel_cut_enabled = false;
@@ -69,10 +79,9 @@ TwoStrokeScenario::TwoStrokeScenario(int sample_rate, TwoStrokeConfig config)
plenum_inlet_ = &intake_plenum.create_port(); plenum_inlet_ = &intake_plenum.create_port();
plenum_outlet_ = &intake_plenum.create_port(); plenum_outlet_ = &intake_plenum.create_port();
intake_boundary_area_ = pipes[0].area; boundaries_.add_radiating_open_end(3, false, 101325.0f,
boundaries_.add_orifice(intake_atmosphere.port, 0, true, intake_boundary_area_);
boundaries_.add_open_end(3, false, 101325.0f,
pipes[3].cell_area(static_cast<int>(pipes[3].cells.size()) - 1)); pipes[3].cell_area(static_cast<int>(pipes[3].cells.size()) - 1));
boundaries_.add_radiating_open_end(0, true, 101325, pipes[0].area);
boundaries_.add_orifice(*plenum_inlet_, 0, false, max_throttle_area_, 0.72f); boundaries_.add_orifice(*plenum_inlet_, 0, false, max_throttle_area_, 0.72f);
boundaries_.add_orifice(*plenum_outlet_, 1, true, reed_in_area_); boundaries_.add_orifice(*plenum_outlet_, 1, true, reed_in_area_);
boundaries_.add_orifice(crankcase.intake_port, 1, false, reed_out_area_, 0.9f); boundaries_.add_orifice(crankcase.intake_port, 1, false, reed_out_area_, 0.9f);
@@ -80,7 +89,6 @@ TwoStrokeScenario::TwoStrokeScenario(int sample_rate, TwoStrokeConfig config)
boundaries_.add_orifice(cylinder.intake, 2, false, transfer_valve_area_); boundaries_.add_orifice(cylinder.intake, 2, false, transfer_valve_area_);
boundaries_.add_orifice(cylinder.exhaust, 3, true, exhaust_valve_area_); boundaries_.add_orifice(cylinder.exhaust, 3, true, exhaust_valve_area_);
solver_.minimum_substeps = 4; solver_.minimum_substeps = 4;
solver_.add_component(intake_atmosphere);
solver_.add_component(intake_plenum); solver_.add_component(intake_plenum);
solver_.add_component(crankcase); solver_.add_component(crankcase);
solver_.add_component(cylinder); solver_.add_component(cylinder);
@@ -105,9 +113,10 @@ AudioFrame TwoStrokeScenario::process() {
solver_.step(dt_); solver_.step(dt_);
++step_count_; ++step_count_;
intake_flow = intake_atmosphere.port.mass_flow; intake_flow = boundaries_.open_end_mass_flow(1);
exhaust_flow = boundaries_.open_end_mass_flow(0); exhaust_flow = boundaries_.open_end_mass_flow(0);
return audio_.process(intake_flow, exhaust_flow); return audio_.process(ambient_density * boundaries_.open_end_volume_flow(1),
ambient_density * boundaries_.open_end_volume_flow(0));
} }
void TwoStrokeScenario::set_throttle(float value) { throttle = std::clamp(value, 0.0f, 1.0f); } void TwoStrokeScenario::set_throttle(float value) { throttle = std::clamp(value, 0.0f, 1.0f); }

View File

@@ -5,6 +5,7 @@
#include <iostream> #include <iostream>
#include <stdexcept> #include <stdexcept>
#include <vector> #include <vector>
#include <span>
using namespace fluidsim::port; using namespace fluidsim::port;
namespace { namespace {
@@ -14,7 +15,8 @@ void check(bool condition, const char* message) {
} }
int main() { int main() {
SaabB205Scenario s; SaabB205Scenario s;
const auto cylinders = s.cylinders(); const auto storage = s.cylinders();
const std::span<FourStrokeCylinder* const> cylinders(storage.data(), s.cylinder_count());
check(s.cylinder_count() == 4 && s.pipes.size() == 10, "B205 topology is not four-cylinder"); check(s.cylinder_count() == 4 && s.pipes.size() == 10, "B205 topology is not four-cylinder");
check(std::abs(4*s.cylinder1.swept_volume()*1e6f-1984.9f) < 1, "B205 displacement incorrect"); check(std::abs(4*s.cylinder1.swept_volume()*1e6f-1984.9f) < 1, "B205 displacement incorrect");
check(s.vehicle.gear_ratios.size() == 5 && s.vehicle.mass == 1675, "wagon configuration incorrect"); check(s.vehicle.gear_ratios.size() == 5 && s.vehicle.mass == 1675, "wagon configuration incorrect");

View File

@@ -0,0 +1,37 @@
#include "FluidSim/Desktop/DesktopUi.hpp"
#include <cmath>
#include <iostream>
#include <stdexcept>
using namespace fluidsim::port::desktop;
void check(bool condition,const char* reason) {if(!condition)throw std::runtime_error(reason);}
int main() {
for(auto size : {sf::Vector2u{933,1010},sf::Vector2u{1920,1080},sf::Vector2u{1040,800},sf::Vector2u{800,700}}) {
const auto l=WorkspaceLayout::calculate(size);
for(auto r : {l.sidebar,l.viewport,l.chart,l.inspector,l.pedal}) {
check(r.position.x>=0 && r.position.y>=0 && r.size.x>0 && r.size.y>0,"invalid panel bounds");
check(r.position.x+r.size.x<=size.x && r.position.y+r.size.y<=size.y,"panel escapes window");
}
check(l.sidebar.position.x+l.sidebar.size.x<l.viewport.position.x,"sidebar overlaps engine");
check(l.viewport.position.y+l.viewport.size.y<l.chart.position.y,"engine overlaps dyno");
check(l.chart.position.x+l.chart.size.x<l.inspector.position.x,"dyno overlaps inspector");
}
DesktopUi ui;
const sf::Vector2u size{1440,900};const auto track=WorkspaceLayout::calculate(size).pedal;
const sf::Vector2i point{int(track.position.x+track.size.x*.7f),int(track.position.y+12)};
ui.handle_event(sf::Event::MouseButtonPressed{sf::Mouse::Button::Left,point},size);
check(std::abs(ui.pedal()-.7f)<.01f,"slider should set direct pedal request");
ui.update_pedal(.5f,false,true);
check(std::abs(ui.pedal()-.7f)<.01f,"keyboard ramp must not interfere while dragging slider");
ui.handle_event(sf::Event::MouseButtonReleased{sf::Mouse::Button::Left,point},size);
check(ui.pedal()==0,"slider must spring to zero on release");
ui.update_pedal(.5f,true,false);check(ui.pedal()==1,"W full travel must take 0.5 seconds");
ui.update_pedal(.25f,false,false);check(std::abs(ui.pedal()-.5f)<1e-6f,"keyboard release travel timing");
ui.update_pedal(.25f,false,false);check(ui.pedal()==0,"keyboard must return to idle request");
ui.handle_event(sf::Event::MouseButtonPressed{sf::Mouse::Button::Left,point},size);
ui.handle_event(sf::Event::FocusLost{},size);check(ui.pedal()==0,"lost focus must release UI pedal");
OrbitCamera camera;camera.reset(2);auto before=camera.target;
camera.orbit({40,20});camera.pan({20,15},500);camera.zoom(2);
check(camera.pitch<=80 && camera.pitch>=-75 && camera.distance>.35f,"camera bounds");
check(camera.target.x!=before.x || camera.target.y!=before.y,"camera panning must move target");
std::cout<<"Desktop layout, spring pedal, keyboard travel and orbit camera checks passed\n";
}

View File

@@ -1,13 +1,16 @@
#include "FluidSim/Atmosphere.hpp" #include "FluidSim/Atmosphere.hpp"
#include "FluidSim/Crankcase.hpp" #include "FluidSim/Crankcase.hpp"
#include "FluidSim/Fluid.hpp" #include "FluidSim/Fluid.hpp"
#include "FluidSim/Junction.hpp"
#include "FluidSim/Solver.hpp" #include "FluidSim/Solver.hpp"
#include <cmath> #include <cmath>
#include <complex>
#include <functional> #include <functional>
#include <iostream> #include <iostream>
#include <string> #include <string>
using namespace fluidsim::port; using namespace fluidsim::port;
namespace { namespace {
PipeSolver selected_solver=PipeSolver::Rusanov;
void check(bool b, const std::string &s) { void check(bool b, const std::string &s) {
if (!b) if (!b)
throw std::runtime_error(s); throw std::runtime_error(s);
@@ -19,6 +22,7 @@ void set(Pipe1D &p, int i, float pressure, float rho, float u = 0, float air = 1
p.cells[i] = {rho, rho * u, pressure / pipe_gm1 + .5f * rho * u * u, air, fuel}; p.cells[i] = {rho, rho * u, pressure / pipe_gm1 + .5f * rho * u * u, air, fuel};
} }
void lossless(Pipe1D &p) { void lossless(Pipe1D &p) {
p.solver=selected_solver;
p.damping_multiplier = 0; p.damping_multiplier = 0;
p.energy_relaxation_rate = 0; p.energy_relaxation_rate = 0;
} }
@@ -36,7 +40,8 @@ double energy(const Pipe1D &p) {
} }
double vol_energy(const Volume0D &v) { return v.pressure() * v.volume() / (v.gamma - 1); } double vol_energy(const Volume0D &v) { return v.pressure() * v.volume() / (v.gamma - 1); }
} // namespace } // namespace
int main() { int main(int argc,char** argv) {
if(argc>1) selected_solver=parse_pipe_solver(argv[1]);
int failed = 0, total = 0; int failed = 0, total = 0;
auto test = [&](const char *n, std::function<void()> fn) { auto test = [&](const char *n, std::function<void()> fn) {
++total; ++total;
@@ -48,6 +53,163 @@ int main() {
std::cout << "FAIL " << n << ": " << e.what() << '\n'; std::cout << "FAIL " << n << ": " << e.what() << '\n';
} }
}; };
test("solver flux consistency and reflection symmetry", [] {
for(auto solver : {PipeSolver::Rusanov,PipeSolver::Hlle,PipeSolver::Hllc}) {
GasState a{1.2f,30,101325,.8f,.05f}, b{.5f,-10,60000,.1f,.3f};
const auto equal=Pipe1D::numerical_flux(a,a,solver);
near(equal.m,a.rho*a.velocity,1e-5,"uniform mass flux");
near(equal.p,a.rho*a.velocity*a.velocity+a.pressure,.02,"uniform momentum flux");
const auto forward=Pipe1D::numerical_flux(a,b,solver);
a.velocity=-a.velocity; b.velocity=-b.velocity;
const auto reverse=Pipe1D::numerical_flux(b,a,solver);
near(forward.m,-reverse.m,.0001,"orientation mass symmetry");
near(forward.p,reverse.p,.1,"orientation momentum symmetry");
near(forward.e,-reverse.e,1,"orientation energy symmetry");
near(forward.y,-reverse.y,.0001,"orientation air symmetry");
near(forward.fuel,-reverse.fuel,.0001,"orientation fuel symmetry");
}
});
test("HLLC preserves stationary contact and falls back at expansion vacuum", [] {
GasState a{1,0,1,.8f,.1f}, b{.125f,0,1,.1f,.5f};
auto f=Pipe1D::numerical_flux(a,b,PipeSolver::Hllc);
near(f.m,0,1e-7,"stationary contact mass diffusion");
near(f.e,0,1e-7,"stationary contact energy diffusion");
near(f.y,0,1e-7,"stationary contact tracer diffusion");
a.velocity=-10; b.velocity=10;
bool fallback=false;
f=Pipe1D::numerical_flux(a,b,PipeSolver::Hllc,std::sqrt(1.4f),std::sqrt(1.4f/.125f),&fallback);
check(fallback && std::isfinite(f.e),"strong expansion did not use safe HLLE flux");
});
test("unflanged radiation reflection magnitude and phase", [] {
for (double ka : {.1, .5, 1., 2.}) {
OpenEndRadiation reflection;
std::complex<double> input{}, output{};
double t = 0;
const double dt = 2*3.141592653589793/(ka*4096);
for (int i=0; i<4096*32; ++i) {
t += dt;
const double incident = std::cos(ka*t);
const double reflected = reflection.process(incident, dt);
if (i >= 4096*16) {
auto phase = std::exp(std::complex<double>(0, -ka*t));
input += incident*phase;
output += reflected*phase;
}
}
constexpr double n=.167, d=n+2*.6133, e=(d*d-n*n-1)/2;
const std::complex<double> z(0,ka);
const auto expected = -(1.+n*z)/(1.+d*z+e*z*z);
near(std::abs(output/input-expected), 0, .001, "radiation amplitude/phase mismatch");
check(std::abs(output/input) < 1, "radiation boundary creates acoustic energy");
}
});
test("N-port junction conserves sealed network mass and energy", [] {
std::vector<Pipe1D> pipes;
for(int i=0;i<3;++i) { pipes.emplace_back(.001f*(i+1),.3f,10);lossless(pipes.back()); }
for(int i=0;i<10;++i) set(pipes[0],i,120000,1.4f);
BoundarySystem boundaries(pipes);
Junction junction(.0003f);
const std::array<JunctionEnd,3> ends{{{0,false},{1,true},{2,true}}};
junction.connect(boundaries,pipes,ends);
boundaries.add_closed_end(0,true);boundaries.add_closed_end(1,false);boundaries.add_closed_end(2,false);
Solver solver(pipes,boundaries);solver.add_component(junction);
auto totals=[&] { std::array<double,2> t{junction.mass(),junction.internal_energy()};
for(auto& p:pipes) { t[0]+=mass(p);t[1]+=energy(p); } return t; };
const auto before=totals();
for(int i=0;i<1000;++i) solver.step(1.f/44100);
const auto after=totals();
if(std::abs(after[0]-before[0])>before[0]*2e-6) std::cerr << "mass relative drift=" << (after[0]-before[0])/before[0] << "\n";
near(after[0],before[0],before[0]*2e-6,"junction mass drift");
near(after[1],before[1],before[1]*2e-6,"junction energy drift");
check(pipes[1].state(0).pressure>101325,"junction did not transmit pressure to branch");
for(auto& p:pipes) check(p.positivity_repairs==0,"junction required positivity repairs");
});
test("Zero-storage N-port junction conserves sealed network mass and energy", [] {
std::vector<Pipe1D> pipes;
for(int i=0;i<3;++i) { pipes.emplace_back(.001f*(i+1),.3f,10);lossless(pipes.back()); }
for(int i=0;i<10;++i) set(pipes[0],i,120000,1.4f);
BoundarySystem boundaries(pipes);
boundaries.add_junction({{0,false},{1,true},{2,true}});
boundaries.add_closed_end(0,true);boundaries.add_closed_end(1,false);boundaries.add_closed_end(2,false);
Solver solver(pipes,boundaries);
auto totals=[&] { std::array<double,2> t{0,0};
for(auto& p:pipes) { t[0]+=mass(p);t[1]+=energy(p); } return t; };
const auto before=totals();
for(int i=0;i<1000;++i) solver.step(1.f/44100);
const auto after=totals();
// Float cell updates accumulate roundoff over 4000 substeps (50 ppm bound).
near(after[0],before[0],before[0]*5e-5,"junction mass drift");
near(after[1],before[1],before[1]*5e-5,"junction energy drift");
check(pipes[1].state(0).pressure>101325,"junction did not transmit pressure to branch");
for(auto& p:pipes) check(p.positivity_repairs==0,"junction required positivity repairs");
});
test("radiating acoustic volume source is independent of donor density", [] {
for (bool left : {false,true}) {
std::vector<Pipe1D> hot, cold;
hot.emplace_back(.001f,.1f,1); cold.emplace_back(.001f,.1f,1);
BoundarySystem a(hot), b(cold);
a.add_radiating_open_end(0,left,101325,.001f);
b.add_radiating_open_end(0,left,101325,.001f);
for(int i=0;i<4096;++i) {
const float velocity=2*std::sin(2*pi*150*i/44100);
set(hot[0],0,101325,.5f,velocity);
set(cold[0],0,101325,ambient_density,velocity);
a.resolve(1.f/44100); b.resolve(1.f/44100);
near(a.open_end_volume_flow(0),b.open_end_volume_flow(0),2e-7,
"temperature change contaminated acoustic volume source");
}
}
});
test("radiating mouth conserves reported mass at both orientations", [] {
for (bool left : {false,true}) {
std::vector<Pipe1D> p;
p.emplace_back(.001f,.1f,10);
lossless(p[0]);
for (int i=0;i<10;++i) set(p[0],i,105000,1.2f);
BoundarySystem b(p);
b.add_radiating_open_end(0,left,101325,.001f);
b.add_closed_end(0,!left);
const double initial = mass(p[0]);
b.resolve(1e-6f);
const double exchange = -b.open_end_mass_flow(0)*1e-6;
p[0].advance(1e-6f);
near(mass(p[0])-initial,exchange,1e-11,"radiating mass flux mismatch");
}
});
test("radiating mouth reflects rarefaction at either pipe orientation", [] {
for (bool left : {false,true}) {
std::vector<Pipe1D> p;
p.emplace_back(.001f,1,400);
lossless(p[0]);
const double c=std::sqrt(1.4*101325/1.2);
for(int i=0;i<400;++i) {
double x=(i+.5)/400.;
if(left) x=1-x;
float dp=100*std::exp(-std::pow((x-.3)/.04,2));
set(p[0],i,101325+dp,1.2f+dp/(c*c),(left?-1:1)*dp/(1.2*c));
}
BoundarySystem b(p);
b.add_closed_end(0,!left);
b.add_radiating_open_end(0,left,101325,.001f);
Solver solver(p,b); solver.minimum_substeps=1;
for(int i=0;i<int(1.4/c*44100);++i) solver.step(1.f/44100);
check(p[0].state(left?280:119).pressure < 101305,"open mouth did not return an inverted pressure pulse");
check(p[0].positivity_repairs==0,"radiation boundary required positivity repair");
}
});
test("radiating inflow supplies fresh ambient stagnation enthalpy", [] {
std::vector<Pipe1D> p;
p.emplace_back(.001f,.1f,10);
for(int i=0;i<10;++i) set(p[0],i,99000,1.2f,0,.1f,.2f);
BoundarySystem b(p);
b.add_radiating_open_end(0,true,101325,.001f,300);
b.add_closed_end(0,false);
b.resolve(1e-6f);
const auto face=p[0].left_ghost;
check(face.velocity>0 && face.air==1 && face.fuel==0,"backflow supplied exhaust or fuel");
near(fluidsim::port::gamma/(fluidsim::port::gamma-1)*face.pressure/face.rho+.5*face.velocity*face.velocity,
fluidsim::port::gamma/(fluidsim::port::gamma-1)*gas_constant*300, .1,"ambient enthalpy mismatch");
});
test("tracer underflow preserves bulk gas and normal concentrations", [] { test("tracer underflow preserves bulk gas and normal concentrations", [] {
for (float fraction : {1e-40f, 1e-30f, .5f}) { for (float fraction : {1e-40f, 1e-30f, .5f}) {
Pipe1D p(.001f, 1, 8); Pipe1D p(.001f, 1, 8);

View File

@@ -34,8 +34,6 @@ int main() {
"fresh charge heats progressively after repeated blips"); "fresh charge heats progressively after repeated blips");
check(inlet_temperature > 280 && inlet_temperature < 320, check(inlet_temperature > 280 && inlet_temperature < 320,
"inlet no longer supplies near-ambient fresh air"); "inlet no longer supplies near-ambient fresh air");
check(std::abs(s.intake_atmosphere.port.temperature - 300) < .01f, "reservoir temperature drift");
check(std::abs(s.intake_atmosphere.port.air_fraction - 1) < 1e-6, "reservoir admits exhaust composition");
run(s, 7, 0); run(s, 7, 0);
const auto recovered = s.snapshot(); const auto recovered = s.snapshot();
check(recovered.rpm > 1000 && recovered.rpm < 2600, "engine did not recover idle after second blip"); check(recovered.rpm > 1000 && recovered.rpm < 2600, "engine did not recover idle after second blip");

View File

@@ -9,8 +9,10 @@
#include "FluidSim/OutdoorExhaustReverb.hpp" #include "FluidSim/OutdoorExhaustReverb.hpp"
#include "FluidSim/HelmholtzScenario.hpp" #include "FluidSim/HelmholtzScenario.hpp"
#include "FluidSim/PulseTestScenario.hpp" #include "FluidSim/PulseTestScenario.hpp"
#include "FluidSim/ThreadLoadTracker.hpp"
#include "FluidSim/Units.hpp" #include "FluidSim/Units.hpp"
#include "FluidSim/Desktop/PedalControl.hpp"
#include "FluidSim/Desktop/PipePath.hpp"
#include "FluidSim/Desktop/PressurePalette.hpp"
#include <cmath> #include <cmath>
#include <iostream> #include <iostream>
@@ -28,7 +30,64 @@ void require(bool condition, const char* message) {
int main() { int main() {
using namespace fluidsim::port; using namespace fluidsim::port;
Ecu fixed_idle;
fixed_idle.idle_control=Ecu::IdleControl::FixedOpening;
fixed_idle.idle_base_opening=.049f;
fixed_idle.overrun_fuel_cut_enabled=false;
for(float rpm : {0.f,500.f,3000.f,9000.f}) {
for(int i=0;i<4410;++i) fixed_idle.step(1.f/44100,0,rpm);
require(std::abs(fixed_idle.throttle_opening()-.049f)<1e-5f,
"fixed carb idle must not vary with RPM");
}
for(int i=0;i<4410;++i) fixed_idle.step(1.f/44100,.2f,3000);
require(std::abs(fixed_idle.throttle_opening()-.2f)<1e-5f,"pedal must open above carb stop");
for(int i=0;i<4410;++i) fixed_idle.step(1.f/44100,.01f,3000);
require(std::abs(fixed_idle.throttle_opening()-.049f)<1e-5f,"release must return to carb stop");
for(auto preset : {FourStrokePreset::Twin,FourStrokePreset::SaabB205,FourStrokePreset::AmericanV8}) {
FourStrokeScenario starter(44100,preset);
starter.crankshaft.omega=0;
starter.set_starter(true);
starter.process();
require(starter.crankshaft.omega>0,"starter must turn every four-stroke crank from rest");
starter.set_starter(false);
require(!starter.starter_engaged,"starter release must disengage");
}
{
TwoStrokeConfig config; config.idle_slide_opening=.071f;
TwoStrokeScenario starter(44100,config);
require(starter.ecu.idle_base_opening==.071f,"scenario must configure carb idle stop");
starter.crankshaft.omega=0; starter.set_starter(true); starter.process();
require(starter.crankshaft.omega>0,"starter must turn two-stroke crank from rest");
starter.set_starter(false);
}
desktop::PedalControl pedal;
for(int i=0;i<100;++i)pedal.advance(.0025f,true,false);
require(std::abs(pedal.value()-.5f)<1e-5f,"W pedal travel must be halfway after 0.25 seconds");
for(int i=0;i<100;++i)pedal.advance(.0025f,true,false);
require(std::abs(pedal.value()-1)<1e-5f,"W must reach full pedal in 0.5 seconds");
for(int i=0;i<200;++i)pedal.advance(.0025f,false,false);
require(pedal.value()<1e-5f,"keyboard release must return pedal in 0.5 seconds");
pedal.set(.8f);
for(int i=0;i<200;++i)pedal.advance(.0025f,false,true);
require(pedal.value()==0,"S must release pedal");
desktop::PipePath path({{0,{0,0,0}},{.25f,{.2f,.1f,0}},{1,{1,0,.1f}}});
auto knot=path.position(.25f),direction=path.tangent(.25f);
require(std::abs(knot.x-.2f)<1e-6f && std::abs(knot.y-.1f)<1e-6f,"pipe path must interpolate its metre-referenced control point");
require(std::abs(direction.x*direction.x+direction.y*direction.y+direction.z*direction.z-1)<1e-4f,"pipe path tangent must be normalized");
auto atm=desktop::pressure_tint(101325),vac=desktop::pressure_tint(0),peak=desktop::pressure_tint(1000000);
require(atm.y>.8f && vac.z>.99f && peak.x>.99f,"pressure palette must show atmosphere green, vacuum blue, spikes red");
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");
for(int i=0;i<230;++i)fast_actuator.step(1.f/44100,0,5000);
require(fast_actuator.throttle_opening()<.01f,"throttle actuator must close within about 5 ms");
Ecu idle_actuator;
for(int i=0;i<441;++i)idle_actuator.step(1.f/44100,0,1000);
require(idle_actuator.idle_active() && idle_actuator.throttle_opening()>0,"zero UI pedal must retain ECU idle authority");
Ecu controller; Ecu controller;
controller.throttle_time_constant=.08f;controller.throttle_rate=2;
constexpr float ecu_dt = 1.0f / 44100.0f; constexpr float ecu_dt = 1.0f / 44100.0f;
controller.step(ecu_dt, 1.0f, 2000.0f); controller.step(ecu_dt, 1.0f, 2000.0f);
require(controller.throttle_opening() > 0.0f && controller.throttle_opening() <= 2.0f * ecu_dt, require(controller.throttle_opening() > 0.0f && controller.throttle_opening() <= 2.0f * ecu_dt,
@@ -98,21 +157,6 @@ int main() {
"dyno bins must retain their peak power and torque"); "dyno bins must retain their peak power and torque");
dyno_curve.reset(); dyno_curve.reset();
require(dyno_curve.bins().empty(), "dyno reset must clear accumulated history"); require(dyno_curve.bins().empty(), "dyno reset must clear accumulated history");
RadiationSource propagation;
int first_arrival = -1;
for (int i = 0; i < 240; ++i) {
const auto frame = propagation.process(i == 0 ? 0.001f : 0.0f);
if (first_arrival < 0 && (frame.left != 0.0f || frame.right != 0.0f)) first_arrival = i;
}
require(first_arrival >= 190 && first_arrival <= 195,
"1.5 m propagation must delay sound about 193 samples at 44.1 kHz");
ThreadLoadTracker thread_load;
thread_load.record(1.0, 2.0);
require(std::abs(thread_load.load_percent() - 2.5) < 1e-9 &&
std::abs(thread_load.last_step_microseconds() - 1000.0) < 1e-9,
"thread load tracker must report EMA budget use and last-step time");
Crankshaft crank; Crankshaft crank;
FourStrokeCylinder cylinder; FourStrokeCylinder cylinder;
cylinder.initialize(crank); cylinder.initialize(crank);
@@ -264,14 +308,11 @@ int main() {
require(idle_state.throttle > 0.0f && idle_state.pedal == 0.0f, require(idle_state.throttle > 0.0f && idle_state.pedal == 0.0f,
"idle control must open the butterfly independently of the pedal"); "idle control must open the butterfly independently of the pedal");
scenario.toggle_mechanical_sound();
for (int i = 0; i < 1000; ++i) { for (int i = 0; i < 1000; ++i) {
const AudioFrame frame = scenario.process(); const AudioFrame frame = scenario.process();
require(std::isfinite(frame.left) && std::isfinite(frame.right), require(std::isfinite(frame.left) && std::isfinite(frame.right),
"the integrated mechanical/reverb audio path must remain finite"); "the integrated fluid-derived audio path must remain finite");
} }
require(scenario.snapshot().mechanical_sound_enabled,
"mechanical-noise toggle must be reflected in the scenario snapshot");
std::cout << "FluidSim C++ physics tests passed\n"; std::cout << "FluidSim C++ physics tests passed\n";
return 0; return 0;

View File

@@ -1,46 +0,0 @@
#include "FluidSim/Scenario.hpp"
#include <cmath>
#include <iostream>
#include <stdexcept>
using namespace fluidsim::port;
void check(bool ok, const char* message) { if (!ok) throw std::runtime_error(message); }
int main() {
Ecu ecu;
ecu.boost_control_enabled = true;
ecu.step(.001f, 1, 3500);
for (int i=0;i<1000;++i) ecu.control_boost(.001f,1,3500,110000,115000);
check(ecu.wastegate_duty()>0 && ecu.wastegate_duty()<=.85f,"no spool duty");
ecu.control_boost(.001f,1,3500,190000,190000);
check(ecu.fuel_cut() && ecu.wastegate_duty()==0,"overboost protection failed");
ecu.control_boost(.001f,1,3500,140000,140000);
check(!ecu.fuel_cut(),"overboost recovery failed");
ecu.control_boost(.001f,0,3500,100000,100000);
check(ecu.wastegate_duty()==0 && ecu.boost_target()==0,"lift-off did not restore spring control");
ecu.control_boost(.001f,1,3500,NAN,100000);
check(ecu.fuel_cut() && ecu.wastegate_duty()==0,"boost sensor fail-safe failed");
FourStrokeScenario s(44100,FourStrokePreset::SaabB205,true);
check(s.pipes.size()==12 && s.turbo && s.ecu.boost_control_enabled,"Saab turbo topology missing");
double peak_rpm=0, peak_map=0, peak_audio=0, settled_pressure=0;
for(int second=0;second<10;++second) {
for(int i=0;i<44100;++i) {
s.throttle = second>=1 && second<7 ? 1.f : 0.f;
if(second<7) s.crankshaft.omega=3500*2*pi/60;
const auto a=s.process();
if(second>=4 && second<7) settled_pressure+=s.intake_plenum.pressure()/(3.*44100);
check(std::isfinite(a.left) && std::isfinite(a.right),"nonfinite audio");
peak_audio=std::max(peak_audio,double(std::abs(a.left)));
}
auto t=s.turbo->snapshot();
peak_rpm=std::max(peak_rpm,t.rpm);
peak_map=std::max(peak_map,double(s.intake_plenum.pressure()));
check(std::isfinite(t.rpm) && std::abs(t.energy_residual)<.1,"turbo energy balance failed");
check(t.limited_transfers==0,"housing donor limit hit");
std::cout<<second+1<<" MAP="<<s.intake_plenum.pressure()/1000<<" charge="<<t.charge_pressure/1000
<<" rpm="<<t.rpm<<" duty="<<s.ecu.wastegate_duty()<<" gate_mm="<<t.wastegate_lift*1000<<'\n';
}
check(peak_rpm>50000 && peak_map>110000,"GT17 failed to spool/boost");
check(peak_audio<1,"audio exceeds monitoring range");
check(std::abs(settled_pressure-146325)<8000,"boost did not settle near target");
check(s.ecu.wastegate_duty()==0 && s.turbo->snapshot().rpm<peak_rpm,"lift-off control/spool-down failed");
for(auto& p:s.pipes) check(p.positivity_repairs==0,"pipe state repaired");
}

View File

@@ -0,0 +1,33 @@
#include "FluidSim/ScenarioCatalog.hpp"
#include "FluidSim/DesktopScenario.hpp"
#include <cmath>
#include <chrono>
#include <iostream>
#include <set>
#include <stdexcept>
using namespace fluidsim::port;
int main() {
std::set<std::string> ids;
for(const auto& example:scenario_examples()) {
if(!ids.insert(example.id).second)throw std::runtime_error("duplicate example id");
DesktopScenario scenario(example.kind,example.two,example.exhaust,example.v8,example.engine,example.title);
scenario.set_audio_scene(OutdoorScene::Dry);
const auto started=std::chrono::steady_clock::now();
float peak_power=0;
for(int i=0;i<88200;++i) {
scenario.set_controls(i<44100?.45f:0.f,0,1);
const auto audio=scenario.process();
if(i%4410==0) peak_power=std::max(peak_power,scenario.snapshot().average_power);
if(!std::isfinite(audio.left)||!std::isfinite(audio.right))throw std::runtime_error(example.id+" nonfinite audio");
}
const auto snapshot=scenario.snapshot();
if(snapshot.pipe_positivity_repairs)throw std::runtime_error(example.id+" required positivity repairs");
if(example.id=="big-carb" && peak_power<10000)throw std::runtime_error("big carb V8 failed to produce power");
if(!std::isfinite(snapshot.rpm)||snapshot.rpm<=0)throw std::runtime_error(example.id+" invalid engine speed");
if(example.engine.cylinders && (snapshot.cylinder_count!=example.engine.cylinders || snapshot.layout!=example.engine.layout))throw std::runtime_error("custom cylinder layout lost");
if(snapshot.example_name!=example.title)throw std::runtime_error("example name lost");
if(example.kind==ScenarioKind::AmericanV8 && snapshot.h_pipe!=example.v8.h_pipe)throw std::runtime_error("example crossover selection lost");
if(example.kind==ScenarioKind::SaabB205 && snapshot.silencers!=example.exhaust.enabled)throw std::runtime_error("example silencer selection lost");
std::cout<<example.id<<" passed, rpm="<<snapshot.rpm<<", wall/sim="<<std::chrono::duration<double>(std::chrono::steady_clock::now()-started).count()/2<<std::endl;
}
}

View File

@@ -1,115 +0,0 @@
#include "FluidSim/Turbocharger.hpp"
#include "FluidSim/Atmosphere.hpp"
#include "FluidSim/Solver.hpp"
#include <iostream>
#include <stdexcept>
using namespace fluidsim::port;
namespace {
void check(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); }
double mass(const Turbocharger& t) {
return double(t.compressor_inlet.mass())+t.compressor_outlet.mass()+t.turbine_inlet.mass()+t.turbine_outlet.mass();
}
double energy(const Turbocharger& t) { return t.fluid_energy()+t.shaft_energy()+t.snapshot().bearing_heat; }
void closed_balance(double initial_rpm) {
Turbocharger t;
t.set_initial_rpm(initial_rpm);
t.turbine_inlet.set_pressure(250000, 900);
const double before = energy(t), initial_mass = mass(t);
for (int i = 0; i < 10000; ++i) t.update_state(1e-6f);
check(std::abs(mass(t)-initial_mass) < initial_mass*3e-5, "turbo created/lost gas mass");
check(std::abs(energy(t)-before) < before*2e-4, "fluid + shaft + bearing heat energy drift");
check(t.omega() > 0, "turbine did not start its shaft from pressure energy");
const auto s = t.snapshot();
check(std::abs(s.energy_residual) < before*2e-4, "turbo energy residual excessive");
std::cout << "Closed balance initial_rpm=" << initial_rpm << " mass_error=" << mass(t)-initial_mass
<< " energy_error=" << energy(t)-before << " J\n";
}
double turbine_flow(float downstream) {
Turbocharger t;
t.turbine_inlet.set_pressure(300000, 900);
t.turbine_outlet.set_pressure(downstream, 600);
t.update_state(1e-7f);
return t.snapshot().turbine_flow;
}
}
int main() {
Turbocharger rest;
const double e = energy(rest);
for (int i = 0; i < 1000; ++i) rest.update_state(1.f/44100);
check(rest.omega() == 0 && energy(rest) == e && rest.snapshot().compressor_pressure_ratio == 1,
"stationary equilibrium generated boost/shaft energy");
closed_balance(0);
closed_balance(120000);
check(std::abs(turbine_flow(100000)-turbine_flow(50000)) < 1e-10, "choked turbine flow depends on back pressure");
check(turbine_flow(250000) < turbine_flow(100000), "higher backpressure did not reduce subcritical flow");
Turbocharger reverse;
reverse.compressor_outlet.set_pressure(150000, 350);
reverse.turbine_outlet.set_pressure(150000, 600);
const auto reverse_energy = energy(reverse);
reverse.update_state(1e-6f);
check(reverse.snapshot().compressor_flow < 0 && reverse.snapshot().turbine_flow < 0,
"turbo acted as an artificial one-way valve");
check(std::abs(energy(reverse)-reverse_energy) < reverse_energy*1e-6, "reverse flow creates energy");
Turbocharger gate;
gate.compressor_outlet.set_pressure(220000, 350);
gate.turbine_inlet.set_pressure(250000, 900);
gate.update_state(1e-6f);
check(gate.snapshot().wastegate_lift > 0 && gate.snapshot().wastegate_flow > 0,
"pressure can did not open the wastegate bypass");
check(std::abs(gate.snapshot().energy_residual) < 1e-4, "wastegate bypass created energy");
Turbocharger coarse;
coarse.set_initial_rpm(200000);
const double coarse_energy = energy(coarse);
coarse.update_state(.5f);
check(coarse.omega() >= 0 && std::abs(energy(coarse)-coarse_energy) < coarse_energy*1e-5,
"large turbo timestep creates energy or reverses shaft");
bool caught = false;
try { TurboConfig bad; bad.shaft_inertia = 0; Turbocharger invalid(bad); }
catch (const std::invalid_argument&) { caught = true; }
check(caught, "zero rotor inertia accepted");
// Hot pulsing reservoir -> real pipes -> turbine -> exhaust reservoir.
// Compressor feeds a pressure reservoir representing a loaded charge duct.
std::vector<Pipe1D> pipes;
for (int i = 0; i < 4; ++i) pipes.emplace_back(pi*.024f*.024f*.25f, .08f, 8);
Atmosphere fresh, charge, hot, exhaust;
charge.pressure = 130000;
hot.temperature = 900;
hot.pressure = 220000;
charge.update_state(0); hot.update_state(0);
BoundarySystem boundaries(pipes);
Turbocharger turbo;
turbo.connect(boundaries, pipes, 0, false, 1, true, 2, false, 3, true);
std::array<float, 4> areas;
const std::array<Atmosphere*, 4> reservoirs{&fresh, &charge, &hot, &exhaust};
for (int i = 0; i < 4; ++i) {
areas[i] = pipes[i].area;
boundaries.add_orifice(reservoirs[i]->port, i, i%2 == 0, areas[i]);
}
Solver solver(pipes, boundaries);
solver.add_component(turbo);
float minimum_power = 1e9f, maximum_power = -1e9f;
double initial_speed = 0;
for (int i = 0; i < 44100; ++i) {
hot.pressure = 220000+60000*std::sin(2*pi*80*i/44100);
hot.update_state(0);
solver.step(1.f/44100);
const auto s = turbo.snapshot();
if (i == 100) initial_speed = s.rpm;
if (i > 40000) {
minimum_power = std::min(minimum_power, float(s.turbine_power));
maximum_power = std::max(maximum_power, float(s.turbine_power));
}
}
const auto result = turbo.snapshot();
check(result.rpm > initial_speed+10000, "hot gas did not spool the coupled turbo");
check(result.compressor_flow > 0 && result.compressor_power > 0 && result.charge_temperature > 300,
"loaded compressor did not deliver heated charge using shaft work");
check(maximum_power-minimum_power > 100, "exhaust pulses were averaged away before turbine torque");
for (const auto& pipe : pipes) check(pipe.positivity_repairs == 0, "turbo bench required pipe repairs");
check(result.limited_transfers == 0, "normal turbo bench depleted a storage node");
std::cout << "Turbo pipe bench passed: rpm=" << result.rpm << " compressor flow=" << result.compressor_flow
<< " kg/s, charge T=" << result.charge_temperature << " K, turbine power range="
<< minimum_power << ".." << maximum_power << " W, residual=" << result.energy_residual << " J\n";
}

View File

@@ -1,43 +0,0 @@
#include "FluidSim/Scenario.hpp"
#include <algorithm>
#include <cmath>
#include <iostream>
#include <stdexcept>
using namespace fluidsim::port;
namespace {
void check(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); }
}
int main() {
TurboTwinScenario s;
double maximum_speed = 0, maximum_charge = 0, maximum_turbine_pressure = 0;
double maximum_gate = 0, peak_audio = 0, coast_speed = 0;
for (int sample = 0; sample < 7*44100; ++sample) {
// Idle, accelerate/limiter operation, abrupt pedal release, then recover.
s.set_controls(sample >= 44100 && sample < 4*44100 ? .7f : 0, 0, 1);
const auto audio = s.process();
check(std::isfinite(audio.left) && std::isfinite(audio.right), "nonfinite turbo twin audio");
peak_audio = std::max({peak_audio, double(std::abs(audio.left)), double(std::abs(audio.right))});
const auto t = s.turbo->snapshot();
check(t.rpm >= 0 && std::isfinite(t.rpm) && t.charge_temperature > 0 &&
t.turbine_inlet_pressure > 0 && t.turbine_outlet_pressure > 0,
"turbo twin developed nonphysical turbo state");
maximum_speed = std::max(maximum_speed, t.rpm);
maximum_charge = std::max(maximum_charge, t.charge_pressure);
maximum_turbine_pressure = std::max(maximum_turbine_pressure, t.turbine_inlet_pressure);
maximum_gate = std::max(maximum_gate, t.wastegate_lift);
if (sample == 5*44100) coast_speed = t.rpm;
}
const auto final = s.turbo->snapshot();
check(maximum_speed > 50000 && maximum_charge > 120000, "turbo did not spool and generate boost");
check(maximum_turbine_pressure > maximum_charge, "twin has no turbine backpressure");
check(maximum_gate > 0, "wastegate never opened under boost");
check(final.rpm < coast_speed, "turbo did not coast down after throttle release");
check(final.limited_transfers == 0, "turbo twin depleted its internal volumes");
check(std::abs(final.energy_residual) < .1, "turbo twin accumulated excessive energy residual");
check(peak_audio < 1, "turbo twin audio exceeds device range");
for (const auto& p : s.pipes) check(p.positivity_repairs == 0, "turbo twin required pipe repairs");
std::cout << "Turbo twin passed: max_rpm=" << maximum_speed << ", max_charge=" << maximum_charge/1000
<< " kPa, max_turbine_pressure=" << maximum_turbine_pressure/1000 << " kPa, gate=" << maximum_gate*1000
<< " mm, final_rpm=" << final.rpm << ", peak_audio=" << peak_audio
<< ", energy_residual=" << final.energy_residual << " J\n";
}

View File

@@ -53,12 +53,11 @@ int main() {
std::to_string(maximum_rpm) + ", idle=" + std::to_string(final.rpm)); std::to_string(maximum_rpm) + ", idle=" + std::to_string(final.rpm));
if (!reed_open_seen || !reed_closed_seen) if (!reed_open_seen || !reed_closed_seen)
throw std::runtime_error("Reed did not open and close"); throw std::runtime_error("Reed did not open and close");
if (final.rpm < 2000 || final.rpm > 4000) if (std::abs(final.throttle-scenario.configuration.idle_slide_opening)>1e-5f)
throw std::runtime_error("Two-stroke idle did not recover after throttle release: " + throw std::runtime_error("Two-stroke did not return to its configured fixed carb stop");
std::to_string(final.rpm) + " RPM, coast firings=" + if (final.rpm < 300 || final.rpm >= release_rpm)
std::to_string(one_second_coast_firings) + ", carb AFR=" + throw std::runtime_error("Two-stroke failed to coast down and remain running at fixed idle stop: " +
std::to_string(scenario.carburetor.indicated_afr()) + std::to_string(final.rpm));
", cylinder fuel=" + std::to_string(scenario.cylinder.fuel_mass));
if (peak >= 1) if (peak >= 1)
throw std::runtime_error("Two-stroke audio exceeds device range"); throw std::runtime_error("Two-stroke audio exceeds device range");
for (const auto &pipe : scenario.pipes) for (const auto &pipe : scenario.pipes)
@@ -97,7 +96,7 @@ int main() {
if (pipe.positivity_repairs != 0) if (pipe.positivity_repairs != 0)
throw std::runtime_error("Motorcycle launch required pipe positivity repairs"); throw std::runtime_error("Motorcycle launch required pipe positivity repairs");
std::cout << "Two-stroke regression passed: peak=" << peak << ", maximum RPM=" << maximum_rpm std::cout << "Two-stroke regression passed: peak=" << peak << ", maximum RPM=" << maximum_rpm
<< ", recovered RPM=" << final.rpm << ", bike speed=" << scenario.vehicle.speed * 3.6f << ", post-release RPM=" << final.rpm << ", bike speed=" << scenario.vehicle.speed * 3.6f
<< " km/h, release RPM=" << release_rpm << " km/h, release RPM=" << release_rpm
<< ", first coast-second firings=" << one_second_coast_firings << '\n'; << ", first coast-second firings=" << one_second_coast_firings << '\n';
} }

View File

@@ -65,17 +65,23 @@ struct Metrics {
} }
}; };
template <class Scenario> template <class Scenario>
void capture(const std::string &prefix, int frames, float pedal, OutdoorScene scene, TwoStrokeConfig config) { void capture(const std::string &prefix, int frames, float pedal, OutdoorScene scene, TwoStrokeConfig config, PipeSolver solver, float fixed_rpm, float warmup) {
std::unique_ptr<Scenario> s; std::unique_ptr<Scenario> s;
if constexpr (std::is_same_v<Scenario, TwoStrokeScenario>) s = std::make_unique<Scenario>(44100, config); if constexpr (std::is_same_v<Scenario, TwoStrokeScenario>) s = std::make_unique<Scenario>(44100, config);
else s = std::make_unique<Scenario>(); else s = std::make_unique<Scenario>();
s->set_pipe_solver(solver);
s->throttle = pedal; s->throttle = pedal;
s->set_audio_scene(scene); s->set_audio_scene(scene);
for(int i=0; i<int(std::lround(warmup*44100)); ++i) {
if(fixed_rpm>0) s->crankshaft.omega=fixed_rpm*2*pi/60;
s->process();
}
Wav rendered(prefix + "-rendered.wav", frames), dry(prefix + "-dry.wav", frames), Wav rendered(prefix + "-rendered.wav", frames), dry(prefix + "-dry.wav", frames),
raw(prefix + "-unfiltered.wav", frames); raw(prefix + "-unfiltered.wav", frames);
Metrics m, d, r; Metrics m, d, r;
int cuts = 0; int cuts = 0;
for (int i = 0; i < frames; i++) { for (int i = 0; i < frames; i++) {
if(fixed_rpm>0) s->crankshaft.omega=fixed_rpm*2*pi/60;
auto f = s->process(), df = s->dry_audio(), rf = s->unfiltered_audio(); auto f = s->process(), df = s->dry_audio(), rf = s->unfiltered_audio();
m.add(f); m.add(f);
d.add(df); d.add(df);
@@ -96,10 +102,9 @@ void capture(const std::string &prefix, int frames, float pedal, OutdoorScene sc
int main(int argc, char **argv) { int main(int argc, char **argv) {
try { try {
std::string scenario = "twin", prefix = "build/audio/twin", scene = "wall"; std::string scenario = "twin", prefix = "build/audio/twin", scene = "wall";
float seconds = 6, pedal = 1; float seconds = 6, pedal = 1, fixed_rpm = 0, warmup = 0;
TwoStrokeConfig config; TwoStrokeConfig config;
bool turbo_enabled = false; PipeSolver solver=PipeSolver::Rusanov;
std::string turbo_name;
for (int i = 1; i < argc; i++) { for (int i = 1; i < argc; i++) {
std::string key = argv[i]; std::string key = argv[i];
if (i + 1 >= argc) if (i + 1 >= argc)
@@ -113,11 +118,9 @@ int main(int argc, char **argv) {
seconds = std::stof(value); seconds = std::stof(value);
else if (key == "--throttle") else if (key == "--throttle")
pedal = std::stof(value); pedal = std::stof(value);
else if (key == "--turbo") { else if (key == "--warmup") warmup=std::stof(value);
if (value != "none" && value != "vz21" && value != "gt17") throw std::runtime_error("choose none, vz21 or gt17 turbo"); else if (key == "--rpm") fixed_rpm=std::stof(value);
turbo_name = value; else if (key == "--solver") solver=parse_pipe_solver(value);
turbo_enabled = value != "none";
}
else if (key == "--exhaust") { else if (key == "--exhaust") {
if (value == "racing") config.exhaust = TwoStrokeExhaust::Racing; if (value == "racing") config.exhaust = TwoStrokeExhaust::Racing;
else if (value == "straight") config.exhaust = TwoStrokeExhaust::Straight; else if (value == "straight") config.exhaust = TwoStrokeExhaust::Straight;
@@ -131,6 +134,9 @@ int main(int argc, char **argv) {
if (!std::isfinite(seconds) || seconds <= 0 || seconds > 600 || !std::isfinite(pedal) || pedal < 0 || if (!std::isfinite(seconds) || seconds <= 0 || seconds > 600 || !std::isfinite(pedal) || pedal < 0 ||
pedal > 1) pedal > 1)
throw std::runtime_error("invalid capture duration or throttle"); throw std::runtime_error("invalid capture duration or throttle");
if(!std::isfinite(fixed_rpm) || fixed_rpm<0 || fixed_rpm>30000)
throw std::runtime_error("invalid capture RPM");
if(!std::isfinite(warmup) || warmup<0 || warmup>600) throw std::runtime_error("invalid warmup");
OutdoorScene selected; OutdoorScene selected;
if (scene == "dry") if (scene == "dry")
selected = OutdoorScene::Dry; selected = OutdoorScene::Dry;
@@ -140,27 +146,21 @@ int main(int argc, char **argv) {
selected = OutdoorScene::Wall; selected = OutdoorScene::Wall;
else if (scene == "woodland") else if (scene == "woodland")
selected = OutdoorScene::Woodland; selected = OutdoorScene::Woodland;
else if (scene == "tunnel")
selected = OutdoorScene::Tunnel;
else else
throw std::runtime_error("invalid scene"); throw std::runtime_error("invalid scene");
if (!std::filesystem::path(prefix).parent_path().empty()) if (!std::filesystem::path(prefix).parent_path().empty())
std::filesystem::create_directories(std::filesystem::path(prefix).parent_path()); std::filesystem::create_directories(std::filesystem::path(prefix).parent_path());
const int frames = int(std::lround(seconds * 44100)); const int frames = int(std::lround(seconds * 44100));
if (turbo_name.empty() && scenario == "b205") turbo_enabled = true; if (scenario == "twin")
if (!turbo_name.empty() && turbo_name!="none" && capture<FourStrokeTwinScenario>(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup);
!((turbo_name=="vz21" && scenario=="twin") || (turbo_name=="gt17" && scenario=="b205")))
throw std::runtime_error("use gt17 with b205 or vz21 with twin");
if (scenario == "twin" && turbo_enabled)
capture<TurboTwinScenario>(prefix, frames, pedal, selected, config);
else if (scenario == "twin")
capture<FourStrokeTwinScenario>(prefix, frames, pedal, selected, config);
else if (scenario == "b205" && turbo_enabled)
capture<SaabTurboScenario>(prefix, frames, pedal, selected, config);
else if (scenario == "b205") else if (scenario == "b205")
capture<SaabB205Scenario>(prefix, frames, pedal, selected, config); capture<SaabB205Scenario>(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup);
else if (scenario == "two-stroke") else if (scenario == "two-stroke")
capture<TwoStrokeScenario>(prefix, frames, pedal, selected, config); capture<TwoStrokeScenario>(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup);
else if (scenario == "v8") else if (scenario == "v8")
capture<AmericanV8Scenario>(prefix, frames, pedal, selected, config); capture<AmericanV8Scenario>(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup);
else else
throw std::runtime_error("choose twin, two-stroke, b205 or v8"); throw std::runtime_error("choose twin, two-stroke, b205 or v8");
} catch (const std::exception &e) { } catch (const std::exception &e) {

View File

@@ -2,12 +2,31 @@
#include "FluidSim/TwoStrokeScenario.hpp" #include "FluidSim/TwoStrokeScenario.hpp"
#include "FluidSim/HelmholtzScenario.hpp" #include "FluidSim/HelmholtzScenario.hpp"
#include "FluidSim/PulseTestScenario.hpp" #include "FluidSim/PulseTestScenario.hpp"
#include <algorithm>
#include <chrono> #include <chrono>
#include <cmath>
#include <iostream> #include <iostream>
#include <limits> #include <limits>
#include <memory> #include <memory>
#include <numeric>
#include <stdexcept>
#include <string> #include <string>
#include <vector>
using namespace fluidsim::port; using namespace fluidsim::port;
static void set_uniform_pipe_resolution(std::vector<Pipe1D>& pipes, int cells_per_meter) {
if (cells_per_meter <= 0) throw std::invalid_argument("cells per meter must be positive");
for (auto& pipe : pipes) {
const float length = std::accumulate(pipe.cell_lengths.begin(), pipe.cell_lengths.end(), 0.0f);
const float damping = pipe.damping_multiplier;
const float relaxation = pipe.energy_relaxation_rate;
const float ambient = pipe.ambient_pressure;
const int count = std::max(1, static_cast<int>(std::ceil(length * cells_per_meter)));
pipe = Pipe1D(pipe.area, length, count);
pipe.damping_multiplier = damping;
pipe.energy_relaxation_rate = relaxation;
pipe.ambient_pressure = ambient;
}
}
template<class S, class Control> void run(const char* name, S& s, int seconds, Control control) { template<class S, class Control> void run(const char* name, S& s, int seconds, Control control) {
double checksum = 0; double checksum = 0;
std::uint64_t previous_iterations = 0; std::uint64_t previous_iterations = 0;
@@ -30,6 +49,10 @@ template<class S, class Control> void run(const char* name, S& s, int seconds, C
std::cout << name << ',' << second+1 << ',' << elapsed << ',' << checksum << std::endl; std::cout << name << ',' << second+1 << ',' << elapsed << ',' << checksum << std::endl;
if constexpr (requires { s.pipes; }) { if constexpr (requires { s.pipes; }) {
unsigned tiny = 0; unsigned tiny = 0;
std::uint64_t repairs=0, fallbacks=0;
for(const auto& pipe:s.pipes) { repairs+=pipe.positivity_repairs; fallbacks+=pipe.hllc_fallbacks; }
if(repairs) throw std::runtime_error("benchmark required pipe positivity repairs");
std::cerr<<name<<" repairs="<<repairs<<" hllc_fallbacks="<<fallbacks<<" peak="<<peak<<"\n";
for (const auto& pipe : s.pipes) for (const auto& pipe : s.pipes)
for (const auto& cell : pipe.cells) for (const auto& cell : pipe.cells)
for (float value : {cell.rho, cell.momentum, cell.energy, cell.air}) for (float value : {cell.rho, cell.momentum, cell.energy, cell.air})
@@ -49,10 +72,10 @@ template<class S, class Control> void run(const char* name, S& s, int seconds, C
std::cerr << name << " substeps_per_frame=" << (state.solver_substeps-previous_substeps)/44100.0 std::cerr << name << " substeps_per_frame=" << (state.solver_substeps-previous_substeps)/44100.0
<< " limiting_pipe=" << limiting_pipe << " cfl_us=" << stable_dt*1e6f << '\n'; << " limiting_pipe=" << limiting_pipe << " cfl_us=" << stable_dt*1e6f << '\n';
previous_substeps = state.solver_substeps; previous_substeps = state.solver_substeps;
unsigned long long repairs = 0; unsigned long long repairs = 0, fallbacks = 0;
for (const auto& pipe : s.pipes) repairs += pipe.positivity_repairs; for (const auto& pipe : s.pipes) { repairs += pipe.positivity_repairs; fallbacks += pipe.hllc_fallbacks; }
std::cerr << name << " rpm=" << state.rpm << " power_kW=" << state.average_power/1000 std::cerr << name << " rpm=" << state.rpm << " power_kW=" << state.average_power/1000
<< " peak=" << peak << " pipe_repairs=" << repairs << '\n'; << " peak=" << peak << " hllc_fallbacks=" << fallbacks << " pipe_repairs=" << repairs << '\n';
previous_iterations = state.boundary_iterations; previous_iterations = state.boundary_iterations;
} }
} }
@@ -61,20 +84,27 @@ int main(int argc, char** argv) {
const int seconds = argc > 1 ? std::stoi(argv[1]) : 6; const int seconds = argc > 1 ? std::stoi(argv[1]) : 6;
const std::string mode = argc > 2 ? argv[2] : "pedal"; const std::string mode = argc > 2 ? argv[2] : "pedal";
const std::string only = argc > 3 ? argv[3] : "all"; const std::string only = argc > 3 ? argv[3] : "all";
const int uniform_v8_cells_per_meter = argc > 4 ? std::stoi(argv[4]) : 0;
const auto selected_solver=argc>5 ? parse_pipe_solver(argv[5]) : PipeSolver::Rusanov;
std::cerr << "solver=" << pipe_solver_name(selected_solver) << "\n";
auto pedal = [&](int second) { auto pedal = [&](int second) {
return mode == "idle" ? 0.0f : mode == "coast" ? (second >= 1 && second < 5 ? .7f : 0.f) : return mode == "idle" ? 0.0f : mode == "coast" ? (second >= 1 && second < 5 ? .7f : 0.f) :
mode == "transient" && (second == 0 || second >= seconds-2) ? 0.0f : .7f; mode == "transient" && (second == 0 || second >= seconds-2) ? 0.0f : .7f;
}; };
std::cout << "scenario,second,wall_seconds,checksum\n"; std::cout << "scenario,second,wall_seconds,checksum\n";
for (auto name : {"twin", "turbo", "b205", "b205-gt17", "v8"}) { for (auto name : {"twin", "b205", "v8"}) {
if (only != "all" && only != name) continue; if (only != "all" && only != name) continue;
auto s = std::make_unique<FourStrokeScenario>(44100, auto s = std::make_unique<FourStrokeScenario>(44100,
std::string(name)=="v8" ? FourStrokePreset::AmericanV8 : std::string(name).starts_with("b205") ? FourStrokePreset::SaabB205 : FourStrokePreset::Twin, std::string(name)=="v8" ? FourStrokePreset::AmericanV8 : std::string(name)=="b205" ? FourStrokePreset::SaabB205 : FourStrokePreset::Twin);
std::string(name)=="turbo" || std::string(name)=="b205-gt17"); if (std::string(name) == "v8" && uniform_v8_cells_per_meter > 0) {
set_uniform_pipe_resolution(s->pipes, uniform_v8_cells_per_meter);
std::cerr << "v8 uniform_cells_per_meter=" << uniform_v8_cells_per_meter << '\n';
}
s->set_pipe_solver(selected_solver);
s->throttle = .7f; s->throttle = .7f;
run(name, *s, seconds, [&](int second) { run(name, *s, seconds, [&](int second) {
s->throttle = pedal(second); s->throttle = pedal(second);
if (mode == "loaded") s->crankshaft.omega = (std::string(name)=="v8" ? 4500 : std::string(name)=="b205-gt17" ? 5500 : 7000) * 2*pi/60; if (mode == "loaded") s->crankshaft.omega = (std::string(name)=="v8" ? 4500 : 7000) * 2*pi/60;
}); });
} }
for (bool racing : {true, false}) { for (bool racing : {true, false}) {
@@ -82,6 +112,7 @@ int main(int argc, char** argv) {
TwoStrokeConfig config; TwoStrokeConfig config;
config.exhaust = racing ? TwoStrokeExhaust::Racing : TwoStrokeExhaust::Straight; config.exhaust = racing ? TwoStrokeExhaust::Racing : TwoStrokeExhaust::Straight;
TwoStrokeScenario s(44100, config); TwoStrokeScenario s(44100, config);
s.set_pipe_solver(selected_solver);
s.set_throttle(.7f); s.set_throttle(.7f);
run(racing ? "two-stroke-racing" : "two-stroke-straight", s, seconds, [&](int second) { run(racing ? "two-stroke-racing" : "two-stroke-straight", s, seconds, [&](int second) {
s.set_throttle(pedal(second)); s.set_throttle(pedal(second));

View File

@@ -3,13 +3,31 @@
#include "FluidSim/TwoStrokeScenario.hpp" #include "FluidSim/TwoStrokeScenario.hpp"
#include "FluidSim/HelmholtzScenario.hpp" #include "FluidSim/HelmholtzScenario.hpp"
#include "FluidSim/PulseTestScenario.hpp" #include "FluidSim/PulseTestScenario.hpp"
#include <algorithm>
#include <barrier> #include <barrier>
#include <chrono> #include <chrono>
#include <cmath>
#include <iostream> #include <iostream>
#include <numeric>
#include <stdexcept>
#include <thread> #include <thread>
#include <vector>
using namespace fluidsim::port; using namespace fluidsim::port;
int main() { static void set_uniform_resolution(std::vector<Pipe1D>& pipes, int cells_per_meter) {
for (std::string name : {"twin", "turbo", "b205", "two-stroke-racing", "two-stroke-straight", "helmholtz", "pulse", "large"}) { if (cells_per_meter <= 0) throw std::invalid_argument("cells per meter must be positive");
for (auto& pipe : pipes) {
const float length = std::accumulate(pipe.cell_lengths.begin(), pipe.cell_lengths.end(), 0.0f);
const int count = std::max(1, static_cast<int>(std::ceil(length * cells_per_meter)));
const float damping = pipe.damping_multiplier;
const float relaxation = pipe.energy_relaxation_rate;
pipe = Pipe1D(pipe.area, length, count);
pipe.damping_multiplier = damping;
pipe.energy_relaxation_rate = relaxation;
}
}
int main(int argc, char** argv) {
const int uniform_cells_per_meter = argc > 1 ? std::stoi(argv[1]) : 0;
for (std::string name : {"twin", "b205", "v8", "two-stroke-racing", "two-stroke-straight", "helmholtz", "pulse", "large"}) {
std::vector<Pipe1D> geometry; std::vector<Pipe1D> geometry;
if (name == "large") { if (name == "large") {
for (int i=0; i<64; ++i) geometry.emplace_back(.001f, 2, 256); for (int i=0; i<64; ++i) geometry.emplace_back(.001f, 2, 256);
@@ -22,9 +40,11 @@ int main() {
config.exhaust = name == "two-stroke-straight" ? TwoStrokeExhaust::Straight : TwoStrokeExhaust::Racing; config.exhaust = name == "two-stroke-straight" ? TwoStrokeExhaust::Straight : TwoStrokeExhaust::Racing;
TwoStrokeScenario s(44100, config); geometry = s.pipes; TwoStrokeScenario s(44100, config); geometry = s.pipes;
} else { } else {
FourStrokeScenario s(44100, name == "b205" ? FourStrokePreset::SaabB205 : FourStrokePreset::Twin, name == "turbo"); FourStrokeScenario s(44100, name == "v8" ? FourStrokePreset::AmericanV8 : name == "b205" ? FourStrokePreset::SaabB205 : FourStrokePreset::Twin);
geometry = s.pipes; geometry = s.pipes;
} }
if (name == "v8" && uniform_cells_per_meter > 0)
set_uniform_resolution(geometry, uniform_cells_per_meter);
const int steps = 4000; const int steps = 4000;
for (int workers : {1, 2, 4}) { for (int workers : {1, 2, 4}) {
auto p = geometry; auto p = geometry;

View File

@@ -1,48 +0,0 @@
#include "FluidSim/Scenario.hpp"
#include <chrono>
#include <cmath>
#include <iostream>
#include <ctime>
using namespace fluidsim::port;
int main() {
std::cout << "engine_rpm,turbo,MAP_kPa,exhaust_kPa,shaft_kW,turbo_rpm,charge_kPa,charge_K,turbine_power_ripple_W,gate_mm,limited,energy_residual_J,substeps_per_frame,iterations_per_frame,wall_s,cpu_s\n";
for (float rpm : {4000.f, 7000.f}) {
for (bool enabled : {false, true}) {
FourStrokeScenario s(44100, FourStrokePreset::Twin, enabled);
s.throttle = .7f;
double map = 0, exhaust = 0, power = 0, turbo_rpm = 0, charge_p = 0, charge_t = 0, gate = 0;
double min_power = 1e30, max_power = -1e30;
const auto start = std::chrono::steady_clock::now();
const auto cpu_start = std::clock();
for (int i = 0; i < 2*44100; ++i) {
// Ideal speed-holding dyno. The turbo remains completely free.
s.crankshaft.omega = rpm*2*pi/60;
s.process();
if (i >= 3*44100/2) {
constexpr double samples = 22050;
map += s.intake_plenum.pressure()/samples/1000;
exhaust += s.exhaust_collector.pressure()/samples/1000;
power += s.crankshaft.average_power/samples/1000;
if (s.turbo) {
const auto t = s.turbo->snapshot();
turbo_rpm += t.rpm/samples;
charge_p += t.charge_pressure/samples/1000;
charge_t += t.charge_temperature/samples;
gate += t.wastegate_lift/samples*1000;
min_power = std::min(min_power, t.turbine_power);
max_power = std::max(max_power, t.turbine_power);
}
}
}
const auto t = s.turbo ? s.turbo->snapshot() : TurboSnapshot{};
const auto state = s.snapshot();
const double cpu = double(std::clock()-cpu_start)/CLOCKS_PER_SEC;
const auto wall = std::chrono::duration<double>(std::chrono::steady_clock::now()-start).count();
std::cout << rpm << ',' << enabled << ',' << map << ',' << exhaust << ',' << power << ','
<< turbo_rpm << ',' << charge_p << ',' << charge_t << ','
<< (enabled ? max_power-min_power : 0) << ',' << gate << ',' << t.limited_transfers
<< ',' << t.energy_residual << ',' << state.solver_substeps/88200.0 << ','
<< state.boundary_iterations/88200.0 << ',' << wall << ',' << cpu << std::endl;
}
}
}

View File

@@ -1,97 +1,112 @@
# Static outdoor audio and two-stroke verification # Audio rendering notes
Verified on Linux in Release mode, 2026-10-23. The target remains fluid-derived The desktop and capture tool produce sound at 44.1 kHz. Intake and exhaust
sound at 44.1 kHz. No synthetic bang, saturation, or combustion event is added ambient-density-scaled volume-flow derivatives drive the compact-monopole source, with static
by the audio renderer. Acoustic processing does not feed energy into the solver. source/listener positions, fractional-sample propagation, inverse-distance
spreading, and separate left/right ear paths. The monitor uses a second-
order 6 kHz low-pass; desktop playback removes DC and applies PCM clipping
protection.
## Renderer ## Outdoor scenes
`OutdoorAudio` replaces the active twin and two-stroke legacy reverb paths. `OutdoorAudio` renders direct sound, image-source ground and wall reflections,
Each intake/exhaust mass-flow derivative drives a compact monopole. Separate and six approximate woodland scatter paths. The tunnel profile adds image
left/right paths use inverse distance and fractional-sample delays at 343 m/s. paths for a nominally 6 m wide, 4 m high hard-surface enclosure. Its late tail
The listener and sources are static. Default listener position is (1.6, 1, 2) m; uses the live eight-line feedback-delay network in `OutdoorExhaustReverb`. The
intake is (-0.2, .7, 0), exhaust (.55, .5, 0), ground y=0, wall x=-1.5. five scene profiles set different reflection paths, tail levels, and
Ground and wall reflection coefficients are .35 and .65. These are configurable frequency-dependent decay times. Scene changes smooth over 20 ms. These
assumptions rather than measurements of a particular location. profiles are intended for listening comparisons, not as validated models of
particular locations.
A second-order 6 kHz monitoring low-pass reduces sharp acquisition transients; The direct source remains prominent, so scene differences are clearest in the
it is linear and does not create afterfire. Early ground/wall reflections use reflections and decay after transients. Ground effects depend on surface,
image paths. Woodland uses six approximate scatterers. A stable four-line geometry, and frequency; the current coefficients are illustrative settings,
feedback-delay network supplies an empirical diffuse tail; it does not solve not measured material data. The feedback-delay network gives a diffuse late
outdoor wave propagation or vegetation scattering. Scene changes smooth over tail, but does not simulate outdoor wave propagation or vegetation in detail.
20 ms. The old `OutdoorExhaustReverb` remains for legacy tests but is inactive
in both engine scenarios.
Pipe-end radiation impedance, calibrated directivity, alias-free acquisition, Calibrated directivity, alias-free acquisition,
and exhaust afterfire chemistry remain unresolved. PCM output is not calibrated and exhaust afterfire chemistry remain unresolved. Filtering cannot restore
speaker SPL. The monitoring filter cannot recover information lost to aliasing. source information lost to aliasing. Mechanical sound synthesis has been
removed; no mechanical source is currently rendered.
## Measured captures ## Listening and capture
Four seconds at full pedal, wall environment, 44.1 kHz stereo. Each row's raw Run the desktop with `--scenario v8 --scene tunnel` and press **R** to cycle
and rendered signals come from the same simulation run. Measurements are through Dry, Field, Wall, Woodland, and Tunnel. Use 1x playback when judging
before PCM clipping; the raw signal includes direct spatial propagation. the audio.
To write comparable WAVs for one scene, build and run:
| Engine | Raw maximum adjacent-sample change | Rendered change | Rendered peak | Clipping samples |
| --- | ---: | ---: | ---: | ---: |
| Twin | .569144 | .0881295 | .119947 | 0 |
| Two-stroke | .412956 | .138741 | .390789 | 0 |
These measurements establish transient reduction, not a subjective judgment
that limiter events sound like realistic bangs. Listening on the target audio
device is still required. Reproduce from the repository root:
```sh ```sh
cmake --build build-audit --target fluidsim_audio_capture -j 4 cmake --build build-desktop --target fluidsim_audio_capture -j 4
./build-audit/fluidsim_audio_capture --scenario twin --out build/audio/twin-wall --seconds 4 --throttle 1 --scene wall ./build-desktop/fluidsim_audio_capture --scenario v8 --scene field --seconds 1 --throttle .7 --out /tmp/v8-field
./build-audit/fluidsim_audio_capture --scenario two-stroke --out build/audio/two-stroke-wall --seconds 4 --throttle 1 --scene wall
``` ```
Each produces `-rendered.wav`, `-dry.wav` (filtered direct paths) and The capture tool writes rendered, dry-filtered, and unfiltered WAV files. The
`-unfiltered.wav` for comparison. Capture files are build artifacts. dry and unfiltered outputs omit the outdoor reflections and reverb, which helps
separate source filtering from scene processing.
## Two-stroke and regression checks
The existing 125 cc geometry now uses the shared ECU and finite-response
throttle, fuel dosing after the last scavenging port closes, 3000 RPM idle
control with ignition trim, and a dynamic damped cantilever reed with contacts.
Cylinder/crankcase geometry work uses adiabatic energy differences. An active
charge finishes combustion when a fuel cut begins. Ignition event crossing
handles wraparound without spuriously firing retarded ignition at zero degrees.
Reed parameters, combustion, scavenging and controller calibrations have not
been validated against a measured engine.
Both desktop engines share `MotorcycleTest` for clutch/vehicle integration,
common control dispatch and a common dashboard/dyno renderer. The two-stroke
limiter is disabled explicitly in the ECU and cylinder; the twin limiter is
unchanged. Idle and overrun controls remain enabled. The motorcycle and gearing
reuse the existing twin test configuration, not a measured 125 cc motorcycle.
The rolling peak dyno reports crank power and torque versus RPM.
All five CTest suites passed: basic physics/control, fluid boundary validation,
intake regression, outdoor audio, and two-stroke regression. Audio tests cover
fractional direct arrival, image reflection arrival, stereo ordering, distance
gain, linearity, filtering, causal decaying tails, and reed static compliance
and seating. The two-stroke sequence idles for two seconds, holds .6 pedal for
four, then releases for eight. It checks fuel stays enabled at open pedal above
the old limit, reed opening/seating, positive states, finite audio and idle
recovery, with no pipe positivity repairs. It then launches the motorcycle for
six seconds and checks shifting, neutral and positive dyno power/torque.
Latest Release results: free-rev maximum 22471.3 RPM, recovered idle 3236.08 RPM,
free-rev audio peak .512784, motorcycle speed 51.3773 km/h after launch. The high
unloaded RPM exposes the uncalibrated high-speed friction/combustion behavior;
removing the limiter does not validate this as realistic engine performance.
Unit checks also verify disabling an already latched limiter restores fueling.
The earlier four-second capture table above predates limiter removal and is
retained as an audio-renderer measurement, not a current engine baseline.
Windows execution of the latest changes is pending.
## Reference basis ## Reference basis
- [Image source geometry](https://www.acs.psu.edu/noahparker/acs/img-src/img-src.html) - [FHWA ground-effect guidance](https://www.fhwa.dot.gov/Environment/noise/noise_barriers/design_construction/design/design03.cfm)
- [Feedback delay networks](https://msp.ucsd.edu/techniques/latest/book-html/node111.html) - [Feedback delay networks in artificial reverberation](https://theses.eurasip.org/document/feedback-delay-networks-in-artificial-reverberation-and-reverberation-enhancement/)
- [Beam theory](https://mechanics.ju.se/SolidMechanics/BeamTheory.html), including uniform-load cantilever deflection used for reed compliance.
The later YZ125 baseline changes these engine calibrations and measurements; see ## Pipe mouth boundary
[YZ125 baseline verification](YZ125_BASELINE.md) for current results.
All scenario pipe mouths use `add_radiating_open_end`. The intake atmosphere
component/orifice has been replaced by the same boundary used at exhaust mouths.
Physical intake plenums and exhaust collectors remain finite storage volumes.
The older `add_open_end` pressure-release boundary remains for idealized tests.
The unflanged circular-mouth reflection follows the causal second-order rational
fit in Silva et al., Eq. 16, with the low-frequency constraints in Eq. 17.
Its pressure reflection tends to -1 at low frequency, includes the 0.6133-radius
end correction, and loses reflected energy as frequency rises. Two scalar states
are integrated with the trapezoidal rule at each actual CFL substep; no history
buffers or allocations are needed. The filtered incoming characteristic combines
with the outgoing duct characteristic to determine the face state. One face flux
updates the pipe. The same face velocity and area supply the audio volume-flow
source, scaled by ambient density. Ambient inflow carries
fresh air, zero fuel, and 300 K stagnation enthalpy.
This is a plane-wave, no-flow radiation approximation coupled to a nonlinear
Euler duct. It does not establish accurate hot-jet/high-Mach radiation, vortex
shedding, flange effects, or higher duct modes. The published continuous model
has errors below 8 percent for reflection magnitude/end correction at ka <= 2;
mesh damping and time discretization introduce additional simulation error.
The audio renderer still uses a compact monopole approximation, so this boundary
upgrade alone does not calibrate external SPL or directivity.
Source: [Silva et al., acoustic radiation impedance](https://arxiv.org/html/0811.3625).
Recorded Linux Release check after this change: all seven CTest suites passed
(27.83 s). Tests cover the reflection fit's amplitude and phase at ka=0.1, 0.5,
1 and 2, inverted returning pulses at both orientations, reported mass exchange,
and fresh-air inflow stagnation enthalpy. These are implementation checks, not
experimental validation of strong-flow radiation. The B205 regression now checks
only its four active cylinders instead of the unused slots added for the V8.
A four-second V8 4500-RPM loaded benchmark took 0.395, 0.387, 0.403 and 0.403
wall seconds per simulated second (mean 0.397). This is headless runtime evidence
on this machine, not a measurement of audio device underruns. A four-second
V8/tunnel capture at 50 percent pedal had rendered peak 0.1011, RMS 0.02027,
and zero clipped samples. Capture files are under `build-desktop/audio/open-end-v8-*`.
## Light-load mouth-source correction
The radiating mouth previously sent actual mass flow to the sound renderer.
Switching between hot exhaust and cool ambient inflow introduces a donor-density
change into that signal. The renderer now differentiates ambient-density-scaled
volume flow, following the compact monopole formulation in the
[openCFS acoustic documentation](https://opencfs.gitlab.io/userdocu/PDEExplanations/Singlefield/AcousticPDE/).
Physical mass and energy fluxes and the causal reflection model are unchanged.
This remains a compact, no-mean-flow acoustic approximation.
A prescribed 4500 RPM Saab run, with one second of warmup and two seconds of
capture, compared the same straight-through exhaust before and after this audio
change. At 5% pedal, rendered RMS changed from 0.022763 to 0.024120; the fraction
of power above 2 kHz fell from 0.0475% to 0.0003%. At 15% pedal, maximum sample
change divided by RMS fell from 0.607 to 0.086. These are signal measurements,
not confirmation of perceived realism or a complete road-transient validation.
A regression checks that prescribed mouth velocity produces the same acoustic
volume source at hot and ambient donor densities, in both pipe orientations.

View File

@@ -32,9 +32,8 @@ Cams/combustion retain the shared four-stroke model. The ideal ECU targets
850 RPM idle, with estimated 6500/6250 RPM cut/resume and 2000/1300 overrun 850 RPM idle, with estimated 6500/6250 RPM cut/resume and 2000/1300 overrun
thresholds. These are scenario calibrations, not a factory Trionic map. thresholds. These are scenario calibrations, not a factory Trionic map.
There is no compressor, turbine, intercooler, wastegate, catalyst or muffler in This naturally aspirated first version has ambient intake boundary pressure and
this first version. Intake boundary pressure is ambient. Consequently factory does not reproduce factory boosted torque or exhaust sound. Thermal,
turbo torque/power and factory exhaust sound are not reproduced. Thermal,
combustion and single-zone mixing limitations from the fluid audit still apply. combustion and single-zone mixing limitations from the fluid audit still apply.
## Verification ## Verification

View File

@@ -29,9 +29,9 @@ tolerance; long engine/audio traces need not be bit-identical to the reference.
A `-pg` build running every scenario for two simulated seconds attributed about A `-pg` build running every scenario for two simulated seconds attributed about
39% of sampled executable time to pipe advancement, 38% to orifice boundary 39% of sampled executable time to pipe advancement, 38% to orifice boundary
solves, and 13% to Solver::step (including inlined preparation). Turbo mechanics solves, and 13% to Solver::step (including inlined preparation). Outdoor emitter
were under 1% and outdoor emitter rendering about 1%. These are sampled gprof rendering was about 1%. These are sampled gprof results, not precise attribution
results, not precise attribution of time inside dynamically linked math functions. of time inside dynamically linked math functions.
`fluidsim_thread_benchmark` now uses each scenario's actual pipe geometry and `fluidsim_thread_benchmark` now uses each scenario's actual pipe geometry and
compares persistent 1/2/4-worker execution with barriers per substep. This isolates compares persistent 1/2/4-worker execution with barriers per substep. This isolates
@@ -52,7 +52,6 @@ Mean wall seconds per simulated second, six-second sustained-load runs:
| Scenario | Reference | Optimized | Time reduction | SIMD disabled | | Scenario | Reference | Optimized | Time reduction | SIMD disabled |
|---|---:|---:|---:|---:| |---|---:|---:|---:|---:|
| Twin | .528 | .405 | 23% | .423 | | Twin | .528 | .405 | 23% | .423 |
| Turbo twin | 1.368 | .649 | 53% | .684 |
| B205 | .884 | .689 | 22% | .737 | | B205 | .884 | .689 | 22% | .737 |
| Two-stroke racing | .561 | .470 | 16% | .535 | | Two-stroke racing | .561 | .470 | 16% | .535 |
| Two-stroke straight | .539 | .464 | 14% | .525 | | Two-stroke straight | .539 | .464 | 14% | .525 |
@@ -60,7 +59,7 @@ Mean wall seconds per simulated second, six-second sustained-load runs:
| Pulse | .256 | .238 | 7% | .315 | | Pulse | .256 | .238 | 7% | .315 |
Idle/transient optimized means (seconds per simulated second): twin .394/.399, Idle/transient optimized means (seconds per simulated second): twin .394/.399,
turbo .627/.651, B205 .666/.683, racing two-stroke .450/.459, straight two-stroke B205 .666/.683, racing two-stroke .450/.459, straight two-stroke
.452/.453, Helmholtz .141/.140, and pulse .238/.238. .452/.453, Helmholtz .141/.140, and pulse .238/.238.
The pulse scenario has no orifice, so its improvement isolates the SIMD-friendly The pulse scenario has no orifice, so its improvement isolates the SIMD-friendly
@@ -69,7 +68,7 @@ native-instruction build improves some timings further, but is not shipped as
the default: FMA also changes rounding and the binary is CPU-specific. the default: FMA also changes rounding and the binary is CPU-specific.
Initial isolated changes measured approximately .40 seconds for the twin after Initial isolated changes measured approximately .40 seconds for the twin after
boundary warm starts alone (previously .53). The sustained turbo still took boundary warm starts alone (previously .53). The sustained high-load scenario still took
about 1.2 seconds after that change; eliminating subnormal tracer tails brought about 1.2 seconds after that change; eliminating subnormal tracer tails brought
it to about .65. The diagnostic found 22 subnormal pipe fields before truncation it to about .65. The diagnostic found 22 subnormal pipe fields before truncation
and zero afterwards. This is distinct from reducing CFL substeps or geometry; and zero afterwards. This is distinct from reducing CFL substeps or geometry;
@@ -79,8 +78,9 @@ performance for every user configuration or arbitrarily long simulation.
## Reproduction ## Reproduction
Final validation: all nine CTest suites passed both with SIMD enabled (28.50 s) Final validation at the time of this audit: all nine then-registered CTest
and disabled (30.86 s). The fluid validation executable includes 30 checks, suites passed both with SIMD enabled (28.50 s) and disabled (30.86 s). The
fluid validation executable includes 30 checks,
including tracer underflow conservation. The Release SFML desktop target also including tracer underflow conservation. The Release SFML desktop target also
rebuilt successfully. This audit verifies desktop compilation and headless audio rebuilt successfully. This audit verifies desktop compilation and headless audio
rendering; interactive playback underrun testing remains a separate user check. rendering; interactive playback underrun testing remains a separate user check.
@@ -95,12 +95,12 @@ ctest --test-dir build-opt --output-on-failure
./build-opt/fluidsim_thread_benchmark ./build-opt/fluidsim_thread_benchmark
``` ```
The scenario benchmark runs twin, turbo twin, B205, racing and straight-pipe The scenario benchmark runs twin, B205, V8, racing and straight-pipe
two-strokes, Helmholtz and pulse scenarios, including their normal audio path. two-strokes, Helmholtz and pulse scenarios, including their normal audio path.
`loaded` holds four-strokes at 7,000 RPM and two-strokes at 10,000 RPM using an `loaded` holds four-strokes at 7,000 RPM and two-strokes at 10,000 RPM using an
ideal speed-holding dyno; engine throttle is 70%. `idle` releases the pedal; ideal speed-holding dyno; engine throttle is 70%. `idle` releases the pedal;
`transient` starts released, applies 70%, then releases for the last two seconds. `transient` starts released, applies 70%, then releases for the last two seconds.
The optional final argument selects one scenario, e.g. `6 loaded turbo`. The optional final argument selects one scenario, e.g. `6 loaded v8`.
CSV timing/checksum goes to stdout; subnormal and iteration diagnostics go to CSV timing/checksum goes to stdout; subnormal and iteration diagnostics go to
stderr. Inspection happens outside the timed sample-processing loop. stderr. Inspection happens outside the timed sample-processing loop.

View File

@@ -1,46 +0,0 @@
# Saab B205 with GT17 and ECU boost control
The desktop Saab menu entry and `--scenario b205` now select an estimated GT1752
turbo configuration. `--turbo gt17` makes that choice explicit; `--turbo none`
selects the previous naturally aspirated baseline. CLI and WAV capture follow
the same defaults. `SaabB205Scenario` preserves its baseline API;
`SaabTurboScenario` is the boosted convenience class.
The GT17 calibration uses a 26 mm compressor tip radius, 19 mm effective turbine
radius, 2e-5 kg m² rotor inertia, 420/350 mm² effective compressor/turbine areas,
70% efficiencies, 100 cc housing volumes and a 24 mm wastegate. These are model
estimates, not a measured Saab map or verified rotor inertia. The Garrett catalog
identifies the Saab GT1752 application:
https://www.turbomaster.info/eng/downloads/catalog_garrett.pdf
The ECU requests up to 45 kPa gauge boost, ramping with pedal and RPM. A PI
controller uses manifold pressure filtered over 50 ms, conditional integration,
and bounded pneumatic bleed duty (0–85%). Duty reduces the pressure signal to
the existing spring/diaphragm actuator. Actual valve travel remains damped and
boost emerges from the physical rotor, restrictions and conserved gas transfers.
Zero duty restores the mechanical pressure-can response; it does not forcibly
open the valve or impose a manifold pressure. Targets below spring boost cannot
be achieved by wastegate bleed alone.
Lift-off, fuel cut and invalid sensor data request zero duty and clear the PI
integral. Invalid boost sensors cut fuel. Manifold or charge pressure above
75 kPa gauge triggers fuel cut, recovering below 55 kPa gauge. This is a basic
estimated safety calibration, not a reproduction of Saab Trionic software.
No intercooler, compressor surge/BOV model or knock model has been added;
charge heating and exhaust backpressure remain present.
The regression holds the engine at 3,500 RPM, applies full pedal for six seconds,
then releases for three. It checks boost settling within 8 kPa of the 45 kPa
target, shaft spool-up/down, finite audio, no pipe positivity repairs, no housing
donor limiting, and gas/rotor energy residual below 0.1 J. Controller checks cover
overboost, recovery, pedal release and an invalid sensor. The HUD shows target
gauge boost, actuator bleed duty, turbo RPM and absolute gas pressures.
```sh
cmake --build build-desktop --target fluidsim_desktop -j4
./build-desktop/fluidsim_desktop --scenario b205 --turbo gt17
./build-audit/fluidsim_scenario_benchmark 6 loaded b205-gt17
```
All ten CTest suites pass, including the new ten-second Saab turbo regression.
The GT17 benchmark holds 5,500 RPM, below the Saab's 6,500 RPM limiter.

View File

@@ -0,0 +1,124 @@
# Pipe solver comparison — 2026-10-23
The original Rusanov/local Lax–Friedrichs flux, HLLE and HLLC are selectable
with `--solver` in the desktop, engine CLI, and audio capture tool. Press S
in the desktop to cycle all engine pipes together; the dashboard shows the
selected solver. The existing simulation lock serializes a change between
samples. Rusanov remains the startup default. Live switching keeps the ongoing
engine and acoustic state, so it is a listening convenience rather than a
repeatable experiment. The WAVs below use independent, matching initial states.
All three use the existing per-pipe meshes, first-order spatial update, Euler
thermodynamics, CFL limit, and new radiating mouth boundaries. HLLE uses wave
bounds enclosing both endpoints and the Roe acoustic estimates. HLLC restores
the intermediate contact wave and uses HLLE if its selected star state is
nonphysical. Fallback counts refer to internal pipe faces. Pipe junctions share
one flux; differently configured participants use the shared Rusanov flux.
No global fast-math, CPU-specific flags or multithreaded pipe updates were added.
## Performance
Intel Core i5-12600K, Linux, GCC 16.2.1, the existing `build-desktop`
RelWithDebInfo configuration (`-O2 -g -DNDEBUG`, SIMD enabled). Timed runs were
sequential, without a desktop or concurrent build. Each loaded run lasts eight
simulated seconds, repeated three times in rotating solver order. The table
reports the median of each run's mean for seconds 2–8. Transients last twelve
seconds: released pedal, then 70 percent, then release for the last two seconds.
Their means also exclude the first second. All work includes normal audio rendering.
Values are **wall seconds per simulated second**, so less than 1 allows real time
in these headless measurements. Loaded RPM is held at 10000 for the YZ125 racing
exhaust and 4500 for the V8 with 70 percent pedal.
| Scenario | Solver | Loaded | Worst loaded second | Transient |
|---|---|---:|---:|---:|
| two-stroke-racing | rusanov | 0.548 | 0.568 | 0.547 |
| two-stroke-racing | hlle | 0.794 | 0.874 | 0.795 |
| two-stroke-racing | hllc | 0.893 | 0.916 | 0.898 |
| v8 | rusanov | 0.378 | 0.400 | 0.382 |
| v8 | hlle | 0.460 | 0.487 | 0.470 |
| v8 | hllc | 0.497 | 0.542 | 0.499 |
All benchmark runs completed with finite audio, zero pipe positivity repairs,
and zero HLLC fallbacks. This covers the measured windows rather than every
possible operating condition. HLLC has the least two-stroke real-time headroom.
The original HLLC experiment took 1.014 s/s on the loaded two-stroke. Caching Roe
root-density/enthalpy data once per cell, specializing the flux loop by solver,
and evaluating only the star state selected by the contact speed brought this
to 0.893 s/s. The cache is allocated at construction and reused at every substep;
there are no allocations in the new flux routines.
Raw measurements: [initial.csv](solver-comparison/initial.csv),
[final.csv](solver-comparison/final.csv), and
[diagnostics.log](solver-comparison/diagnostics.log). `summary.json` contains
per-run means. The benchmark fourth argument is optional uniform V8 cells/m
(0 preserves the existing mesh); the fifth selects the solver.
```sh
cmake --build build-desktop -j 8
./build-desktop/fluidsim_scenario_benchmark 8 loaded two-stroke-racing 0 hllc
./build-desktop/fluidsim_scenario_benchmark 8 loaded v8 0 hllc
./build-desktop/fluidsim_scenario_benchmark 12 transient v8 0 hllc
```
## Numerical checks
All nine CTest suites passed on the final build (26.96 seconds). The fluid
validation runs separately with Rusanov, HLLE and HLLC, with 36 checks per run.
The lossless pipe cases exercise the selected solver: closed/profiled mass and
energy conservation, acoustic reflection, strong pressure pulses, near-vacuum
expansion and Sod convergence. Additional checks cover uniform fluxes,
orientation symmetry for all transported quantities, a stationary HLLC contact,
and intentional HLLE fallback during a strong expansion.
[validation.log](solver-comparison/validation.log) records their output.
Passing these checks does not establish complete engine or acoustic accuracy.
At 400 cells, the Sod density L1 error was about 0.01595 for Rusanov,
0.00702 for HLLE, and 0.00654 for HLLC. This is a shock-tube result; contact
resolution and sound quality are different questions. With first-order meshes,
changing the flux alone still leaves considerable numerical acoustic damping.
## Listening
The controlled recordings are six seconds each, after two seconds of warmup,
with the same dry scene, 70 percent pedal, per-pipe geometry, monitor filter,
and capture gain. An ideal speed-holding dyno fixes the two-stroke at 10000 RPM
and V8 at 4500 RPM. The recordings have no desktop automatic gain and no
normalization. Numerical fuel/temperature transport can change the subsequent
combustion, so these compare complete engine simulations with different fluxes.
Files are under `build-desktop/audio/solvers/`, named
`two-stroke-{rusanov,hlle,hllc}-rendered.wav` and
`v8-{rusanov,hlle,hllc}-rendered.wav`. Dry and unfiltered versions are also
available; each capture's `.log` records its measurements.
| Scenario | Solver | Rendered peak | Rendered RMS | Clipped samples |
|---|---|---:|---:|---:|
| two-stroke | rusanov | 0.1416 | 0.01795 | 0 |
| two-stroke | hlle | 0.1467 | 0.01836 | 0 |
| two-stroke | hllc | 0.1489 | 0.01815 | 0 |
| v8 | rusanov | 0.0650 | 0.02452 | 0 |
| v8 | hlle | 0.0904 | 0.02746 | 0 |
| v8 | hllc | 0.1204 | 0.03318 | 0 |
```sh
./build-desktop/fluidsim_audio_capture --scenario two-stroke --solver hllc --scene dry --throttle .7 --rpm 10000 --warmup 2 --seconds 6 --out build-desktop/audio/solvers/two-stroke-hllc
./build-desktop/fluidsim_audio_capture --scenario v8 --solver hllc --scene dry --throttle .7 --rpm 4500 --warmup 2 --seconds 6 --out build-desktop/audio/solvers/v8-hllc
./build-desktop/fluidsim_desktop --scenario two-stroke --scene dry --solver hlle
./build-desktop/fluidsim_desktop --scenario v8 --scene dry --solver hllc
```
Both desktop scenarios were actually launched with audio streams visible in
PipeWire. A two-stroke HLLE-to-HLLC switch was exercised live; its dashboard
showed 1.000x playback and zero underruns. The V8 was then launched with HLLC
and a later snapshot showed live HLLE at 6544 RPM, 100 percent
throttle, 1.000x playback and zero underruns. These are spot observations;
both desktop processes subsequently exited cleanly. Screenshots are
`build-desktop/audio/solvers/two-stroke-live.png` and `v8-live.png`.
Use S to cycle solvers and G to toggle automatic gain during listening.
## Method references
- [Clawpack: approximate Euler solvers](https://www.clawpack.org/riemann_book/html/Euler_approximate.html)
- [Toro, Spruce and Speares: restoration of the contact surface in HLL](https://doi.org/10.1007/BF01414629)
- [Athena solver documentation](https://princetonuniversity.github.io/Athena-Cversion/AthenaDocsUGRiemann.html)

View File

@@ -1,111 +0,0 @@
# VZ21-size turbo prototype
Linux Release, 2026-10-23. Core is ready for an early interactive test. Further
map fitting, acoustic work and performance tuning are deferred at the user's
request so testing can start. No blade-passing whistle has been implemented yet.
## Model
Four finite 0-D gas volumes connect compressor inlet/outlet and turbine
inlet/outlet to existing `BoundarySystem` pipe orifices. Register the turbo once
with `Solver`; do not separately register its internal volumes. Internal flows
are bidirectional and choked where appropriate. Donor budgets preserve positive
mass/energy. Compensated 0-D volume accumulation prevents sub-ulp mass drift.
The compressor's estimated Euler head is psi*(omega*R)^2. Efficiency maps this
to a virtual stagnation pressure; a compressible restriction determines actual
flow against the downstream pressure. The turbine flow follows an effective
nozzle. Its torque uses a mean-line speed-ratio efficiency parabola, with finite
starting torque. Reverse flow is passive; deep surge/reverse-wheel aerodynamics
are not validated. These correlations are not a measured compressor/turbine map.
A rigid shaft obeys I*domega/dt = turbine torque - compressor torque - bearing
and windage torque. Implicit midpoint work balances the exact change in rotor
kinetic energy, including starting from rest. Adaptive shaft steps prevent
large timesteps from reversing the rotor numerically. Turbine work is removed
from gas enthalpy; compressor work is added to charge enthalpy. Bearing loss is
tracked as heat outside the gas circuits. No shaft-speed or boost clamp is used.
Estimated default parameters: inertia 4e-6 kg m^2, compressor effective radius
18 mm, turbine mean effective radius 13.5 mm, efficiencies .65, compressor head
coefficient .6, effective compressor/turbine flow areas 110/100 mm^2. Housing
volumes are 20/30/30/20 cc. Connection bores are 24/24/25/26 mm. Viscous bearing
coefficient is 1e-7 Nm/(rad/s), windage coefficient 2e-11 Nm/(rad/s)^2.
The estimated wastegate has an 800 mm^2 pressure diaphragm, 40 N preload,
6000 N/m spring, 120 Ns/m overdamped linkage, 14 mm valve and 4 mm maximum lift.
Compressor delivery gauge pressure drives its opening. Bypass gas transfers
enthalpy without delivering turbine shaft work. The actuator is an idealized
external pressure-control linkage; its mechanical/thermal energy is outside the
audited gas/rotor system. There is no intercooler, blow-off valve, oil-temperature
model, knock model or thermal/wheel failure model.
## Reference basis
[Ecotrons VZ21 specification](https://ecotrons.com/files/ECOTRONS%20Turbo%20VZ21%20technical%20specl.pdf)
provides rough applications and casing dimensions; its performance illustrations
explicitly refer to a similar IHI family rather than measured VZ21 maps. Those
figures cannot validate the coefficients used here. A
[supplier's VZ21 listing](https://www.maxpeedingrods.nl/en/products/rhb31-vz21-turbocharger-junta-100ps-for-motor-pequeno-rhino-motocicleta-at-vz21-npvl)
lists 24.2/37.1 mm compressor and 35/26.7 mm turbine inducer/exducer diameters.
The model's effective radii remain estimates.
[NASA compressor/turbine work relations](https://www.grc.nasa.gov/WWW/K-12/BGP/ctmatch.html)
provide the ideal-gas thermodynamic basis. Conservation checks establish
numerical consistency, not predictive accuracy for a real VZ21.
## Verification
All nine CTest suites passed after the changes. New checks cover stationary
equilibrium, startup, closed mass/energy balance, a pre-spun rotor, large shaft
timestep passivity, reverse flow, choking, subcritical backpressure response,
wastegate bypass, hot pulsing pipe coupling and twin throttle/limiter/release.
Closed 10 ms runs: measured mass drift zero; energy error -0.16 microjoule from
rest and -0.71 microjoule with an initially 120000 RPM shaft. Energy totals
include all gas, rotor and bearing heat. A hot 80 Hz pulsing pipe bench reached
189122 RPM, delivered .03067 kg/s charge at 376 K, and retained turbine power
pulses of 1189..4022 W. No internal transfer limiting or pipe repairs occurred.
The seven-second twin transient reached 236218 RPM, peak charge pressure
181.46 kPa absolute, peak instantaneous turbine-inlet pressure 674.10 kPa and
2.78 mm gate lift. It coasted down to 151755 RPM after release. Audio peak was
.02933 full scale, with no clipping; accumulated turbo energy residual was
-8.8 microjoules. High shaft speeds are model output, not validated operating
limits or evidence that a real VZ21 would survive them.
Same-engine-speed dyno comparison, .7 pedal, averaging the last half-second of
each two-second run:
| RPM | Turbo | MAP kPa | Exhaust collector kPa | Shaft kW | Charge K | Shaft turbo RPM |
| ---: | :---: | ---: | ---: | ---: | ---: | ---: |
| 4000 | no | 93.30 | 110.00 | 23.54 | — | — |
| 4000 | yes | 151.31 | 172.99 | 24.01 | 387.58 | 194130 |
| 7000 | no | 84.56 | 115.02 | 40.64 | — | — |
| 7000 | yes | 147.13 | 226.09 | 39.69 | 397.86 | 214361 |
Pressure and temperature costs are retained: boost does not guarantee more
shaft power. There is no forced power gain. Effective area/efficiency, gas
properties and the base engine's approximate combustion/overlap affect these
numbers strongly. They are prototype comparisons, not real engine predictions.
The original benchmark used four solver substeps per output frame. The turbo cost .81
CPU seconds per simulated second at 4000 RPM and 1.28 at 7000 RPM, versus .53
for the unboosted twin. Sustained high-load real-time playback is an open issue;
monitor underruns. The mixed transient cost .84 wall seconds per simulated
second. The subsequent [optimization audit](OPTIMIZATION.md) resolves the
subnormal-tracer slowdown and measures about .65 seconds per simulated second
in its 7,000-RPM workload. Tests run headlessly; latest Windows runtime is unverified.
## Run
```sh
cmake --build build-desktop --target fluidsim_desktop -j 4
./build-desktop/fluidsim_desktop --scenario twin --turbo vz21 --scene field
cmake --build build-audit --target fluidsim_turbo_benchmark -j 4
./build-audit/fluidsim_turbo_benchmark
```
The startup menu also offers the turbo twin. WAV capture accepts
`--scenario twin --turbo vz21`. Blade-passing acoustics are the next requested
part: they must follow rotor geometry/loading and be filtered before 44.1 kHz
sampling, rather than adding an arbitrary sound or aliased pressure ripple.

View File

@@ -0,0 +1,77 @@
# V8 pipe resolution and performance check
Measured on an Intel Core i5-12600K with GCC 16.2.1, Release (`-O3`, no
fast-math). Each scenario run advances 44,100 frames in pedal mode. Uniform
resolution runs rebuild every V8 pipe to the requested cells/m; cell counts are
rounded up to whole cells, so short pipes can be slightly denser than the target.
## Resolution sweep
| Requested cells/m | Wall time per simulated second | Solver substeps/frame |
|---:|---:|---:|
| Existing per-pipe meshes | 0.429 s | 1.00 |
| 20 | 0.421 s | 1.00 |
| 30 | 0.793 s | 1.53 |
| 33 | 0.886 s | 1.74 |
| 35 | 0.979 s | 1.80 |
| 36 | 1.048 s | 1.82 |
| 40 | 1.151 s | 1.89 |
Thirty-five cells/m was barely faster than real time on this host; 36 missed
real time. The existing per-pipe mesh is substantially faster and remains the
default.
These are one-second runs at one workload and machine; they do not establish a
cross-machine guarantee.
A second sweep held the V8 at 4,500 RPM (`loaded` mode), a steadier workload:
| Requested cells/m | Wall time per simulated second |
|---:|---:|
| Existing per-pipe meshes | 0.424 s |
| 25 | 0.552 s |
| 30 | 0.830 s |
| 32 | 0.951 s |
| 33 | 0.983 s |
| 34 | 1.049 s |
Thirty-three cells/m only just stayed under real time, while 34 missed. For
headroom, 25 cells/m took 0.55 s and 30 cells/m took 0.83 s; the existing
per-pipe setting remains much faster.
Run with `fluidsim_scenario_benchmark 1 pedal v8 <cells-per-meter>`. The optional
last argument affects only this benchmark's V8 instance and does not change
application mesh defaults.
## Pipe threading
The isolated pipe-advance benchmark used the same 35 cells/m V8 mesh and 4,000
steps. It measured 31.6 ms with one worker, 28.2 ms with two, and 23.4 ms with
four. This is a 26% reduction in pipe-only time at four workers. Boundary
resolution, engine components, and audio remain serial, and the measured gain
does not include the extra synchronization needed to parallelize both pipe
preparation and advancement in the full solver. Other small scenarios generally
lost with worker threads. No threaded solver path was retained.
Run with `fluidsim_thread_benchmark 35` to include the V8 mesh. Omitting the
argument uses the original per-pipe V8 geometry.
## SIMD and allocations
At 30 cells/m, a five-second V8 run averaged 0.646 s per simulated second with
compiler vectorization enabled and 0.585 s with GCC vectorization disabled,
excluding the first second from each average. State checksums and substep counts
matched. This V8 result did not show a SIMD benefit; the project's existing
optimization audit found SIMD useful for its other scenarios, so the global
setting was left unchanged. A `-march=native` build did not improve the 33
cells/m run (0.891 s versus 0.886 s portable).
Pipe cell, face, state, wave, and next-state vectors are allocated when a pipe
is constructed. `Pipe1D::advance` reuses those buffers and swaps the cell
buffers; it performs no heap allocation per solver substep. Boundaries and
component updates also use their setup-time storage. The desktop snapshot path
does copy pipe geometry and states into fresh vectors each render frame; that is
outside the audio sample update, but could be changed to reuse a persistent
snapshot if UI allocation traffic becomes measurable.
An experiment that precomputed per-cell inverse volumes and area-gradient
factors did not improve the V8 benchmark and was reverted.

View File

@@ -0,0 +1,828 @@
two-stroke-racing-loaded-hllc-1
solver=hllc
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.155666
two-stroke-racing second=1 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.156295
two-stroke-racing second=2 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.155836
two-stroke-racing second=3 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.156248
two-stroke-racing second=4 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.155806
two-stroke-racing second=5 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.155462
two-stroke-racing second=6 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.156293
two-stroke-racing second=7 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.156324
two-stroke-racing second=8 subnormal_cell_fields=0
two-stroke-racing-loaded-hllc-2
solver=hllc
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.155666
two-stroke-racing second=1 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.156295
two-stroke-racing second=2 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.155836
two-stroke-racing second=3 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.156248
two-stroke-racing second=4 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.155806
two-stroke-racing second=5 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.155462
two-stroke-racing second=6 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.156293
two-stroke-racing second=7 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.156324
two-stroke-racing second=8 subnormal_cell_fields=0
two-stroke-racing-loaded-hllc-3
solver=hllc
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.155666
two-stroke-racing second=1 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.156295
two-stroke-racing second=2 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.155836
two-stroke-racing second=3 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.156248
two-stroke-racing second=4 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.155806
two-stroke-racing second=5 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.155462
two-stroke-racing second=6 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.156293
two-stroke-racing second=7 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.156324
two-stroke-racing second=8 subnormal_cell_fields=0
two-stroke-racing-loaded-hlle-1
solver=hlle
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.152866
two-stroke-racing second=1 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.152739
two-stroke-racing second=2 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.15186
two-stroke-racing second=3 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.152724
two-stroke-racing second=4 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.153207
two-stroke-racing second=5 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.152497
two-stroke-racing second=6 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.153468
two-stroke-racing second=7 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.151904
two-stroke-racing second=8 subnormal_cell_fields=0
two-stroke-racing-loaded-hlle-2
solver=hlle
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.152866
two-stroke-racing second=1 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.152739
two-stroke-racing second=2 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.15186
two-stroke-racing second=3 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.152724
two-stroke-racing second=4 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.153207
two-stroke-racing second=5 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.152497
two-stroke-racing second=6 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.153468
two-stroke-racing second=7 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.151904
two-stroke-racing second=8 subnormal_cell_fields=0
two-stroke-racing-loaded-hlle-3
solver=hlle
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.152866
two-stroke-racing second=1 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.152739
two-stroke-racing second=2 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.15186
two-stroke-racing second=3 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.152724
two-stroke-racing second=4 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.153207
two-stroke-racing second=5 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.152497
two-stroke-racing second=6 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.153468
two-stroke-racing second=7 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.151904
two-stroke-racing second=8 subnormal_cell_fields=0
two-stroke-racing-loaded-rusanov-1
solver=rusanov
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.147292
two-stroke-racing second=1 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.146841
two-stroke-racing second=2 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.146614
two-stroke-racing second=3 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.147836
two-stroke-racing second=4 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.147299
two-stroke-racing second=5 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.146466
two-stroke-racing second=6 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.14884
two-stroke-racing second=7 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.147115
two-stroke-racing second=8 subnormal_cell_fields=0
two-stroke-racing-loaded-rusanov-2
solver=rusanov
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.147292
two-stroke-racing second=1 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.146841
two-stroke-racing second=2 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.146614
two-stroke-racing second=3 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.147836
two-stroke-racing second=4 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.147299
two-stroke-racing second=5 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.146466
two-stroke-racing second=6 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.14884
two-stroke-racing second=7 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.147115
two-stroke-racing second=8 subnormal_cell_fields=0
two-stroke-racing-loaded-rusanov-3
solver=rusanov
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.147292
two-stroke-racing second=1 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.146841
two-stroke-racing second=2 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.146614
two-stroke-racing second=3 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.147836
two-stroke-racing second=4 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.147299
two-stroke-racing second=5 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.146466
two-stroke-racing second=6 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.14884
two-stroke-racing second=7 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.147115
two-stroke-racing second=8 subnormal_cell_fields=0
two-stroke-racing-transient-hllc-1
solver=hllc
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.0996217
two-stroke-racing second=1 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.172912
two-stroke-racing second=2 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.165154
two-stroke-racing second=3 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.172989
two-stroke-racing second=4 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.157416
two-stroke-racing second=5 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.174719
two-stroke-racing second=6 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.168114
two-stroke-racing second=7 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.120366
two-stroke-racing second=8 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.175854
two-stroke-racing second=9 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.176225
two-stroke-racing second=10 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.106232
two-stroke-racing second=11 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.0242671
two-stroke-racing second=12 subnormal_cell_fields=0
two-stroke-racing-transient-hlle-1
solver=hlle
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.0915528
two-stroke-racing second=1 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.0960099
two-stroke-racing second=2 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.163112
two-stroke-racing second=3 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.138994
two-stroke-racing second=4 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.0841626
two-stroke-racing second=5 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.0842203
two-stroke-racing second=6 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.0824561
two-stroke-racing second=7 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.0899877
two-stroke-racing second=8 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.0821239
two-stroke-racing second=9 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.0855026
two-stroke-racing second=10 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.090022
two-stroke-racing second=11 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.0308628
two-stroke-racing second=12 subnormal_cell_fields=0
two-stroke-racing-transient-rusanov-1
solver=rusanov
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.0897181
two-stroke-racing second=1 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.0923889
two-stroke-racing second=2 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.167296
two-stroke-racing second=3 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.166314
two-stroke-racing second=4 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.0860296
two-stroke-racing second=5 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.0898762
two-stroke-racing second=6 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.0874002
two-stroke-racing second=7 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.0938077
two-stroke-racing second=8 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.0911723
two-stroke-racing second=9 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.0908261
two-stroke-racing second=10 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.0871361
two-stroke-racing second=11 subnormal_cell_fields=0
two-stroke-racing repairs=0 hllc_fallbacks=0 peak=0.0293941
two-stroke-racing second=12 subnormal_cell_fields=0
v8-loaded-hllc-1
solver=hllc
v8 repairs=0 hllc_fallbacks=0 peak=0.12647
v8 second=1 subnormal_cell_fields=0
v8 iterations_per_frame=74.1809
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=34.5279
v8 rpm=4500.04 power_kW=240.547 peak=0.12647 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.112578
v8 second=2 subnormal_cell_fields=0
v8 iterations_per_frame=74.0101
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.2404
v8 rpm=4500.05 power_kW=240.424 peak=0.112578 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.112595
v8 second=3 subnormal_cell_fields=0
v8 iterations_per_frame=74.0167
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=34.6423
v8 rpm=4500.05 power_kW=240.383 peak=0.112595 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.112631
v8 second=4 subnormal_cell_fields=0
v8 iterations_per_frame=74.0099
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.2567
v8 rpm=4500.05 power_kW=240.336 peak=0.112631 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.112631
v8 second=5 subnormal_cell_fields=0
v8 iterations_per_frame=74.014
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=34.766
v8 rpm=4500.05 power_kW=240.269 peak=0.112631 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.112397
v8 second=6 subnormal_cell_fields=0
v8 iterations_per_frame=74.0091
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.2689
v8 rpm=4500.05 power_kW=240.715 peak=0.112397 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.112719
v8 second=7 subnormal_cell_fields=0
v8 iterations_per_frame=74.0131
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=34.8639
v8 rpm=4500.06 power_kW=240.317 peak=0.112719 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.112499
v8 second=8 subnormal_cell_fields=0
v8 iterations_per_frame=74.0126
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.2118
v8 rpm=4500.06 power_kW=240.425 peak=0.112499 hllc_fallbacks=0 pipe_repairs=0
v8-loaded-hllc-2
solver=hllc
v8 repairs=0 hllc_fallbacks=0 peak=0.12647
v8 second=1 subnormal_cell_fields=0
v8 iterations_per_frame=74.1809
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=34.5279
v8 rpm=4500.04 power_kW=240.547 peak=0.12647 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.112578
v8 second=2 subnormal_cell_fields=0
v8 iterations_per_frame=74.0101
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.2404
v8 rpm=4500.05 power_kW=240.424 peak=0.112578 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.112595
v8 second=3 subnormal_cell_fields=0
v8 iterations_per_frame=74.0167
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=34.6423
v8 rpm=4500.05 power_kW=240.383 peak=0.112595 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.112631
v8 second=4 subnormal_cell_fields=0
v8 iterations_per_frame=74.0099
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.2567
v8 rpm=4500.05 power_kW=240.336 peak=0.112631 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.112631
v8 second=5 subnormal_cell_fields=0
v8 iterations_per_frame=74.014
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=34.766
v8 rpm=4500.05 power_kW=240.269 peak=0.112631 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.112397
v8 second=6 subnormal_cell_fields=0
v8 iterations_per_frame=74.0091
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.2689
v8 rpm=4500.05 power_kW=240.715 peak=0.112397 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.112719
v8 second=7 subnormal_cell_fields=0
v8 iterations_per_frame=74.0131
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=34.8639
v8 rpm=4500.06 power_kW=240.317 peak=0.112719 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.112499
v8 second=8 subnormal_cell_fields=0
v8 iterations_per_frame=74.0126
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.2118
v8 rpm=4500.06 power_kW=240.425 peak=0.112499 hllc_fallbacks=0 pipe_repairs=0
v8-loaded-hllc-3
solver=hllc
v8 repairs=0 hllc_fallbacks=0 peak=0.12647
v8 second=1 subnormal_cell_fields=0
v8 iterations_per_frame=74.1809
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=34.5279
v8 rpm=4500.04 power_kW=240.547 peak=0.12647 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.112578
v8 second=2 subnormal_cell_fields=0
v8 iterations_per_frame=74.0101
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.2404
v8 rpm=4500.05 power_kW=240.424 peak=0.112578 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.112595
v8 second=3 subnormal_cell_fields=0
v8 iterations_per_frame=74.0167
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=34.6423
v8 rpm=4500.05 power_kW=240.383 peak=0.112595 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.112631
v8 second=4 subnormal_cell_fields=0
v8 iterations_per_frame=74.0099
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.2567
v8 rpm=4500.05 power_kW=240.336 peak=0.112631 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.112631
v8 second=5 subnormal_cell_fields=0
v8 iterations_per_frame=74.014
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=34.766
v8 rpm=4500.05 power_kW=240.269 peak=0.112631 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.112397
v8 second=6 subnormal_cell_fields=0
v8 iterations_per_frame=74.0091
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.2689
v8 rpm=4500.05 power_kW=240.715 peak=0.112397 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.112719
v8 second=7 subnormal_cell_fields=0
v8 iterations_per_frame=74.0131
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=34.8639
v8 rpm=4500.06 power_kW=240.317 peak=0.112719 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.112499
v8 second=8 subnormal_cell_fields=0
v8 iterations_per_frame=74.0126
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.2118
v8 rpm=4500.06 power_kW=240.425 peak=0.112499 hllc_fallbacks=0 pipe_repairs=0
v8-loaded-hlle-1
solver=hlle
v8 repairs=0 hllc_fallbacks=0 peak=0.128345
v8 second=1 subnormal_cell_fields=0
v8 iterations_per_frame=74.2236
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=34.6893
v8 rpm=4500.04 power_kW=240.715 peak=0.128345 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0824719
v8 second=2 subnormal_cell_fields=0
v8 iterations_per_frame=74.0479
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.3013
v8 rpm=4500.05 power_kW=240.592 peak=0.0824719 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0826311
v8 second=3 subnormal_cell_fields=0
v8 iterations_per_frame=74.0477
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=34.8155
v8 rpm=4500.05 power_kW=240.551 peak=0.0826311 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0825946
v8 second=4 subnormal_cell_fields=0
v8 iterations_per_frame=74.0466
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.1831
v8 rpm=4500.05 power_kW=240.502 peak=0.0825946 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0825547
v8 second=5 subnormal_cell_fields=0
v8 iterations_per_frame=74.048
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=34.9507
v8 rpm=4500.05 power_kW=240.435 peak=0.0825547 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0824446
v8 second=6 subnormal_cell_fields=0
v8 iterations_per_frame=74.0436
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.1893
v8 rpm=4500.05 power_kW=240.882 peak=0.0824446 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0826919
v8 second=7 subnormal_cell_fields=0
v8 iterations_per_frame=74.0449
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=35.0565
v8 rpm=4500.06 power_kW=240.485 peak=0.0826919 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0824734
v8 second=8 subnormal_cell_fields=0
v8 iterations_per_frame=74.045
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.1338
v8 rpm=4500.06 power_kW=240.592 peak=0.0824734 hllc_fallbacks=0 pipe_repairs=0
v8-loaded-hlle-2
solver=hlle
v8 repairs=0 hllc_fallbacks=0 peak=0.128345
v8 second=1 subnormal_cell_fields=0
v8 iterations_per_frame=74.2236
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=34.6893
v8 rpm=4500.04 power_kW=240.715 peak=0.128345 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0824719
v8 second=2 subnormal_cell_fields=0
v8 iterations_per_frame=74.0479
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.3013
v8 rpm=4500.05 power_kW=240.592 peak=0.0824719 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0826311
v8 second=3 subnormal_cell_fields=0
v8 iterations_per_frame=74.0477
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=34.8155
v8 rpm=4500.05 power_kW=240.551 peak=0.0826311 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0825946
v8 second=4 subnormal_cell_fields=0
v8 iterations_per_frame=74.0466
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.1831
v8 rpm=4500.05 power_kW=240.502 peak=0.0825946 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0825547
v8 second=5 subnormal_cell_fields=0
v8 iterations_per_frame=74.048
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=34.9507
v8 rpm=4500.05 power_kW=240.435 peak=0.0825547 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0824446
v8 second=6 subnormal_cell_fields=0
v8 iterations_per_frame=74.0436
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.1893
v8 rpm=4500.05 power_kW=240.882 peak=0.0824446 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0826919
v8 second=7 subnormal_cell_fields=0
v8 iterations_per_frame=74.0449
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=35.0565
v8 rpm=4500.06 power_kW=240.485 peak=0.0826919 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0824734
v8 second=8 subnormal_cell_fields=0
v8 iterations_per_frame=74.045
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.1338
v8 rpm=4500.06 power_kW=240.592 peak=0.0824734 hllc_fallbacks=0 pipe_repairs=0
v8-loaded-hlle-3
solver=hlle
v8 repairs=0 hllc_fallbacks=0 peak=0.128345
v8 second=1 subnormal_cell_fields=0
v8 iterations_per_frame=74.2236
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=34.6893
v8 rpm=4500.04 power_kW=240.715 peak=0.128345 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0824719
v8 second=2 subnormal_cell_fields=0
v8 iterations_per_frame=74.0479
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.3013
v8 rpm=4500.05 power_kW=240.592 peak=0.0824719 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0826311
v8 second=3 subnormal_cell_fields=0
v8 iterations_per_frame=74.0477
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=34.8155
v8 rpm=4500.05 power_kW=240.551 peak=0.0826311 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0825946
v8 second=4 subnormal_cell_fields=0
v8 iterations_per_frame=74.0466
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.1831
v8 rpm=4500.05 power_kW=240.502 peak=0.0825946 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0825547
v8 second=5 subnormal_cell_fields=0
v8 iterations_per_frame=74.048
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=34.9507
v8 rpm=4500.05 power_kW=240.435 peak=0.0825547 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0824446
v8 second=6 subnormal_cell_fields=0
v8 iterations_per_frame=74.0436
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.1893
v8 rpm=4500.05 power_kW=240.882 peak=0.0824446 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0826919
v8 second=7 subnormal_cell_fields=0
v8 iterations_per_frame=74.0449
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=35.0565
v8 rpm=4500.06 power_kW=240.485 peak=0.0826919 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0824734
v8 second=8 subnormal_cell_fields=0
v8 iterations_per_frame=74.045
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.1338
v8 rpm=4500.06 power_kW=240.592 peak=0.0824734 hllc_fallbacks=0 pipe_repairs=0
v8-loaded-rusanov-1
solver=rusanov
v8 repairs=0 hllc_fallbacks=0 peak=0.124651
v8 second=1 subnormal_cell_fields=0
v8 iterations_per_frame=74.1786
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=35.3066
v8 rpm=4500.04 power_kW=241.892 peak=0.124651 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0663823
v8 second=2 subnormal_cell_fields=0
v8 iterations_per_frame=74.0042
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=36.1845
v8 rpm=4500.05 power_kW=241.765 peak=0.0663823 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0662289
v8 second=3 subnormal_cell_fields=0
v8 iterations_per_frame=74.009
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=35.377
v8 rpm=4500.05 power_kW=241.726 peak=0.0662289 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0663436
v8 second=4 subnormal_cell_fields=0
v8 iterations_per_frame=74.0043
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=36.1616
v8 rpm=4500.05 power_kW=241.677 peak=0.0663436 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0662657
v8 second=5 subnormal_cell_fields=0
v8 iterations_per_frame=74.0068
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=35.4386
v8 rpm=4500.05 power_kW=241.609 peak=0.0662657 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0661241
v8 second=6 subnormal_cell_fields=0
v8 iterations_per_frame=74.0014
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=36.1844
v8 rpm=4500.05 power_kW=242.057 peak=0.0661241 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0662563
v8 second=7 subnormal_cell_fields=0
v8 iterations_per_frame=74.0088
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=35.4637
v8 rpm=4500.06 power_kW=241.658 peak=0.0662563 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.066231
v8 second=8 subnormal_cell_fields=0
v8 iterations_per_frame=74.0051
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=36.1327
v8 rpm=4500.06 power_kW=241.766 peak=0.066231 hllc_fallbacks=0 pipe_repairs=0
v8-loaded-rusanov-2
solver=rusanov
v8 repairs=0 hllc_fallbacks=0 peak=0.124651
v8 second=1 subnormal_cell_fields=0
v8 iterations_per_frame=74.1786
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=35.3066
v8 rpm=4500.04 power_kW=241.892 peak=0.124651 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0663823
v8 second=2 subnormal_cell_fields=0
v8 iterations_per_frame=74.0042
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=36.1845
v8 rpm=4500.05 power_kW=241.765 peak=0.0663823 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0662289
v8 second=3 subnormal_cell_fields=0
v8 iterations_per_frame=74.009
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=35.377
v8 rpm=4500.05 power_kW=241.726 peak=0.0662289 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0663436
v8 second=4 subnormal_cell_fields=0
v8 iterations_per_frame=74.0043
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=36.1616
v8 rpm=4500.05 power_kW=241.677 peak=0.0663436 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0662657
v8 second=5 subnormal_cell_fields=0
v8 iterations_per_frame=74.0068
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=35.4386
v8 rpm=4500.05 power_kW=241.609 peak=0.0662657 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0661241
v8 second=6 subnormal_cell_fields=0
v8 iterations_per_frame=74.0014
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=36.1844
v8 rpm=4500.05 power_kW=242.057 peak=0.0661241 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0662563
v8 second=7 subnormal_cell_fields=0
v8 iterations_per_frame=74.0088
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=35.4637
v8 rpm=4500.06 power_kW=241.658 peak=0.0662563 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.066231
v8 second=8 subnormal_cell_fields=0
v8 iterations_per_frame=74.0051
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=36.1327
v8 rpm=4500.06 power_kW=241.766 peak=0.066231 hllc_fallbacks=0 pipe_repairs=0
v8-loaded-rusanov-3
solver=rusanov
v8 repairs=0 hllc_fallbacks=0 peak=0.124651
v8 second=1 subnormal_cell_fields=0
v8 iterations_per_frame=74.1786
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=35.3066
v8 rpm=4500.04 power_kW=241.892 peak=0.124651 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0663823
v8 second=2 subnormal_cell_fields=0
v8 iterations_per_frame=74.0042
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=36.1845
v8 rpm=4500.05 power_kW=241.765 peak=0.0663823 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0662289
v8 second=3 subnormal_cell_fields=0
v8 iterations_per_frame=74.009
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=35.377
v8 rpm=4500.05 power_kW=241.726 peak=0.0662289 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0663436
v8 second=4 subnormal_cell_fields=0
v8 iterations_per_frame=74.0043
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=36.1616
v8 rpm=4500.05 power_kW=241.677 peak=0.0663436 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0662657
v8 second=5 subnormal_cell_fields=0
v8 iterations_per_frame=74.0068
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=35.4386
v8 rpm=4500.05 power_kW=241.609 peak=0.0662657 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0661241
v8 second=6 subnormal_cell_fields=0
v8 iterations_per_frame=74.0014
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=36.1844
v8 rpm=4500.05 power_kW=242.057 peak=0.0661241 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0662563
v8 second=7 subnormal_cell_fields=0
v8 iterations_per_frame=74.0088
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=35.4637
v8 rpm=4500.06 power_kW=241.658 peak=0.0662563 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.066231
v8 second=8 subnormal_cell_fields=0
v8 iterations_per_frame=74.0051
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=36.1327
v8 rpm=4500.06 power_kW=241.766 peak=0.066231 hllc_fallbacks=0 pipe_repairs=0
v8-transient-hllc-1
solver=hllc
v8 repairs=0 hllc_fallbacks=0 peak=0.0512003
v8 second=1 subnormal_cell_fields=0
v8 iterations_per_frame=73.86
v8 substeps_per_frame=1 limiting_pipe=7 cfl_us=50.4675
v8 rpm=1166.23 power_kW=-8.46316 peak=0.0512003 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.143051
v8 second=2 subnormal_cell_fields=0
v8 iterations_per_frame=73.3462
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=32.1854
v8 rpm=4960.12 power_kW=233.517 peak=0.143051 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0991083
v8 second=3 subnormal_cell_fields=0
v8 iterations_per_frame=75.052
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=43.9608
v8 rpm=6994.14 power_kW=11.1045 peak=0.0991083 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0945291
v8 second=4 subnormal_cell_fields=0
v8 iterations_per_frame=75.53
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=74.5907
v8 rpm=6908.73 power_kW=10.8923 peak=0.0945291 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0623547
v8 second=5 subnormal_cell_fields=0
v8 iterations_per_frame=75.5158
v8 substeps_per_frame=1 limiting_pipe=13 cfl_us=66.7933
v8 rpm=6824.11 power_kW=10.7361 peak=0.0623547 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0850739
v8 second=6 subnormal_cell_fields=0
v8 iterations_per_frame=75.5146
v8 substeps_per_frame=1 limiting_pipe=10 cfl_us=45.5696
v8 rpm=6779.04 power_kW=-50.6894 peak=0.0850739 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0854453
v8 second=7 subnormal_cell_fields=0
v8 iterations_per_frame=75.4563
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=52.6194
v8 rpm=6973.52 power_kW=11.0042 peak=0.0854453 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0944347
v8 second=8 subnormal_cell_fields=0
v8 iterations_per_frame=75.5016
v8 substeps_per_frame=1 limiting_pipe=13 cfl_us=61.338
v8 rpm=6890.5 power_kW=11.1764 peak=0.0944347 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.105143
v8 second=9 subnormal_cell_fields=0
v8 iterations_per_frame=75.5134
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=74.5004
v8 rpm=6808.18 power_kW=11.244 peak=0.105143 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0940691
v8 second=10 subnormal_cell_fields=0
v8 iterations_per_frame=75.4995
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.8785
v8 rpm=6858.5 power_kW=-29.5698 peak=0.0940691 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.136591
v8 second=11 subnormal_cell_fields=0
v8 iterations_per_frame=76.2322
v8 substeps_per_frame=1 limiting_pipe=15 cfl_us=30.063
v8 rpm=6116.1 power_kW=-55.1266 peak=0.136591 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0566888
v8 second=12 subnormal_cell_fields=0
v8 iterations_per_frame=76.5737
v8 substeps_per_frame=1 limiting_pipe=9 cfl_us=32.2902
v8 rpm=5424.69 power_kW=-45.8357 peak=0.0566888 hllc_fallbacks=0 pipe_repairs=0
v8-transient-hlle-1
solver=hlle
v8 repairs=0 hllc_fallbacks=0 peak=0.0561182
v8 second=1 subnormal_cell_fields=0
v8 iterations_per_frame=73.8786
v8 substeps_per_frame=1 limiting_pipe=13 cfl_us=67.4301
v8 rpm=1232.3 power_kW=-6.68 peak=0.0561182 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.128174
v8 second=2 subnormal_cell_fields=0
v8 iterations_per_frame=73.427
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=33.6684
v8 rpm=5128.83 power_kW=238.821 peak=0.128174 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.107174
v8 second=3 subnormal_cell_fields=0
v8 iterations_per_frame=75.14
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=63.408
v8 rpm=6970.96 power_kW=11.5256 peak=0.107174 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.101693
v8 second=4 subnormal_cell_fields=0
v8 iterations_per_frame=75.5065
v8 substeps_per_frame=1 limiting_pipe=9 cfl_us=64.1085
v8 rpm=6896.43 power_kW=11.7089 peak=0.101693 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0825038
v8 second=5 subnormal_cell_fields=0
v8 iterations_per_frame=75.5059
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=75.4988
v8 rpm=6818.91 power_kW=11.2748 peak=0.0825038 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.101304
v8 second=6 subnormal_cell_fields=0
v8 iterations_per_frame=75.5101
v8 substeps_per_frame=1 limiting_pipe=15 cfl_us=47.1498
v8 rpm=6770.12 power_kW=-52.1607 peak=0.101304 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0741981
v8 second=7 subnormal_cell_fields=0
v8 iterations_per_frame=75.4967
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=49.5771
v8 rpm=6975.67 power_kW=11.1096 peak=0.0741981 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.101455
v8 second=8 subnormal_cell_fields=0
v8 iterations_per_frame=75.5052
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=76.5938
v8 rpm=6900.55 power_kW=11.7703 peak=0.101455 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0745993
v8 second=9 subnormal_cell_fields=0
v8 iterations_per_frame=75.5312
v8 substeps_per_frame=1 limiting_pipe=14 cfl_us=72.0186
v8 rpm=6822.05 power_kW=11.4315 peak=0.0745993 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0832385
v8 second=10 subnormal_cell_fields=0
v8 iterations_per_frame=75.5029
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=76.774
v8 rpm=6760.54 power_kW=-54.5486 peak=0.0832385 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.109613
v8 second=11 subnormal_cell_fields=0
v8 iterations_per_frame=76.0489
v8 substeps_per_frame=1 limiting_pipe=13 cfl_us=33.0777
v8 rpm=6001.46 power_kW=-57.8174 peak=0.109613 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0744274
v8 second=12 subnormal_cell_fields=0
v8 iterations_per_frame=75.6918
v8 substeps_per_frame=1 limiting_pipe=9 cfl_us=38.1914
v8 rpm=5255.64 power_kW=-47.6221 peak=0.0744274 hllc_fallbacks=0 pipe_repairs=0
v8-transient-rusanov-1
solver=rusanov
v8 repairs=0 hllc_fallbacks=0 peak=0.0548538
v8 second=1 subnormal_cell_fields=0
v8 iterations_per_frame=73.8479
v8 substeps_per_frame=1 limiting_pipe=13 cfl_us=63.572
v8 rpm=1234.44 power_kW=-6.64865 peak=0.0548538 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.12885
v8 second=2 subnormal_cell_fields=0
v8 iterations_per_frame=73.3563
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=35.7206
v8 rpm=5133.66 power_kW=239.141 peak=0.12885 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.106975
v8 second=3 subnormal_cell_fields=0
v8 iterations_per_frame=75.0899
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=60.457
v8 rpm=6972.56 power_kW=11.317 peak=0.106975 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0566431
v8 second=4 subnormal_cell_fields=0
v8 iterations_per_frame=75.4854
v8 substeps_per_frame=1 limiting_pipe=13 cfl_us=68.7429
v8 rpm=6898.57 power_kW=12.0271 peak=0.0566431 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0881226
v8 second=5 subnormal_cell_fields=0
v8 iterations_per_frame=75.4583
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=75.1851
v8 rpm=6825.62 power_kW=11.9413 peak=0.0881226 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0959038
v8 second=6 subnormal_cell_fields=0
v8 iterations_per_frame=75.4575
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=74.7684
v8 rpm=6752.19 power_kW=-51.7815 peak=0.0959038 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0884333
v8 second=7 subnormal_cell_fields=0
v8 iterations_per_frame=75.4309
v8 substeps_per_frame=1 limiting_pipe=13 cfl_us=47.8691
v8 rpm=6993.86 power_kW=12.0499 peak=0.0884333 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0792929
v8 second=8 subnormal_cell_fields=0
v8 iterations_per_frame=75.47
v8 substeps_per_frame=1 limiting_pipe=18 cfl_us=75.7269
v8 rpm=6918.5 power_kW=11.7127 peak=0.0792929 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0960156
v8 second=9 subnormal_cell_fields=0
v8 iterations_per_frame=75.4588
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=74.4959
v8 rpm=6845.68 power_kW=12.1654 peak=0.0960156 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0762166
v8 second=10 subnormal_cell_fields=0
v8 iterations_per_frame=75.4839
v8 substeps_per_frame=1 limiting_pipe=17 cfl_us=80.1748
v8 rpm=6769.38 power_kW=-26.5415 peak=0.0762166 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0996633
v8 second=11 subnormal_cell_fields=0
v8 iterations_per_frame=76.0188
v8 substeps_per_frame=1 limiting_pipe=13 cfl_us=41.3481
v8 rpm=6006.96 power_kW=-57.6712 peak=0.0996633 hllc_fallbacks=0 pipe_repairs=0
v8 repairs=0 hllc_fallbacks=0 peak=0.0698359
v8 second=12 subnormal_cell_fields=0
v8 iterations_per_frame=75.7186
v8 substeps_per_frame=1 limiting_pipe=13 cfl_us=33.4053
v8 rpm=5262.18 power_kW=-47.6176 peak=0.0698359 hllc_fallbacks=0 pipe_repairs=0

View File

@@ -0,0 +1,217 @@
scenario,mode,solver,repeat,second,wall_seconds,checksum
two-stroke-racing,loaded,hllc,1,1,0.898907,0.375504
two-stroke-racing,loaded,hllc,1,2,0.890372,-0.575059
two-stroke-racing,loaded,hllc,1,3,0.915618,0.0554895
two-stroke-racing,loaded,hllc,1,4,0.892088,0.354059
two-stroke-racing,loaded,hllc,1,5,0.88714,-0.558311
two-stroke-racing,loaded,hllc,1,6,0.88841,0.0691558
two-stroke-racing,loaded,hllc,1,7,0.893041,0.324915
two-stroke-racing,loaded,hllc,1,8,0.892857,-0.539058
two-stroke-racing,loaded,hllc,2,1,0.906924,0.375504
two-stroke-racing,loaded,hllc,2,2,0.896024,-0.575059
two-stroke-racing,loaded,hllc,2,3,0.881232,0.0554895
two-stroke-racing,loaded,hllc,2,4,0.883484,0.354059
two-stroke-racing,loaded,hllc,2,5,0.881763,-0.558311
two-stroke-racing,loaded,hllc,2,6,0.886815,0.0691558
two-stroke-racing,loaded,hllc,2,7,0.885594,0.324915
two-stroke-racing,loaded,hllc,2,8,0.88354,-0.539058
two-stroke-racing,loaded,hllc,3,1,0.905856,0.375504
two-stroke-racing,loaded,hllc,3,2,0.885781,-0.575059
two-stroke-racing,loaded,hllc,3,3,0.888772,0.0554895
two-stroke-racing,loaded,hllc,3,4,0.909393,0.354059
two-stroke-racing,loaded,hllc,3,5,0.88774,-0.558311
two-stroke-racing,loaded,hllc,3,6,0.885598,0.0691558
two-stroke-racing,loaded,hllc,3,7,0.902796,0.324915
two-stroke-racing,loaded,hllc,3,8,0.892952,-0.539058
two-stroke-racing,loaded,hlle,1,1,0.874236,0.43566
two-stroke-racing,loaded,hlle,1,2,0.832059,-0.562762
two-stroke-racing,loaded,hlle,1,3,0.827599,0.0365935
two-stroke-racing,loaded,hlle,1,4,0.821388,0.40405
two-stroke-racing,loaded,hlle,1,5,0.80107,-0.562104
two-stroke-racing,loaded,hlle,1,6,0.804767,0.0268677
two-stroke-racing,loaded,hlle,1,7,0.797548,0.369049
two-stroke-racing,loaded,hlle,1,8,0.794501,-0.552236
two-stroke-racing,loaded,hlle,2,1,0.83538,0.43566
two-stroke-racing,loaded,hlle,2,2,0.810791,-0.562762
two-stroke-racing,loaded,hlle,2,3,0.789608,0.0365935
two-stroke-racing,loaded,hlle,2,4,0.796431,0.40405
two-stroke-racing,loaded,hlle,2,5,0.791847,-0.562104
two-stroke-racing,loaded,hlle,2,6,0.789067,0.0268677
two-stroke-racing,loaded,hlle,2,7,0.788781,0.369049
two-stroke-racing,loaded,hlle,2,8,0.791738,-0.552236
two-stroke-racing,loaded,hlle,3,1,0.819132,0.43566
two-stroke-racing,loaded,hlle,3,2,0.790674,-0.562762
two-stroke-racing,loaded,hlle,3,3,0.788079,0.0365935
two-stroke-racing,loaded,hlle,3,4,0.797116,0.40405
two-stroke-racing,loaded,hlle,3,5,0.78818,-0.562104
two-stroke-racing,loaded,hlle,3,6,0.788718,0.0268677
two-stroke-racing,loaded,hlle,3,7,0.786934,0.369049
two-stroke-racing,loaded,hlle,3,8,0.790049,-0.552236
two-stroke-racing,loaded,rusanov,1,1,0.562815,0.432643
two-stroke-racing,loaded,rusanov,1,2,0.543635,-0.563588
two-stroke-racing,loaded,rusanov,1,3,0.557183,0.0358499
two-stroke-racing,loaded,rusanov,1,4,0.544435,0.402933
two-stroke-racing,loaded,rusanov,1,5,0.546818,-0.562433
two-stroke-racing,loaded,rusanov,1,6,0.543303,0.0223812
two-stroke-racing,loaded,rusanov,1,7,0.567618,0.369953
two-stroke-racing,loaded,rusanov,1,8,0.568245,-0.552817
two-stroke-racing,loaded,rusanov,2,1,0.564288,0.432643
two-stroke-racing,loaded,rusanov,2,2,0.545886,-0.563588
two-stroke-racing,loaded,rusanov,2,3,0.543808,0.0358499
two-stroke-racing,loaded,rusanov,2,4,0.542763,0.402933
two-stroke-racing,loaded,rusanov,2,5,0.554005,-0.562433
two-stroke-racing,loaded,rusanov,2,6,0.5459,0.0223812
two-stroke-racing,loaded,rusanov,2,7,0.546832,0.369953
two-stroke-racing,loaded,rusanov,2,8,0.546307,-0.552817
two-stroke-racing,loaded,rusanov,3,1,0.558141,0.432643
two-stroke-racing,loaded,rusanov,3,2,0.543952,-0.563588
two-stroke-racing,loaded,rusanov,3,3,0.545782,0.0358499
two-stroke-racing,loaded,rusanov,3,4,0.543825,0.402933
two-stroke-racing,loaded,rusanov,3,5,0.547158,-0.562433
two-stroke-racing,loaded,rusanov,3,6,0.558153,0.0223812
two-stroke-racing,loaded,rusanov,3,7,0.550717,0.369953
two-stroke-racing,loaded,rusanov,3,8,0.548038,-0.552817
v8,loaded,hllc,1,1,0.51712,-1.25789
v8,loaded,hllc,1,2,0.507211,0.0914825
v8,loaded,hllc,1,3,0.493042,-1.35427
v8,loaded,hllc,1,4,0.493428,0.11053
v8,loaded,hllc,1,5,0.494402,-1.46657
v8,loaded,hllc,1,6,0.497352,0.105948
v8,loaded,hllc,1,7,0.496204,-1.55901
v8,loaded,hllc,1,8,0.498245,0.116575
v8,loaded,hllc,2,1,0.541912,-1.25789
v8,loaded,hllc,2,2,0.528779,0.0914825
v8,loaded,hllc,2,3,0.534608,-1.35427
v8,loaded,hllc,2,4,0.519766,0.11053
v8,loaded,hllc,2,5,0.523768,-1.46657
v8,loaded,hllc,2,6,0.513363,0.105948
v8,loaded,hllc,2,7,0.502696,-1.55901
v8,loaded,hllc,2,8,0.517714,0.116575
v8,loaded,hllc,3,1,0.507043,-1.25789
v8,loaded,hllc,3,2,0.493548,0.0914825
v8,loaded,hllc,3,3,0.495322,-1.35427
v8,loaded,hllc,3,4,0.49828,0.11053
v8,loaded,hllc,3,5,0.494915,-1.46657
v8,loaded,hllc,3,6,0.49499,0.105948
v8,loaded,hllc,3,7,0.491214,-1.55901
v8,loaded,hllc,3,8,0.498495,0.116575
v8,loaded,hlle,1,1,0.479082,-0.968916
v8,loaded,hlle,1,2,0.460545,0.426525
v8,loaded,hlle,1,3,0.460356,-1.04861
v8,loaded,hlle,1,4,0.458321,0.43059
v8,loaded,hlle,1,5,0.457712,-1.1376
v8,loaded,hlle,1,6,0.457303,0.411077
v8,loaded,hlle,1,7,0.459832,-1.21676
v8,loaded,hlle,1,8,0.457624,0.408273
v8,loaded,hlle,2,1,0.476108,-0.968916
v8,loaded,hlle,2,2,0.466478,0.426525
v8,loaded,hlle,2,3,0.460536,-1.04861
v8,loaded,hlle,2,4,0.459763,0.43059
v8,loaded,hlle,2,5,0.457158,-1.1376
v8,loaded,hlle,2,6,0.459958,0.411077
v8,loaded,hlle,2,7,0.456695,-1.21676
v8,loaded,hlle,2,8,0.45772,0.408273
v8,loaded,hlle,3,1,0.487149,-0.968916
v8,loaded,hlle,3,2,0.473019,0.426525
v8,loaded,hlle,3,3,0.456872,-1.04861
v8,loaded,hlle,3,4,0.457474,0.43059
v8,loaded,hlle,3,5,0.483505,-1.1376
v8,loaded,hlle,3,6,0.475632,0.411077
v8,loaded,hlle,3,7,0.459302,-1.21676
v8,loaded,hlle,3,8,0.457047,0.408273
v8,loaded,rusanov,1,1,0.39771,-1.25887
v8,loaded,rusanov,1,2,0.375476,0.36172
v8,loaded,rusanov,1,3,0.37258,-1.34285
v8,loaded,rusanov,1,4,0.373225,0.355362
v8,loaded,rusanov,1,5,0.372021,-1.43271
v8,loaded,rusanov,1,6,0.371566,0.329141
v8,loaded,rusanov,1,7,0.374192,-1.51861
v8,loaded,rusanov,1,8,0.371875,0.318454
v8,loaded,rusanov,2,1,0.393405,-1.25887
v8,loaded,rusanov,2,2,0.382959,0.36172
v8,loaded,rusanov,2,3,0.388557,-1.34285
v8,loaded,rusanov,2,4,0.38115,0.355362
v8,loaded,rusanov,2,5,0.376133,-1.43271
v8,loaded,rusanov,2,6,0.373073,0.329141
v8,loaded,rusanov,2,7,0.372073,-1.51861
v8,loaded,rusanov,2,8,0.372234,0.318454
v8,loaded,rusanov,3,1,0.380908,-1.25887
v8,loaded,rusanov,3,2,0.378432,0.36172
v8,loaded,rusanov,3,3,0.372992,-1.34285
v8,loaded,rusanov,3,4,0.378921,0.355362
v8,loaded,rusanov,3,5,0.376067,-1.43271
v8,loaded,rusanov,3,6,0.389736,0.329141
v8,loaded,rusanov,3,7,0.399614,-1.51861
v8,loaded,rusanov,3,8,0.394655,0.318454
two-stroke-racing,transient,hllc,1,1,0.900677,0.047932
two-stroke-racing,transient,hllc,1,2,0.888808,0.78033
two-stroke-racing,transient,hllc,1,3,0.910981,0.325172
two-stroke-racing,transient,hllc,1,4,0.900632,-0.297369
two-stroke-racing,transient,hllc,1,5,0.897301,-0.303403
two-stroke-racing,transient,hllc,1,6,0.919263,-0.103515
two-stroke-racing,transient,hllc,1,7,0.909823,0.350997
two-stroke-racing,transient,hllc,1,8,0.895161,0.113019
two-stroke-racing,transient,hllc,1,9,0.902135,-0.18787
two-stroke-racing,transient,hllc,1,10,0.899785,-0.375214
two-stroke-racing,transient,hllc,1,11,0.886202,-0.192638
two-stroke-racing,transient,hllc,1,12,0.86722,-0.0422888
two-stroke-racing,transient,hlle,1,1,0.797276,-0.0693344
two-stroke-racing,transient,hlle,1,2,0.792808,-0.0564268
two-stroke-racing,transient,hlle,1,3,0.817458,0.235254
two-stroke-racing,transient,hlle,1,4,0.797844,-0.145124
two-stroke-racing,transient,hlle,1,5,0.792606,0.133833
two-stroke-racing,transient,hlle,1,6,0.796034,-0.104748
two-stroke-racing,transient,hlle,1,7,0.79525,0.0463649
two-stroke-racing,transient,hlle,1,8,0.804991,0.274437
two-stroke-racing,transient,hlle,1,9,0.793281,0.221262
two-stroke-racing,transient,hlle,1,10,0.794076,-0.219595
two-stroke-racing,transient,hlle,1,11,0.788833,-0.0168035
two-stroke-racing,transient,hlle,1,12,0.768442,0.00294675
two-stroke-racing,transient,rusanov,1,1,0.56353,-0.00949596
two-stroke-racing,transient,rusanov,1,2,0.542585,0.343048
two-stroke-racing,transient,rusanov,1,3,0.555265,0.0393343
two-stroke-racing,transient,rusanov,1,4,0.548917,0.264534
two-stroke-racing,transient,rusanov,1,5,0.547014,-0.358644
two-stroke-racing,transient,rusanov,1,6,0.547,-0.097238
two-stroke-racing,transient,rusanov,1,7,0.550593,-0.327873
two-stroke-racing,transient,rusanov,1,8,0.557342,-0.0399539
two-stroke-racing,transient,rusanov,1,9,0.555567,0.267054
two-stroke-racing,transient,rusanov,1,10,0.551099,-0.339717
two-stroke-racing,transient,rusanov,1,11,0.544649,-0.124783
two-stroke-racing,transient,rusanov,1,12,0.519626,-0.00297952
v8,transient,hllc,1,1,0.507561,0.505224
v8,transient,hllc,1,2,0.494128,1.06866
v8,transient,hllc,1,3,0.496893,-4.03643
v8,transient,hllc,1,4,0.507198,-0.313736
v8,transient,hllc,1,5,0.504568,-0.589717
v8,transient,hllc,1,6,0.504926,-0.645144
v8,transient,hllc,1,7,0.500433,0.307813
v8,transient,hllc,1,8,0.495754,-0.271564
v8,transient,hllc,1,9,0.497857,-0.290266
v8,transient,hllc,1,10,0.497437,7.25865
v8,transient,hllc,1,11,0.49726,0.24476
v8,transient,hllc,1,12,0.497758,-0.102464
v8,transient,hlle,1,1,0.469088,-0.615984
v8,transient,hlle,1,2,0.462305,1.60934
v8,transient,hlle,1,3,0.462143,0.216284
v8,transient,hlle,1,4,0.466875,0.0425016
v8,transient,hlle,1,5,0.484561,-0.199247
v8,transient,hlle,1,6,0.475605,-0.0859847
v8,transient,hlle,1,7,0.470984,4.30145
v8,transient,hlle,1,8,0.461019,0.301313
v8,transient,hlle,1,9,0.470204,-0.448985
v8,transient,hlle,1,10,0.465713,-0.660645
v8,transient,hlle,1,11,0.470345,-1.20044
v8,transient,hlle,1,12,0.483598,-0.110843
v8,transient,rusanov,1,1,0.387003,-0.246586
v8,transient,rusanov,1,2,0.379741,0.988927
v8,transient,rusanov,1,3,0.376149,1.30821
v8,transient,rusanov,1,4,0.379127,-0.258464
v8,transient,rusanov,1,5,0.375879,-0.675008
v8,transient,rusanov,1,6,0.379759,0.268399
v8,transient,rusanov,1,7,0.389442,-1.3748
v8,transient,rusanov,1,8,0.390952,-0.0903278
v8,transient,rusanov,1,9,0.385831,0.23786
v8,transient,rusanov,1,10,0.384152,-0.0822659
v8,transient,rusanov,1,11,0.380412,1.83047
v8,transient,rusanov,1,12,0.385969,1.43902
1 scenario mode solver repeat second wall_seconds checksum
2 two-stroke-racing loaded hllc 1 1 0.898907 0.375504
3 two-stroke-racing loaded hllc 1 2 0.890372 -0.575059
4 two-stroke-racing loaded hllc 1 3 0.915618 0.0554895
5 two-stroke-racing loaded hllc 1 4 0.892088 0.354059
6 two-stroke-racing loaded hllc 1 5 0.88714 -0.558311
7 two-stroke-racing loaded hllc 1 6 0.88841 0.0691558
8 two-stroke-racing loaded hllc 1 7 0.893041 0.324915
9 two-stroke-racing loaded hllc 1 8 0.892857 -0.539058
10 two-stroke-racing loaded hllc 2 1 0.906924 0.375504
11 two-stroke-racing loaded hllc 2 2 0.896024 -0.575059
12 two-stroke-racing loaded hllc 2 3 0.881232 0.0554895
13 two-stroke-racing loaded hllc 2 4 0.883484 0.354059
14 two-stroke-racing loaded hllc 2 5 0.881763 -0.558311
15 two-stroke-racing loaded hllc 2 6 0.886815 0.0691558
16 two-stroke-racing loaded hllc 2 7 0.885594 0.324915
17 two-stroke-racing loaded hllc 2 8 0.88354 -0.539058
18 two-stroke-racing loaded hllc 3 1 0.905856 0.375504
19 two-stroke-racing loaded hllc 3 2 0.885781 -0.575059
20 two-stroke-racing loaded hllc 3 3 0.888772 0.0554895
21 two-stroke-racing loaded hllc 3 4 0.909393 0.354059
22 two-stroke-racing loaded hllc 3 5 0.88774 -0.558311
23 two-stroke-racing loaded hllc 3 6 0.885598 0.0691558
24 two-stroke-racing loaded hllc 3 7 0.902796 0.324915
25 two-stroke-racing loaded hllc 3 8 0.892952 -0.539058
26 two-stroke-racing loaded hlle 1 1 0.874236 0.43566
27 two-stroke-racing loaded hlle 1 2 0.832059 -0.562762
28 two-stroke-racing loaded hlle 1 3 0.827599 0.0365935
29 two-stroke-racing loaded hlle 1 4 0.821388 0.40405
30 two-stroke-racing loaded hlle 1 5 0.80107 -0.562104
31 two-stroke-racing loaded hlle 1 6 0.804767 0.0268677
32 two-stroke-racing loaded hlle 1 7 0.797548 0.369049
33 two-stroke-racing loaded hlle 1 8 0.794501 -0.552236
34 two-stroke-racing loaded hlle 2 1 0.83538 0.43566
35 two-stroke-racing loaded hlle 2 2 0.810791 -0.562762
36 two-stroke-racing loaded hlle 2 3 0.789608 0.0365935
37 two-stroke-racing loaded hlle 2 4 0.796431 0.40405
38 two-stroke-racing loaded hlle 2 5 0.791847 -0.562104
39 two-stroke-racing loaded hlle 2 6 0.789067 0.0268677
40 two-stroke-racing loaded hlle 2 7 0.788781 0.369049
41 two-stroke-racing loaded hlle 2 8 0.791738 -0.552236
42 two-stroke-racing loaded hlle 3 1 0.819132 0.43566
43 two-stroke-racing loaded hlle 3 2 0.790674 -0.562762
44 two-stroke-racing loaded hlle 3 3 0.788079 0.0365935
45 two-stroke-racing loaded hlle 3 4 0.797116 0.40405
46 two-stroke-racing loaded hlle 3 5 0.78818 -0.562104
47 two-stroke-racing loaded hlle 3 6 0.788718 0.0268677
48 two-stroke-racing loaded hlle 3 7 0.786934 0.369049
49 two-stroke-racing loaded hlle 3 8 0.790049 -0.552236
50 two-stroke-racing loaded rusanov 1 1 0.562815 0.432643
51 two-stroke-racing loaded rusanov 1 2 0.543635 -0.563588
52 two-stroke-racing loaded rusanov 1 3 0.557183 0.0358499
53 two-stroke-racing loaded rusanov 1 4 0.544435 0.402933
54 two-stroke-racing loaded rusanov 1 5 0.546818 -0.562433
55 two-stroke-racing loaded rusanov 1 6 0.543303 0.0223812
56 two-stroke-racing loaded rusanov 1 7 0.567618 0.369953
57 two-stroke-racing loaded rusanov 1 8 0.568245 -0.552817
58 two-stroke-racing loaded rusanov 2 1 0.564288 0.432643
59 two-stroke-racing loaded rusanov 2 2 0.545886 -0.563588
60 two-stroke-racing loaded rusanov 2 3 0.543808 0.0358499
61 two-stroke-racing loaded rusanov 2 4 0.542763 0.402933
62 two-stroke-racing loaded rusanov 2 5 0.554005 -0.562433
63 two-stroke-racing loaded rusanov 2 6 0.5459 0.0223812
64 two-stroke-racing loaded rusanov 2 7 0.546832 0.369953
65 two-stroke-racing loaded rusanov 2 8 0.546307 -0.552817
66 two-stroke-racing loaded rusanov 3 1 0.558141 0.432643
67 two-stroke-racing loaded rusanov 3 2 0.543952 -0.563588
68 two-stroke-racing loaded rusanov 3 3 0.545782 0.0358499
69 two-stroke-racing loaded rusanov 3 4 0.543825 0.402933
70 two-stroke-racing loaded rusanov 3 5 0.547158 -0.562433
71 two-stroke-racing loaded rusanov 3 6 0.558153 0.0223812
72 two-stroke-racing loaded rusanov 3 7 0.550717 0.369953
73 two-stroke-racing loaded rusanov 3 8 0.548038 -0.552817
74 v8 loaded hllc 1 1 0.51712 -1.25789
75 v8 loaded hllc 1 2 0.507211 0.0914825
76 v8 loaded hllc 1 3 0.493042 -1.35427
77 v8 loaded hllc 1 4 0.493428 0.11053
78 v8 loaded hllc 1 5 0.494402 -1.46657
79 v8 loaded hllc 1 6 0.497352 0.105948
80 v8 loaded hllc 1 7 0.496204 -1.55901
81 v8 loaded hllc 1 8 0.498245 0.116575
82 v8 loaded hllc 2 1 0.541912 -1.25789
83 v8 loaded hllc 2 2 0.528779 0.0914825
84 v8 loaded hllc 2 3 0.534608 -1.35427
85 v8 loaded hllc 2 4 0.519766 0.11053
86 v8 loaded hllc 2 5 0.523768 -1.46657
87 v8 loaded hllc 2 6 0.513363 0.105948
88 v8 loaded hllc 2 7 0.502696 -1.55901
89 v8 loaded hllc 2 8 0.517714 0.116575
90 v8 loaded hllc 3 1 0.507043 -1.25789
91 v8 loaded hllc 3 2 0.493548 0.0914825
92 v8 loaded hllc 3 3 0.495322 -1.35427
93 v8 loaded hllc 3 4 0.49828 0.11053
94 v8 loaded hllc 3 5 0.494915 -1.46657
95 v8 loaded hllc 3 6 0.49499 0.105948
96 v8 loaded hllc 3 7 0.491214 -1.55901
97 v8 loaded hllc 3 8 0.498495 0.116575
98 v8 loaded hlle 1 1 0.479082 -0.968916
99 v8 loaded hlle 1 2 0.460545 0.426525
100 v8 loaded hlle 1 3 0.460356 -1.04861
101 v8 loaded hlle 1 4 0.458321 0.43059
102 v8 loaded hlle 1 5 0.457712 -1.1376
103 v8 loaded hlle 1 6 0.457303 0.411077
104 v8 loaded hlle 1 7 0.459832 -1.21676
105 v8 loaded hlle 1 8 0.457624 0.408273
106 v8 loaded hlle 2 1 0.476108 -0.968916
107 v8 loaded hlle 2 2 0.466478 0.426525
108 v8 loaded hlle 2 3 0.460536 -1.04861
109 v8 loaded hlle 2 4 0.459763 0.43059
110 v8 loaded hlle 2 5 0.457158 -1.1376
111 v8 loaded hlle 2 6 0.459958 0.411077
112 v8 loaded hlle 2 7 0.456695 -1.21676
113 v8 loaded hlle 2 8 0.45772 0.408273
114 v8 loaded hlle 3 1 0.487149 -0.968916
115 v8 loaded hlle 3 2 0.473019 0.426525
116 v8 loaded hlle 3 3 0.456872 -1.04861
117 v8 loaded hlle 3 4 0.457474 0.43059
118 v8 loaded hlle 3 5 0.483505 -1.1376
119 v8 loaded hlle 3 6 0.475632 0.411077
120 v8 loaded hlle 3 7 0.459302 -1.21676
121 v8 loaded hlle 3 8 0.457047 0.408273
122 v8 loaded rusanov 1 1 0.39771 -1.25887
123 v8 loaded rusanov 1 2 0.375476 0.36172
124 v8 loaded rusanov 1 3 0.37258 -1.34285
125 v8 loaded rusanov 1 4 0.373225 0.355362
126 v8 loaded rusanov 1 5 0.372021 -1.43271
127 v8 loaded rusanov 1 6 0.371566 0.329141
128 v8 loaded rusanov 1 7 0.374192 -1.51861
129 v8 loaded rusanov 1 8 0.371875 0.318454
130 v8 loaded rusanov 2 1 0.393405 -1.25887
131 v8 loaded rusanov 2 2 0.382959 0.36172
132 v8 loaded rusanov 2 3 0.388557 -1.34285
133 v8 loaded rusanov 2 4 0.38115 0.355362
134 v8 loaded rusanov 2 5 0.376133 -1.43271
135 v8 loaded rusanov 2 6 0.373073 0.329141
136 v8 loaded rusanov 2 7 0.372073 -1.51861
137 v8 loaded rusanov 2 8 0.372234 0.318454
138 v8 loaded rusanov 3 1 0.380908 -1.25887
139 v8 loaded rusanov 3 2 0.378432 0.36172
140 v8 loaded rusanov 3 3 0.372992 -1.34285
141 v8 loaded rusanov 3 4 0.378921 0.355362
142 v8 loaded rusanov 3 5 0.376067 -1.43271
143 v8 loaded rusanov 3 6 0.389736 0.329141
144 v8 loaded rusanov 3 7 0.399614 -1.51861
145 v8 loaded rusanov 3 8 0.394655 0.318454
146 two-stroke-racing transient hllc 1 1 0.900677 0.047932
147 two-stroke-racing transient hllc 1 2 0.888808 0.78033
148 two-stroke-racing transient hllc 1 3 0.910981 0.325172
149 two-stroke-racing transient hllc 1 4 0.900632 -0.297369
150 two-stroke-racing transient hllc 1 5 0.897301 -0.303403
151 two-stroke-racing transient hllc 1 6 0.919263 -0.103515
152 two-stroke-racing transient hllc 1 7 0.909823 0.350997
153 two-stroke-racing transient hllc 1 8 0.895161 0.113019
154 two-stroke-racing transient hllc 1 9 0.902135 -0.18787
155 two-stroke-racing transient hllc 1 10 0.899785 -0.375214
156 two-stroke-racing transient hllc 1 11 0.886202 -0.192638
157 two-stroke-racing transient hllc 1 12 0.86722 -0.0422888
158 two-stroke-racing transient hlle 1 1 0.797276 -0.0693344
159 two-stroke-racing transient hlle 1 2 0.792808 -0.0564268
160 two-stroke-racing transient hlle 1 3 0.817458 0.235254
161 two-stroke-racing transient hlle 1 4 0.797844 -0.145124
162 two-stroke-racing transient hlle 1 5 0.792606 0.133833
163 two-stroke-racing transient hlle 1 6 0.796034 -0.104748
164 two-stroke-racing transient hlle 1 7 0.79525 0.0463649
165 two-stroke-racing transient hlle 1 8 0.804991 0.274437
166 two-stroke-racing transient hlle 1 9 0.793281 0.221262
167 two-stroke-racing transient hlle 1 10 0.794076 -0.219595
168 two-stroke-racing transient hlle 1 11 0.788833 -0.0168035
169 two-stroke-racing transient hlle 1 12 0.768442 0.00294675
170 two-stroke-racing transient rusanov 1 1 0.56353 -0.00949596
171 two-stroke-racing transient rusanov 1 2 0.542585 0.343048
172 two-stroke-racing transient rusanov 1 3 0.555265 0.0393343
173 two-stroke-racing transient rusanov 1 4 0.548917 0.264534
174 two-stroke-racing transient rusanov 1 5 0.547014 -0.358644
175 two-stroke-racing transient rusanov 1 6 0.547 -0.097238
176 two-stroke-racing transient rusanov 1 7 0.550593 -0.327873
177 two-stroke-racing transient rusanov 1 8 0.557342 -0.0399539
178 two-stroke-racing transient rusanov 1 9 0.555567 0.267054
179 two-stroke-racing transient rusanov 1 10 0.551099 -0.339717
180 two-stroke-racing transient rusanov 1 11 0.544649 -0.124783
181 two-stroke-racing transient rusanov 1 12 0.519626 -0.00297952
182 v8 transient hllc 1 1 0.507561 0.505224
183 v8 transient hllc 1 2 0.494128 1.06866
184 v8 transient hllc 1 3 0.496893 -4.03643
185 v8 transient hllc 1 4 0.507198 -0.313736
186 v8 transient hllc 1 5 0.504568 -0.589717
187 v8 transient hllc 1 6 0.504926 -0.645144
188 v8 transient hllc 1 7 0.500433 0.307813
189 v8 transient hllc 1 8 0.495754 -0.271564
190 v8 transient hllc 1 9 0.497857 -0.290266
191 v8 transient hllc 1 10 0.497437 7.25865
192 v8 transient hllc 1 11 0.49726 0.24476
193 v8 transient hllc 1 12 0.497758 -0.102464
194 v8 transient hlle 1 1 0.469088 -0.615984
195 v8 transient hlle 1 2 0.462305 1.60934
196 v8 transient hlle 1 3 0.462143 0.216284
197 v8 transient hlle 1 4 0.466875 0.0425016
198 v8 transient hlle 1 5 0.484561 -0.199247
199 v8 transient hlle 1 6 0.475605 -0.0859847
200 v8 transient hlle 1 7 0.470984 4.30145
201 v8 transient hlle 1 8 0.461019 0.301313
202 v8 transient hlle 1 9 0.470204 -0.448985
203 v8 transient hlle 1 10 0.465713 -0.660645
204 v8 transient hlle 1 11 0.470345 -1.20044
205 v8 transient hlle 1 12 0.483598 -0.110843
206 v8 transient rusanov 1 1 0.387003 -0.246586
207 v8 transient rusanov 1 2 0.379741 0.988927
208 v8 transient rusanov 1 3 0.376149 1.30821
209 v8 transient rusanov 1 4 0.379127 -0.258464
210 v8 transient rusanov 1 5 0.375879 -0.675008
211 v8 transient rusanov 1 6 0.379759 0.268399
212 v8 transient rusanov 1 7 0.389442 -1.3748
213 v8 transient rusanov 1 8 0.390952 -0.0903278
214 v8 transient rusanov 1 9 0.385831 0.23786
215 v8 transient rusanov 1 10 0.384152 -0.0822659
216 v8 transient rusanov 1 11 0.380412 1.83047
217 v8 transient rusanov 1 12 0.385969 1.43902

View File

@@ -0,0 +1,217 @@
scenario,mode,solver,repeat,second,wall_seconds,checksum
two-stroke-racing,loaded,hllc,1,1,1.0345,0.375503
two-stroke-racing,loaded,hllc,1,2,1.01643,-0.575062
two-stroke-racing,loaded,hllc,1,3,1.00805,0.055486
two-stroke-racing,loaded,hllc,1,4,1.02154,0.354056
two-stroke-racing,loaded,hllc,1,5,1.00972,-0.558315
two-stroke-racing,loaded,hllc,1,6,1.01332,0.0691515
two-stroke-racing,loaded,hllc,1,7,1.02021,0.324911
two-stroke-racing,loaded,hllc,1,8,1.00968,-0.539062
two-stroke-racing,loaded,hllc,2,1,1.03723,0.375503
two-stroke-racing,loaded,hllc,2,2,1.0184,-0.575062
two-stroke-racing,loaded,hllc,2,3,1.0148,0.055486
two-stroke-racing,loaded,hllc,2,4,1.0139,0.354056
two-stroke-racing,loaded,hllc,2,5,1.01324,-0.558315
two-stroke-racing,loaded,hllc,2,6,1.01063,0.0691515
two-stroke-racing,loaded,hllc,2,7,1.02017,0.324911
two-stroke-racing,loaded,hllc,2,8,1.02457,-0.539062
two-stroke-racing,loaded,hllc,3,1,1.0412,0.375503
two-stroke-racing,loaded,hllc,3,2,1.02424,-0.575062
two-stroke-racing,loaded,hllc,3,3,1.01303,0.055486
two-stroke-racing,loaded,hllc,3,4,1.0163,0.354056
two-stroke-racing,loaded,hllc,3,5,1.01553,-0.558315
two-stroke-racing,loaded,hllc,3,6,1.01073,0.0691515
two-stroke-racing,loaded,hllc,3,7,1.01048,0.324911
two-stroke-racing,loaded,hllc,3,8,1.01098,-0.539062
two-stroke-racing,loaded,hlle,1,1,0.862428,0.43566
two-stroke-racing,loaded,hlle,1,2,0.851193,-0.562762
two-stroke-racing,loaded,hlle,1,3,0.828863,0.0365935
two-stroke-racing,loaded,hlle,1,4,0.812865,0.40405
two-stroke-racing,loaded,hlle,1,5,0.810191,-0.562104
two-stroke-racing,loaded,hlle,1,6,0.823245,0.0268677
two-stroke-racing,loaded,hlle,1,7,0.810627,0.369049
two-stroke-racing,loaded,hlle,1,8,0.816913,-0.552236
two-stroke-racing,loaded,hlle,2,1,0.836842,0.43566
two-stroke-racing,loaded,hlle,2,2,0.810566,-0.562762
two-stroke-racing,loaded,hlle,2,3,0.811131,0.0365935
two-stroke-racing,loaded,hlle,2,4,0.819234,0.40405
two-stroke-racing,loaded,hlle,2,5,0.816111,-0.562104
two-stroke-racing,loaded,hlle,2,6,0.815896,0.0268677
two-stroke-racing,loaded,hlle,2,7,0.818137,0.369049
two-stroke-racing,loaded,hlle,2,8,0.817477,-0.552236
two-stroke-racing,loaded,hlle,3,1,0.862412,0.43566
two-stroke-racing,loaded,hlle,3,2,0.84173,-0.562762
two-stroke-racing,loaded,hlle,3,3,0.84218,0.0365935
two-stroke-racing,loaded,hlle,3,4,0.817609,0.40405
two-stroke-racing,loaded,hlle,3,5,0.810513,-0.562104
two-stroke-racing,loaded,hlle,3,6,0.814916,0.0268677
two-stroke-racing,loaded,hlle,3,7,0.814355,0.369049
two-stroke-racing,loaded,hlle,3,8,0.819992,-0.552236
two-stroke-racing,loaded,rusanov,1,1,0.568351,0.432643
two-stroke-racing,loaded,rusanov,1,2,0.577169,-0.563588
two-stroke-racing,loaded,rusanov,1,3,0.58529,0.0358499
two-stroke-racing,loaded,rusanov,1,4,0.551193,0.402933
two-stroke-racing,loaded,rusanov,1,5,0.549485,-0.562433
two-stroke-racing,loaded,rusanov,1,6,0.546487,0.0223812
two-stroke-racing,loaded,rusanov,1,7,0.547863,0.369953
two-stroke-racing,loaded,rusanov,1,8,0.545577,-0.552817
two-stroke-racing,loaded,rusanov,2,1,0.568699,0.432643
two-stroke-racing,loaded,rusanov,2,2,0.551995,-0.563588
two-stroke-racing,loaded,rusanov,2,3,0.544908,0.0358499
two-stroke-racing,loaded,rusanov,2,4,0.544367,0.402933
two-stroke-racing,loaded,rusanov,2,5,0.546786,-0.562433
two-stroke-racing,loaded,rusanov,2,6,0.556999,0.0223812
two-stroke-racing,loaded,rusanov,2,7,0.551064,0.369953
two-stroke-racing,loaded,rusanov,2,8,0.54936,-0.552817
two-stroke-racing,loaded,rusanov,3,1,0.562092,0.432643
two-stroke-racing,loaded,rusanov,3,2,0.550119,-0.563588
two-stroke-racing,loaded,rusanov,3,3,0.548234,0.0358499
two-stroke-racing,loaded,rusanov,3,4,0.559092,0.402933
two-stroke-racing,loaded,rusanov,3,5,0.55883,-0.562433
two-stroke-racing,loaded,rusanov,3,6,0.546671,0.0223812
two-stroke-racing,loaded,rusanov,3,7,0.546362,0.369953
two-stroke-racing,loaded,rusanov,3,8,0.566723,-0.552817
v8,loaded,hllc,1,1,0.561418,-1.25788
v8,loaded,hllc,1,2,0.536758,0.0914793
v8,loaded,hllc,1,3,0.535254,-1.35427
v8,loaded,hllc,1,4,0.533429,0.11053
v8,loaded,hllc,1,5,0.533863,-1.46656
v8,loaded,hllc,1,6,0.534054,0.105944
v8,loaded,hllc,1,7,0.53679,-1.55901
v8,loaded,hllc,1,8,0.53558,0.116569
v8,loaded,hllc,2,1,0.556012,-1.25788
v8,loaded,hllc,2,2,0.535447,0.0914793
v8,loaded,hllc,2,3,0.537727,-1.35427
v8,loaded,hllc,2,4,0.535075,0.11053
v8,loaded,hllc,2,5,0.537039,-1.46656
v8,loaded,hllc,2,6,0.534696,0.105944
v8,loaded,hllc,2,7,0.536837,-1.55901
v8,loaded,hllc,2,8,0.538061,0.116569
v8,loaded,hllc,3,1,0.554375,-1.25788
v8,loaded,hllc,3,2,0.53761,0.0914793
v8,loaded,hllc,3,3,0.554563,-1.35427
v8,loaded,hllc,3,4,0.551726,0.11053
v8,loaded,hllc,3,5,0.541197,-1.46656
v8,loaded,hllc,3,6,0.554373,0.105944
v8,loaded,hllc,3,7,0.53943,-1.55901
v8,loaded,hllc,3,8,0.568289,0.116569
v8,loaded,hlle,1,1,0.477592,-0.968916
v8,loaded,hlle,1,2,0.470707,0.426525
v8,loaded,hlle,1,3,0.464077,-1.04861
v8,loaded,hlle,1,4,0.465674,0.43059
v8,loaded,hlle,1,5,0.465533,-1.1376
v8,loaded,hlle,1,6,0.464758,0.411077
v8,loaded,hlle,1,7,0.478506,-1.21676
v8,loaded,hlle,1,8,0.465583,0.408273
v8,loaded,hlle,2,1,0.475207,-0.968916
v8,loaded,hlle,2,2,0.477141,0.426525
v8,loaded,hlle,2,3,0.467366,-1.04861
v8,loaded,hlle,2,4,0.464485,0.43059
v8,loaded,hlle,2,5,0.466179,-1.1376
v8,loaded,hlle,2,6,0.464215,0.411077
v8,loaded,hlle,2,7,0.463797,-1.21676
v8,loaded,hlle,2,8,0.468572,0.408273
v8,loaded,hlle,3,1,0.487188,-0.968916
v8,loaded,hlle,3,2,0.491205,0.426525
v8,loaded,hlle,3,3,0.499025,-1.04861
v8,loaded,hlle,3,4,0.483856,0.43059
v8,loaded,hlle,3,5,0.474842,-1.1376
v8,loaded,hlle,3,6,0.494745,0.411077
v8,loaded,hlle,3,7,0.485687,-1.21676
v8,loaded,hlle,3,8,0.49635,0.408273
v8,loaded,rusanov,1,1,0.388499,-1.25887
v8,loaded,rusanov,1,2,0.373823,0.36172
v8,loaded,rusanov,1,3,0.37675,-1.34285
v8,loaded,rusanov,1,4,0.371837,0.355362
v8,loaded,rusanov,1,5,0.377517,-1.43271
v8,loaded,rusanov,1,6,0.370712,0.329141
v8,loaded,rusanov,1,7,0.371495,-1.51861
v8,loaded,rusanov,1,8,0.374671,0.318454
v8,loaded,rusanov,2,1,0.38336,-1.25887
v8,loaded,rusanov,2,2,0.378879,0.36172
v8,loaded,rusanov,2,3,0.371911,-1.34285
v8,loaded,rusanov,2,4,0.372973,0.355362
v8,loaded,rusanov,2,5,0.372276,-1.43271
v8,loaded,rusanov,2,6,0.371085,0.329141
v8,loaded,rusanov,2,7,0.376825,-1.51861
v8,loaded,rusanov,2,8,0.372938,0.318454
v8,loaded,rusanov,3,1,0.413532,-1.25887
v8,loaded,rusanov,3,2,0.386475,0.36172
v8,loaded,rusanov,3,3,0.370165,-1.34285
v8,loaded,rusanov,3,4,0.371908,0.355362
v8,loaded,rusanov,3,5,0.373091,-1.43271
v8,loaded,rusanov,3,6,0.377626,0.329141
v8,loaded,rusanov,3,7,0.377083,-1.51861
v8,loaded,rusanov,3,8,0.389025,0.318454
two-stroke-racing,transient,hllc,1,1,1.00887,0.0498488
two-stroke-racing,transient,hllc,1,2,1.01234,0.786509
two-stroke-racing,transient,hllc,1,3,1.04171,0.345888
two-stroke-racing,transient,hllc,1,4,1.02843,-0.298719
two-stroke-racing,transient,hllc,1,5,1.01825,-0.0716625
two-stroke-racing,transient,hllc,1,6,1.0277,0.276365
two-stroke-racing,transient,hllc,1,7,1.02908,0.0893495
two-stroke-racing,transient,hllc,1,8,1.01382,-0.274945
two-stroke-racing,transient,hllc,1,9,1.02545,0.0940415
two-stroke-racing,transient,hllc,1,10,1.05693,0.143232
two-stroke-racing,transient,hllc,1,11,1.04802,-0.152691
two-stroke-racing,transient,hllc,1,12,0.997328,-0.0558285
two-stroke-racing,transient,hlle,1,1,0.820049,-0.0693344
two-stroke-racing,transient,hlle,1,2,0.812633,-0.0564268
two-stroke-racing,transient,hlle,1,3,0.828641,0.235254
two-stroke-racing,transient,hlle,1,4,0.825289,-0.145124
two-stroke-racing,transient,hlle,1,5,0.816333,0.133833
two-stroke-racing,transient,hlle,1,6,0.82408,-0.104748
two-stroke-racing,transient,hlle,1,7,0.815949,0.0463649
two-stroke-racing,transient,hlle,1,8,0.82025,0.274437
two-stroke-racing,transient,hlle,1,9,0.814858,0.221262
two-stroke-racing,transient,hlle,1,10,0.815676,-0.219595
two-stroke-racing,transient,hlle,1,11,0.816901,-0.0168035
two-stroke-racing,transient,hlle,1,12,0.79006,0.00294675
two-stroke-racing,transient,rusanov,1,1,0.562915,-0.00949596
two-stroke-racing,transient,rusanov,1,2,0.546997,0.343048
two-stroke-racing,transient,rusanov,1,3,0.557931,0.0393343
two-stroke-racing,transient,rusanov,1,4,0.550525,0.264534
two-stroke-racing,transient,rusanov,1,5,0.552067,-0.358644
two-stroke-racing,transient,rusanov,1,6,0.554453,-0.097238
two-stroke-racing,transient,rusanov,1,7,0.554178,-0.327873
two-stroke-racing,transient,rusanov,1,8,0.549745,-0.0399539
two-stroke-racing,transient,rusanov,1,9,0.554266,0.267054
two-stroke-racing,transient,rusanov,1,10,0.554549,-0.339717
two-stroke-racing,transient,rusanov,1,11,0.547034,-0.124783
two-stroke-racing,transient,rusanov,1,12,0.520774,-0.00297952
v8,transient,hllc,1,1,0.547342,0.505241
v8,transient,hllc,1,2,0.530697,1.96217
v8,transient,hllc,1,3,0.539237,-3.91503
v8,transient,hllc,1,4,0.537446,0.0279775
v8,transient,hllc,1,5,0.539026,-0.871005
v8,transient,hllc,1,6,0.540629,0.517934
v8,transient,hllc,1,7,0.541511,0.534398
v8,transient,hllc,1,8,0.541115,-0.274595
v8,transient,hllc,1,9,0.538592,0.98194
v8,transient,hllc,1,10,0.536807,4.28109
v8,transient,hllc,1,11,0.542719,-0.254435
v8,transient,hllc,1,12,0.539089,-0.102749
v8,transient,hlle,1,1,0.477958,-0.615984
v8,transient,hlle,1,2,0.474017,1.60934
v8,transient,hlle,1,3,0.483335,0.216284
v8,transient,hlle,1,4,0.47055,0.0425016
v8,transient,hlle,1,5,0.468981,-0.199247
v8,transient,hlle,1,6,0.47164,-0.0859847
v8,transient,hlle,1,7,0.466657,4.30145
v8,transient,hlle,1,8,0.46676,0.301313
v8,transient,hlle,1,9,0.469689,-0.448985
v8,transient,hlle,1,10,0.467277,-0.660645
v8,transient,hlle,1,11,0.468715,-1.20044
v8,transient,hlle,1,12,0.466929,-0.110843
v8,transient,rusanov,1,1,0.399152,-0.246586
v8,transient,rusanov,1,2,0.396538,0.988927
v8,transient,rusanov,1,3,0.3876,1.30821
v8,transient,rusanov,1,4,0.381343,-0.258464
v8,transient,rusanov,1,5,0.37996,-0.675008
v8,transient,rusanov,1,6,0.376709,0.268399
v8,transient,rusanov,1,7,0.390778,-1.3748
v8,transient,rusanov,1,8,0.374897,-0.0903278
v8,transient,rusanov,1,9,0.373756,0.23786
v8,transient,rusanov,1,10,0.373661,-0.0822659
v8,transient,rusanov,1,11,0.377201,1.83047
v8,transient,rusanov,1,12,0.382746,1.43902
1 scenario mode solver repeat second wall_seconds checksum
2 two-stroke-racing loaded hllc 1 1 1.0345 0.375503
3 two-stroke-racing loaded hllc 1 2 1.01643 -0.575062
4 two-stroke-racing loaded hllc 1 3 1.00805 0.055486
5 two-stroke-racing loaded hllc 1 4 1.02154 0.354056
6 two-stroke-racing loaded hllc 1 5 1.00972 -0.558315
7 two-stroke-racing loaded hllc 1 6 1.01332 0.0691515
8 two-stroke-racing loaded hllc 1 7 1.02021 0.324911
9 two-stroke-racing loaded hllc 1 8 1.00968 -0.539062
10 two-stroke-racing loaded hllc 2 1 1.03723 0.375503
11 two-stroke-racing loaded hllc 2 2 1.0184 -0.575062
12 two-stroke-racing loaded hllc 2 3 1.0148 0.055486
13 two-stroke-racing loaded hllc 2 4 1.0139 0.354056
14 two-stroke-racing loaded hllc 2 5 1.01324 -0.558315
15 two-stroke-racing loaded hllc 2 6 1.01063 0.0691515
16 two-stroke-racing loaded hllc 2 7 1.02017 0.324911
17 two-stroke-racing loaded hllc 2 8 1.02457 -0.539062
18 two-stroke-racing loaded hllc 3 1 1.0412 0.375503
19 two-stroke-racing loaded hllc 3 2 1.02424 -0.575062
20 two-stroke-racing loaded hllc 3 3 1.01303 0.055486
21 two-stroke-racing loaded hllc 3 4 1.0163 0.354056
22 two-stroke-racing loaded hllc 3 5 1.01553 -0.558315
23 two-stroke-racing loaded hllc 3 6 1.01073 0.0691515
24 two-stroke-racing loaded hllc 3 7 1.01048 0.324911
25 two-stroke-racing loaded hllc 3 8 1.01098 -0.539062
26 two-stroke-racing loaded hlle 1 1 0.862428 0.43566
27 two-stroke-racing loaded hlle 1 2 0.851193 -0.562762
28 two-stroke-racing loaded hlle 1 3 0.828863 0.0365935
29 two-stroke-racing loaded hlle 1 4 0.812865 0.40405
30 two-stroke-racing loaded hlle 1 5 0.810191 -0.562104
31 two-stroke-racing loaded hlle 1 6 0.823245 0.0268677
32 two-stroke-racing loaded hlle 1 7 0.810627 0.369049
33 two-stroke-racing loaded hlle 1 8 0.816913 -0.552236
34 two-stroke-racing loaded hlle 2 1 0.836842 0.43566
35 two-stroke-racing loaded hlle 2 2 0.810566 -0.562762
36 two-stroke-racing loaded hlle 2 3 0.811131 0.0365935
37 two-stroke-racing loaded hlle 2 4 0.819234 0.40405
38 two-stroke-racing loaded hlle 2 5 0.816111 -0.562104
39 two-stroke-racing loaded hlle 2 6 0.815896 0.0268677
40 two-stroke-racing loaded hlle 2 7 0.818137 0.369049
41 two-stroke-racing loaded hlle 2 8 0.817477 -0.552236
42 two-stroke-racing loaded hlle 3 1 0.862412 0.43566
43 two-stroke-racing loaded hlle 3 2 0.84173 -0.562762
44 two-stroke-racing loaded hlle 3 3 0.84218 0.0365935
45 two-stroke-racing loaded hlle 3 4 0.817609 0.40405
46 two-stroke-racing loaded hlle 3 5 0.810513 -0.562104
47 two-stroke-racing loaded hlle 3 6 0.814916 0.0268677
48 two-stroke-racing loaded hlle 3 7 0.814355 0.369049
49 two-stroke-racing loaded hlle 3 8 0.819992 -0.552236
50 two-stroke-racing loaded rusanov 1 1 0.568351 0.432643
51 two-stroke-racing loaded rusanov 1 2 0.577169 -0.563588
52 two-stroke-racing loaded rusanov 1 3 0.58529 0.0358499
53 two-stroke-racing loaded rusanov 1 4 0.551193 0.402933
54 two-stroke-racing loaded rusanov 1 5 0.549485 -0.562433
55 two-stroke-racing loaded rusanov 1 6 0.546487 0.0223812
56 two-stroke-racing loaded rusanov 1 7 0.547863 0.369953
57 two-stroke-racing loaded rusanov 1 8 0.545577 -0.552817
58 two-stroke-racing loaded rusanov 2 1 0.568699 0.432643
59 two-stroke-racing loaded rusanov 2 2 0.551995 -0.563588
60 two-stroke-racing loaded rusanov 2 3 0.544908 0.0358499
61 two-stroke-racing loaded rusanov 2 4 0.544367 0.402933
62 two-stroke-racing loaded rusanov 2 5 0.546786 -0.562433
63 two-stroke-racing loaded rusanov 2 6 0.556999 0.0223812
64 two-stroke-racing loaded rusanov 2 7 0.551064 0.369953
65 two-stroke-racing loaded rusanov 2 8 0.54936 -0.552817
66 two-stroke-racing loaded rusanov 3 1 0.562092 0.432643
67 two-stroke-racing loaded rusanov 3 2 0.550119 -0.563588
68 two-stroke-racing loaded rusanov 3 3 0.548234 0.0358499
69 two-stroke-racing loaded rusanov 3 4 0.559092 0.402933
70 two-stroke-racing loaded rusanov 3 5 0.55883 -0.562433
71 two-stroke-racing loaded rusanov 3 6 0.546671 0.0223812
72 two-stroke-racing loaded rusanov 3 7 0.546362 0.369953
73 two-stroke-racing loaded rusanov 3 8 0.566723 -0.552817
74 v8 loaded hllc 1 1 0.561418 -1.25788
75 v8 loaded hllc 1 2 0.536758 0.0914793
76 v8 loaded hllc 1 3 0.535254 -1.35427
77 v8 loaded hllc 1 4 0.533429 0.11053
78 v8 loaded hllc 1 5 0.533863 -1.46656
79 v8 loaded hllc 1 6 0.534054 0.105944
80 v8 loaded hllc 1 7 0.53679 -1.55901
81 v8 loaded hllc 1 8 0.53558 0.116569
82 v8 loaded hllc 2 1 0.556012 -1.25788
83 v8 loaded hllc 2 2 0.535447 0.0914793
84 v8 loaded hllc 2 3 0.537727 -1.35427
85 v8 loaded hllc 2 4 0.535075 0.11053
86 v8 loaded hllc 2 5 0.537039 -1.46656
87 v8 loaded hllc 2 6 0.534696 0.105944
88 v8 loaded hllc 2 7 0.536837 -1.55901
89 v8 loaded hllc 2 8 0.538061 0.116569
90 v8 loaded hllc 3 1 0.554375 -1.25788
91 v8 loaded hllc 3 2 0.53761 0.0914793
92 v8 loaded hllc 3 3 0.554563 -1.35427
93 v8 loaded hllc 3 4 0.551726 0.11053
94 v8 loaded hllc 3 5 0.541197 -1.46656
95 v8 loaded hllc 3 6 0.554373 0.105944
96 v8 loaded hllc 3 7 0.53943 -1.55901
97 v8 loaded hllc 3 8 0.568289 0.116569
98 v8 loaded hlle 1 1 0.477592 -0.968916
99 v8 loaded hlle 1 2 0.470707 0.426525
100 v8 loaded hlle 1 3 0.464077 -1.04861
101 v8 loaded hlle 1 4 0.465674 0.43059
102 v8 loaded hlle 1 5 0.465533 -1.1376
103 v8 loaded hlle 1 6 0.464758 0.411077
104 v8 loaded hlle 1 7 0.478506 -1.21676
105 v8 loaded hlle 1 8 0.465583 0.408273
106 v8 loaded hlle 2 1 0.475207 -0.968916
107 v8 loaded hlle 2 2 0.477141 0.426525
108 v8 loaded hlle 2 3 0.467366 -1.04861
109 v8 loaded hlle 2 4 0.464485 0.43059
110 v8 loaded hlle 2 5 0.466179 -1.1376
111 v8 loaded hlle 2 6 0.464215 0.411077
112 v8 loaded hlle 2 7 0.463797 -1.21676
113 v8 loaded hlle 2 8 0.468572 0.408273
114 v8 loaded hlle 3 1 0.487188 -0.968916
115 v8 loaded hlle 3 2 0.491205 0.426525
116 v8 loaded hlle 3 3 0.499025 -1.04861
117 v8 loaded hlle 3 4 0.483856 0.43059
118 v8 loaded hlle 3 5 0.474842 -1.1376
119 v8 loaded hlle 3 6 0.494745 0.411077
120 v8 loaded hlle 3 7 0.485687 -1.21676
121 v8 loaded hlle 3 8 0.49635 0.408273
122 v8 loaded rusanov 1 1 0.388499 -1.25887
123 v8 loaded rusanov 1 2 0.373823 0.36172
124 v8 loaded rusanov 1 3 0.37675 -1.34285
125 v8 loaded rusanov 1 4 0.371837 0.355362
126 v8 loaded rusanov 1 5 0.377517 -1.43271
127 v8 loaded rusanov 1 6 0.370712 0.329141
128 v8 loaded rusanov 1 7 0.371495 -1.51861
129 v8 loaded rusanov 1 8 0.374671 0.318454
130 v8 loaded rusanov 2 1 0.38336 -1.25887
131 v8 loaded rusanov 2 2 0.378879 0.36172
132 v8 loaded rusanov 2 3 0.371911 -1.34285
133 v8 loaded rusanov 2 4 0.372973 0.355362
134 v8 loaded rusanov 2 5 0.372276 -1.43271
135 v8 loaded rusanov 2 6 0.371085 0.329141
136 v8 loaded rusanov 2 7 0.376825 -1.51861
137 v8 loaded rusanov 2 8 0.372938 0.318454
138 v8 loaded rusanov 3 1 0.413532 -1.25887
139 v8 loaded rusanov 3 2 0.386475 0.36172
140 v8 loaded rusanov 3 3 0.370165 -1.34285
141 v8 loaded rusanov 3 4 0.371908 0.355362
142 v8 loaded rusanov 3 5 0.373091 -1.43271
143 v8 loaded rusanov 3 6 0.377626 0.329141
144 v8 loaded rusanov 3 7 0.377083 -1.51861
145 v8 loaded rusanov 3 8 0.389025 0.318454
146 two-stroke-racing transient hllc 1 1 1.00887 0.0498488
147 two-stroke-racing transient hllc 1 2 1.01234 0.786509
148 two-stroke-racing transient hllc 1 3 1.04171 0.345888
149 two-stroke-racing transient hllc 1 4 1.02843 -0.298719
150 two-stroke-racing transient hllc 1 5 1.01825 -0.0716625
151 two-stroke-racing transient hllc 1 6 1.0277 0.276365
152 two-stroke-racing transient hllc 1 7 1.02908 0.0893495
153 two-stroke-racing transient hllc 1 8 1.01382 -0.274945
154 two-stroke-racing transient hllc 1 9 1.02545 0.0940415
155 two-stroke-racing transient hllc 1 10 1.05693 0.143232
156 two-stroke-racing transient hllc 1 11 1.04802 -0.152691
157 two-stroke-racing transient hllc 1 12 0.997328 -0.0558285
158 two-stroke-racing transient hlle 1 1 0.820049 -0.0693344
159 two-stroke-racing transient hlle 1 2 0.812633 -0.0564268
160 two-stroke-racing transient hlle 1 3 0.828641 0.235254
161 two-stroke-racing transient hlle 1 4 0.825289 -0.145124
162 two-stroke-racing transient hlle 1 5 0.816333 0.133833
163 two-stroke-racing transient hlle 1 6 0.82408 -0.104748
164 two-stroke-racing transient hlle 1 7 0.815949 0.0463649
165 two-stroke-racing transient hlle 1 8 0.82025 0.274437
166 two-stroke-racing transient hlle 1 9 0.814858 0.221262
167 two-stroke-racing transient hlle 1 10 0.815676 -0.219595
168 two-stroke-racing transient hlle 1 11 0.816901 -0.0168035
169 two-stroke-racing transient hlle 1 12 0.79006 0.00294675
170 two-stroke-racing transient rusanov 1 1 0.562915 -0.00949596
171 two-stroke-racing transient rusanov 1 2 0.546997 0.343048
172 two-stroke-racing transient rusanov 1 3 0.557931 0.0393343
173 two-stroke-racing transient rusanov 1 4 0.550525 0.264534
174 two-stroke-racing transient rusanov 1 5 0.552067 -0.358644
175 two-stroke-racing transient rusanov 1 6 0.554453 -0.097238
176 two-stroke-racing transient rusanov 1 7 0.554178 -0.327873
177 two-stroke-racing transient rusanov 1 8 0.549745 -0.0399539
178 two-stroke-racing transient rusanov 1 9 0.554266 0.267054
179 two-stroke-racing transient rusanov 1 10 0.554549 -0.339717
180 two-stroke-racing transient rusanov 1 11 0.547034 -0.124783
181 two-stroke-racing transient rusanov 1 12 0.520774 -0.00297952
182 v8 transient hllc 1 1 0.547342 0.505241
183 v8 transient hllc 1 2 0.530697 1.96217
184 v8 transient hllc 1 3 0.539237 -3.91503
185 v8 transient hllc 1 4 0.537446 0.0279775
186 v8 transient hllc 1 5 0.539026 -0.871005
187 v8 transient hllc 1 6 0.540629 0.517934
188 v8 transient hllc 1 7 0.541511 0.534398
189 v8 transient hllc 1 8 0.541115 -0.274595
190 v8 transient hllc 1 9 0.538592 0.98194
191 v8 transient hllc 1 10 0.536807 4.28109
192 v8 transient hllc 1 11 0.542719 -0.254435
193 v8 transient hllc 1 12 0.539089 -0.102749
194 v8 transient hlle 1 1 0.477958 -0.615984
195 v8 transient hlle 1 2 0.474017 1.60934
196 v8 transient hlle 1 3 0.483335 0.216284
197 v8 transient hlle 1 4 0.47055 0.0425016
198 v8 transient hlle 1 5 0.468981 -0.199247
199 v8 transient hlle 1 6 0.47164 -0.0859847
200 v8 transient hlle 1 7 0.466657 4.30145
201 v8 transient hlle 1 8 0.46676 0.301313
202 v8 transient hlle 1 9 0.469689 -0.448985
203 v8 transient hlle 1 10 0.467277 -0.660645
204 v8 transient hlle 1 11 0.468715 -1.20044
205 v8 transient hlle 1 12 0.466929 -0.110843
206 v8 transient rusanov 1 1 0.399152 -0.246586
207 v8 transient rusanov 1 2 0.396538 0.988927
208 v8 transient rusanov 1 3 0.3876 1.30821
209 v8 transient rusanov 1 4 0.381343 -0.258464
210 v8 transient rusanov 1 5 0.37996 -0.675008
211 v8 transient rusanov 1 6 0.376709 0.268399
212 v8 transient rusanov 1 7 0.390778 -1.3748
213 v8 transient rusanov 1 8 0.374897 -0.0903278
214 v8 transient rusanov 1 9 0.373756 0.23786
215 v8 transient rusanov 1 10 0.373661 -0.0822659
216 v8 transient rusanov 1 11 0.377201 1.83047
217 v8 transient rusanov 1 12 0.382746 1.43902

View File

@@ -0,0 +1,218 @@
[
{
"scenario": "two-stroke-racing",
"mode": "loaded",
"solver": "rusanov",
"repeat": 1,
"mean": 0.5542564999999999,
"steady_mean": 0.5530338571428571,
"worst": 0.568245
},
{
"scenario": "two-stroke-racing",
"mode": "loaded",
"solver": "hlle",
"repeat": 1,
"mean": 0.819146,
"steady_mean": 0.811276,
"worst": 0.874236
},
{
"scenario": "two-stroke-racing",
"mode": "loaded",
"solver": "hllc",
"repeat": 1,
"mean": 0.8948041250000001,
"steady_mean": 0.8942180000000001,
"worst": 0.915618
},
{
"scenario": "two-stroke-racing",
"mode": "loaded",
"solver": "hlle",
"repeat": 2,
"mean": 0.799205375,
"steady_mean": 0.7940375714285715,
"worst": 0.83538
},
{
"scenario": "two-stroke-racing",
"mode": "loaded",
"solver": "hllc",
"repeat": 2,
"mean": 0.888172,
"steady_mean": 0.8854931428571429,
"worst": 0.906924
},
{
"scenario": "two-stroke-racing",
"mode": "loaded",
"solver": "rusanov",
"repeat": 2,
"mean": 0.548723625,
"steady_mean": 0.5465001428571429,
"worst": 0.564288
},
{
"scenario": "two-stroke-racing",
"mode": "loaded",
"solver": "hllc",
"repeat": 3,
"mean": 0.894861,
"steady_mean": 0.8932902857142857,
"worst": 0.909393
},
{
"scenario": "two-stroke-racing",
"mode": "loaded",
"solver": "rusanov",
"repeat": 3,
"mean": 0.54947075,
"steady_mean": 0.5482321428571428,
"worst": 0.558153
},
{
"scenario": "two-stroke-racing",
"mode": "loaded",
"solver": "hlle",
"repeat": 3,
"mean": 0.79361025,
"steady_mean": 0.7899642857142857,
"worst": 0.819132
},
{
"scenario": "two-stroke-racing",
"mode": "transient",
"solver": "rusanov",
"repeat": 1,
"mean": 0.5485989166666667,
"steady_mean": 0.5472415454545455,
"worst": 0.56353
},
{
"scenario": "two-stroke-racing",
"mode": "transient",
"solver": "hlle",
"repeat": 1,
"mean": 0.79490825,
"steady_mean": 0.794693,
"worst": 0.817458
},
{
"scenario": "two-stroke-racing",
"mode": "transient",
"solver": "hllc",
"repeat": 1,
"mean": 0.8981656666666666,
"steady_mean": 0.8979373636363637,
"worst": 0.919263
},
{
"scenario": "v8",
"mode": "loaded",
"solver": "rusanov",
"repeat": 1,
"mean": 0.376080625,
"steady_mean": 0.3729907142857143,
"worst": 0.39771
},
{
"scenario": "v8",
"mode": "loaded",
"solver": "hlle",
"repeat": 1,
"mean": 0.461346875,
"steady_mean": 0.4588132857142857,
"worst": 0.479082
},
{
"scenario": "v8",
"mode": "loaded",
"solver": "hllc",
"repeat": 1,
"mean": 0.4996255,
"steady_mean": 0.4971262857142857,
"worst": 0.51712
},
{
"scenario": "v8",
"mode": "loaded",
"solver": "hlle",
"repeat": 2,
"mean": 0.461802,
"steady_mean": 0.45975828571428573,
"worst": 0.476108
},
{
"scenario": "v8",
"mode": "loaded",
"solver": "hllc",
"repeat": 2,
"mean": 0.52282575,
"steady_mean": 0.5200991428571429,
"worst": 0.541912
},
{
"scenario": "v8",
"mode": "loaded",
"solver": "rusanov",
"repeat": 2,
"mean": 0.379948,
"steady_mean": 0.3780255714285714,
"worst": 0.393405
},
{
"scenario": "v8",
"mode": "loaded",
"solver": "hllc",
"repeat": 3,
"mean": 0.496725875,
"steady_mean": 0.49525199999999997,
"worst": 0.507043
},
{
"scenario": "v8",
"mode": "loaded",
"solver": "rusanov",
"repeat": 3,
"mean": 0.383915625,
"steady_mean": 0.3843452857142857,
"worst": 0.399614
},
{
"scenario": "v8",
"mode": "loaded",
"solver": "hlle",
"repeat": 3,
"mean": 0.46875,
"steady_mean": 0.4661215714285714,
"worst": 0.487149
},
{
"scenario": "v8",
"mode": "transient",
"solver": "rusanov",
"repeat": 1,
"mean": 0.382868,
"steady_mean": 0.3824920909090909,
"worst": 0.390952
},
{
"scenario": "v8",
"mode": "transient",
"solver": "hlle",
"repeat": 1,
"mean": 0.47020333333333336,
"steady_mean": 0.47030472727272726,
"worst": 0.484561
},
{
"scenario": "v8",
"mode": "transient",
"solver": "hllc",
"repeat": 1,
"mean": 0.50014775,
"steady_mean": 0.4994738181818182,
"worst": 0.507561
}
]

View File

@@ -0,0 +1,138 @@
SOLVER rusanov
PASS solver flux consistency and reflection symmetry
PASS HLLC preserves stationary contact and falls back at expansion vacuum
PASS unflanged radiation reflection magnitude and phase
PASS radiating mouth conserves reported mass at both orientations
PASS radiating mouth reflects rarefaction at either pipe orientation
PASS radiating inflow supplies fresh ambient stagnation enthalpy
PASS tracer underflow preserves bulk gas and normal concentrations
PASS invalid uniform geometry
PASS closed pipe conservation
PASS profiled pipe conservation
PASS volume pipe sealed conservation
PASS equal pressure hot contact
PASS NASA choked nozzle mass flow
PASS inertance units
PASS moving volume adiabatic law
PASS profile at rest is well balanced
PASS pipe connection all four orientations
PASS split pipe matches unsplit pipe
PASS three volumes shared donor conservation
PASS open boundary reports actual conserved flux both ends
PASS duplicate and invalid boundary rejection
PASS supersonic outlet extrapolates
PASS underdefined supersonic inlet rejected
PASS closed and open acoustic reflection signs
Sod n=100 density L1=0.0233209
Sod n=200 density L1=0.0159539
Sod n=400 density L1=0.0105216
PASS Sod shock tube convergence
PASS strong pulse and near-vacuum rarefaction
Helmholtz frequency=135.6 analytic lumped=138.508
PASS small-signal Helmholtz resonance
PASS invalid volume and timestep rejection
PASS fresh-air species conservation across connection
PASS unsupported unequal junction rejected
PASS unconnected endpoint rejected by solver
closed mode n=50 f=171.897 analytic=171.91
closed mode n=100 f=171.914 analytic=171.91
closed mode n=200 f=171.911 analytic=171.91
PASS closed duct acoustic eigenfrequency
PASS inertance impulse and dissipative resistance
PASS incompatible gas coupling rejected
PASS thermal relaxation matches analytic energy decay
PASS sealed crankcase compression conserves mass above five bar
36/36 passed
SOLVER hlle
PASS solver flux consistency and reflection symmetry
PASS HLLC preserves stationary contact and falls back at expansion vacuum
PASS unflanged radiation reflection magnitude and phase
PASS radiating mouth conserves reported mass at both orientations
PASS radiating mouth reflects rarefaction at either pipe orientation
PASS radiating inflow supplies fresh ambient stagnation enthalpy
PASS tracer underflow preserves bulk gas and normal concentrations
PASS invalid uniform geometry
PASS closed pipe conservation
PASS profiled pipe conservation
PASS volume pipe sealed conservation
PASS equal pressure hot contact
PASS NASA choked nozzle mass flow
PASS inertance units
PASS moving volume adiabatic law
PASS profile at rest is well balanced
PASS pipe connection all four orientations
PASS split pipe matches unsplit pipe
PASS three volumes shared donor conservation
PASS open boundary reports actual conserved flux both ends
PASS duplicate and invalid boundary rejection
PASS supersonic outlet extrapolates
PASS underdefined supersonic inlet rejected
PASS closed and open acoustic reflection signs
Sod n=100 density L1=0.0174385
Sod n=200 density L1=0.0110652
Sod n=400 density L1=0.00701807
PASS Sod shock tube convergence
PASS strong pulse and near-vacuum rarefaction
Helmholtz frequency=135.6 analytic lumped=138.508
PASS small-signal Helmholtz resonance
PASS invalid volume and timestep rejection
PASS fresh-air species conservation across connection
PASS unsupported unequal junction rejected
PASS unconnected endpoint rejected by solver
closed mode n=50 f=171.897 analytic=171.91
closed mode n=100 f=171.914 analytic=171.91
closed mode n=200 f=171.911 analytic=171.91
PASS closed duct acoustic eigenfrequency
PASS inertance impulse and dissipative resistance
PASS incompatible gas coupling rejected
PASS thermal relaxation matches analytic energy decay
PASS sealed crankcase compression conserves mass above five bar
36/36 passed
SOLVER hllc
PASS solver flux consistency and reflection symmetry
PASS HLLC preserves stationary contact and falls back at expansion vacuum
PASS unflanged radiation reflection magnitude and phase
PASS radiating mouth conserves reported mass at both orientations
PASS radiating mouth reflects rarefaction at either pipe orientation
PASS radiating inflow supplies fresh ambient stagnation enthalpy
PASS tracer underflow preserves bulk gas and normal concentrations
PASS invalid uniform geometry
PASS closed pipe conservation
PASS profiled pipe conservation
PASS volume pipe sealed conservation
PASS equal pressure hot contact
PASS NASA choked nozzle mass flow
PASS inertance units
PASS moving volume adiabatic law
PASS profile at rest is well balanced
PASS pipe connection all four orientations
PASS split pipe matches unsplit pipe
PASS three volumes shared donor conservation
PASS open boundary reports actual conserved flux both ends
PASS duplicate and invalid boundary rejection
PASS supersonic outlet extrapolates
PASS underdefined supersonic inlet rejected
PASS closed and open acoustic reflection signs
Sod n=100 density L1=0.0162129
Sod n=200 density L1=0.0103006
Sod n=400 density L1=0.00654211
PASS Sod shock tube convergence
PASS strong pulse and near-vacuum rarefaction
Helmholtz frequency=135.6 analytic lumped=138.508
PASS small-signal Helmholtz resonance
PASS invalid volume and timestep rejection
PASS fresh-air species conservation across connection
PASS unsupported unequal junction rejected
PASS unconnected endpoint rejected by solver
closed mode n=50 f=171.897 analytic=171.91
closed mode n=100 f=171.914 analytic=171.91
closed mode n=200 f=171.911 analytic=171.91
PASS closed duct acoustic eigenfrequency
PASS inertance impulse and dissipative resistance
PASS incompatible gas coupling rejected
PASS thermal relaxation matches analytic energy decay
PASS sealed crankcase compression conserves mass above five bar
36/36 passed