all of the changes
This commit is contained in:
@@ -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`
|
||||
@@ -7,7 +7,6 @@ set(FLUIDSIM_FONT_FILE "" CACHE FILEPATH "Optional font file copied beside the d
|
||||
add_library(fluidsim_core
|
||||
src/carburetor.cpp
|
||||
src/ecu.cpp
|
||||
src/turbocharger.cpp
|
||||
src/engine.cpp
|
||||
src/crankcase.cpp
|
||||
src/two_stroke_cylinder.cpp
|
||||
@@ -17,7 +16,6 @@ add_library(fluidsim_core
|
||||
src/audio_processing.cpp
|
||||
src/outdoor_audio.cpp
|
||||
src/outdoor_exhaust_reverb.cpp
|
||||
src/mechanical_sound_processor.cpp
|
||||
src/scenario.cpp
|
||||
src/solver.cpp
|
||||
src/vehicle.cpp
|
||||
@@ -64,8 +62,17 @@ if(FLUIDSIM_BUILD_DESKTOP)
|
||||
)
|
||||
FetchContent_MakeAvailable(SFML)
|
||||
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)
|
||||
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)
|
||||
target_compile_options(fluidsim_desktop PRIVATE /W4 /permissive-)
|
||||
else()
|
||||
@@ -102,9 +109,14 @@ endif()
|
||||
|
||||
include(CTest)
|
||||
if(BUILD_TESTING)
|
||||
add_executable(fluidsim_saab_turbo_regression tests/saab_turbo_regression.cpp)
|
||||
target_link_libraries(fluidsim_saab_turbo_regression PRIVATE fluidsim_core)
|
||||
add_test(NAME fluidsim_saab_turbo_regression COMMAND fluidsim_saab_turbo_regression)
|
||||
add_executable(fluidsim_scenario_examples tests/scenario_examples.cpp)
|
||||
target_link_libraries(fluidsim_scenario_examples PRIVATE fluidsim_core)
|
||||
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()
|
||||
if(BUILD_TESTING)
|
||||
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)
|
||||
add_executable(fluidsim_scenario_benchmark tools/scenario_benchmark.cpp)
|
||||
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)
|
||||
add_executable(fluidsim_fluid_validation tests/fluid_validation.cpp)
|
||||
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_hlle COMMAND fluidsim_fluid_validation hlle)
|
||||
add_test(NAME fluidsim_fluid_validation_hllc COMMAND fluidsim_fluid_validation hllc)
|
||||
endif()
|
||||
find_package(Threads REQUIRED)
|
||||
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)
|
||||
add_executable(fluidsim_two_stroke_benchmark tools/two_stroke_benchmark.cpp)
|
||||
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)
|
||||
add_executable(fluidsim_outdoor_audio_tests tests/outdoor_audio_tests.cpp)
|
||||
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)
|
||||
target_link_libraries(fluidsim_b205_regression PRIVATE fluidsim_core)
|
||||
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()
|
||||
|
||||
include(cmake/MinGWRuntime.cmake)
|
||||
|
||||
260
README.md
260
README.md
@@ -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
|
||||
desktop's graphics/audio dependencies.
|
||||
|
||||
Controls: hold `W` for throttle, `C` for clutch, `A` / `D` to shift, `Z` for
|
||||
neutral, up/down to adjust throttle target, left/right to adjust engine load,
|
||||
`M` toggles mechanical engine noise, mouse wheel changes simulation playback
|
||||
speed, and Space toggles real time.
|
||||
Controls: W/S raise/lower the pedal over 0.5 seconds, C disengages the clutch,
|
||||
V applies starter torque while held,
|
||||
A/D shift, Z selects neutral, and left/right adjust engine load. The on-screen
|
||||
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:
|
||||
|
||||
@@ -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.
|
||||
The callback never advances physical time to conceal missing samples.
|
||||
|
||||
The source is a linear compact-monopole approximation,
|
||||
`p(r,t) = (d mass_flow/dt)/(4*pi*r)`. Intake and exhaust have separate static
|
||||
The primary source is a linear compact-monopole approximation,
|
||||
`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,
|
||||
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
|
||||
@@ -128,19 +132,27 @@ A roughly 3.5 Hz device DC blocker and final PCM clipping protection remain.
|
||||
Use 1x playback for acoustic evaluation.
|
||||
|
||||
The default environment is a nearby wall. Press **R** to cycle dry, field,
|
||||
wall and woodland. Ground/wall image paths provide early reflections; the
|
||||
woodland scatterers and decaying feedback-delay tail are approximate effects,
|
||||
not validated outdoor acoustics. Synthetic mechanical noise remains optional.
|
||||
The compact source does not implement pipe-end radiation impedance, validated
|
||||
directivity, or exhaust afterfire chemistry. Filtering cannot recover aliased
|
||||
wall, woodland, and tunnel. Ground/wall image paths provide early reflections;
|
||||
the live eight-line feedback-delay network supplies a scene-dependent late
|
||||
tail. Woodland scatterers and the hard-surface tunnel images are approximate.
|
||||
These are listening profiles, not validated acoustics. Mechanical sounds are
|
||||
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)
|
||||
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.
|
||||
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
|
||||
`--scenario twin`, `--scenario two-stroke` or `--scenario b205` bypasses the menu; `--scene` and
|
||||
`--exhaust` options also apply when choosing through the menu.
|
||||
`--scenario twin`, `--scenario two-stroke`, `--scenario b205`, or `--scenario v8`
|
||||
bypasses the menu; `--scene` and `--exhaust` options also apply
|
||||
when choosing through the menu.
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
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
|
||||
the existing 125 cc configuration; its dimensions and calibrations must be
|
||||
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
|
||||
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:
|
||||
|
||||
```sh
|
||||
@@ -222,36 +242,9 @@ Benchmark or capture with:
|
||||
./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
|
||||
|
||||
The Saab menu entry now includes an estimated GT17 turbo and ECU wastegate boost
|
||||
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.
|
||||
The Saab menu entry models the naturally aspirated B205 baseline.
|
||||
|
||||
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:
|
||||
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.
|
||||
Controls and dyno match the other scenarios. The original atmospheric-breathing
|
||||
geometry baseline remains available with `--turbo none`. Pipe/cam dimensions,
|
||||
Controls and dyno match the other scenarios. Pipe/cam dimensions,
|
||||
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.
|
||||
|
||||
## 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
|
||||
stepping, the impulse-clutch motorcycle/road-load model, and the active 600 cc
|
||||
twin topology. Audio includes the flow-derivative source, spatialized
|
||||
intake/exhaust radiation, outdoor feedback-delay reverb, optional mechanical
|
||||
noise, a lock-free simulation audio buffer, a `SoundEngine` lifecycle wrapper,
|
||||
intake/exhaust radiation, outdoor feedback-delay reverb, a lock-free simulation
|
||||
audio buffer, a `SoundEngine` lifecycle wrapper,
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
gain. Simulation pressures and WAV capture levels are unaffected.
|
||||
|
||||
Both desktop engines use `Ecu` (`include/FluidSim/Ecu.hpp`). The desktop and
|
||||
CLI throttle values are pedal requests; the actual throttle is advanced every
|
||||
simulation sample, independently of rendering. Opening and closing use an
|
||||
80 ms first-order actuator response and a maximum travel rate of two full
|
||||
travels per second. The four-stroke uses an ideal butterfly projection; the
|
||||
1 ms first-order throttle-body response capped at 1000 full travels per second.
|
||||
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.
|
||||
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.
|
||||
|
||||
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
|
||||
control can still stall under excessive load and does not impose crankshaft
|
||||
speed or add artificial sustaining torque. Existing combustion/thermal models
|
||||
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
|
||||
throttle control. Its
|
||||
The two-stroke uses a fixed mechanical carb slide stop instead of RPM-controlled
|
||||
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
|
||||
contacts. Cylinder and crankcase moving-volume work use adiabatic energy changes.
|
||||
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
|
||||
`fluidsim_intake_regression` checks fresh-air supply and pressure/idle recovery.
|
||||
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`).
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
struct AudioFrame { float left{}, right{}; };
|
||||
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 {
|
||||
public:
|
||||
float gain{0.005f};
|
||||
@@ -17,34 +15,4 @@ private:
|
||||
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
|
||||
|
||||
37
include/FluidSim/Desktop/DesktopUi.hpp
Normal file
37
include/FluidSim/Desktop/DesktopUi.hpp
Normal 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};
|
||||
};
|
||||
}
|
||||
6
include/FluidSim/Desktop/DynoRenderer.hpp
Normal file
6
include/FluidSim/Desktop/DynoRenderer.hpp
Normal 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);
|
||||
}
|
||||
19
include/FluidSim/Desktop/EngineRenderer3D.hpp
Normal file
19
include/FluidSim/Desktop/EngineRenderer3D.hpp
Normal 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_;
|
||||
};
|
||||
}
|
||||
6
include/FluidSim/Desktop/EngineSchematic.hpp
Normal file
6
include/FluidSim/Desktop/EngineSchematic.hpp
Normal 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&);
|
||||
}
|
||||
20
include/FluidSim/Desktop/OrbitCamera.hpp
Normal file
20
include/FluidSim/Desktop/OrbitCamera.hpp
Normal 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);}
|
||||
};
|
||||
}
|
||||
15
include/FluidSim/Desktop/PedalControl.hpp
Normal file
15
include/FluidSim/Desktop/PedalControl.hpp
Normal 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_{};
|
||||
};
|
||||
}
|
||||
46
include/FluidSim/Desktop/PipePath.hpp
Normal file
46
include/FluidSim/Desktop/PipePath.hpp
Normal 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_;
|
||||
};
|
||||
}
|
||||
12
include/FluidSim/Desktop/PressurePalette.hpp
Normal file
12
include/FluidSim/Desktop/PressurePalette.hpp
Normal 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};
|
||||
}
|
||||
}
|
||||
7
include/FluidSim/Desktop/ScenarioMenu.hpp
Normal file
7
include/FluidSim/Desktop/ScenarioMenu.hpp
Normal 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>&);
|
||||
}
|
||||
@@ -9,13 +9,12 @@ namespace fluidsim::port {
|
||||
// the twin already protects its own simulation state.
|
||||
class DesktopScenario {
|
||||
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)
|
||||
two_ = std::make_unique<TwoStrokeScenario>(44100, config);
|
||||
else
|
||||
twin_ = std::make_unique<FourStrokeScenario>(44100, kind == ScenarioKind::SaabB205 ?
|
||||
FourStrokePreset::SaabB205 : kind == ScenarioKind::AmericanV8 ? FourStrokePreset::AmericanV8 : FourStrokePreset::Twin,
|
||||
kind == ScenarioKind::TurboTwin || (kind == ScenarioKind::SaabB205 && saab_turbo));
|
||||
FourStrokePreset::SaabB205 : kind == ScenarioKind::AmericanV8 ? FourStrokePreset::AmericanV8 : FourStrokePreset::Twin, exhaust, v8,engine);
|
||||
}
|
||||
AudioFrame process() {
|
||||
if (twin_)
|
||||
@@ -26,23 +25,23 @@ public:
|
||||
void set_controls(float pedal, float load, float 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_down() { visit([](auto& scenario) { scenario.shift_down(); }); }
|
||||
void set_neutral() { visit([](auto& scenario) { scenario.set_neutral(); }); }
|
||||
void toggle_mechanical_sound() {
|
||||
if (twin_)
|
||||
twin_->toggle_mechanical_sound();
|
||||
}
|
||||
void set_pipe_solver(PipeSolver solver) { visit([&](auto& scenario) { scenario.set_pipe_solver(solver); }); }
|
||||
void set_audio_scene(OutdoorScene scene) {
|
||||
visit([&](auto& scenario) { scenario.set_audio_scene(scene); });
|
||||
}
|
||||
ScenarioSnapshot snapshot() const {
|
||||
if (twin_)
|
||||
return twin_->snapshot();
|
||||
if (twin_) { auto s=twin_->snapshot();s.example_name=name_;return s; }
|
||||
const std::lock_guard lock(mutex_);
|
||||
const auto source = two_->snapshot();
|
||||
ScenarioSnapshot s;
|
||||
s.example_name=name_;
|
||||
s.pipe_solver=two_->pipes.front().solver;
|
||||
s.two_stroke = true;
|
||||
s.cylinder_angles[0]=two_->crankshaft.angle;
|
||||
s.cylinder_count = 1;
|
||||
s.racing_exhaust = two_->configuration.exhaust == TwoStrokeExhaust::Racing;
|
||||
s.rpm = source.rpm;
|
||||
@@ -81,6 +80,7 @@ private:
|
||||
if (twin_) function(*twin_);
|
||||
else function(*two_);
|
||||
}
|
||||
std::string name_;
|
||||
std::unique_ptr<FourStrokeTwinScenario> twin_;
|
||||
std::unique_ptr<TwoStrokeScenario> two_;
|
||||
mutable std::mutex mutex_;
|
||||
|
||||
@@ -6,12 +6,14 @@ namespace fluidsim::port {
|
||||
// Ideal sensors and injectors; calibration parameters are not engine measurements.
|
||||
class Ecu {
|
||||
public:
|
||||
enum class IdleControl { Feedback, FixedOpening };
|
||||
IdleControl idle_control{IdleControl::Feedback};
|
||||
struct IgnitionPoint { float rpm, degrees_btdc; };
|
||||
std::vector<IgnitionPoint> ignition_map{{0, 10}, {15000, 40}};
|
||||
float idle_rpm{1800.0f};
|
||||
float idle_base_opening{0.08f}, idle_max_opening{0.25f};
|
||||
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.
|
||||
float throttle_closing_time_constant{}, throttle_closing_rate{};
|
||||
float rev_limit_rpm{10000.0f}, rev_resume_rpm{9750.0f};
|
||||
@@ -20,12 +22,6 @@ public:
|
||||
bool overrun_fuel_cut_enabled{true};
|
||||
float idle_spark_gain{0.0f}, idle_minimum_advance{-5.0f}; // degrees/RPM, degrees BTDC
|
||||
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);
|
||||
float fuel_for_air(float trapped_air_mass) const;
|
||||
@@ -33,14 +29,12 @@ public:
|
||||
float throttle_target() const { return target_; }
|
||||
float target_afr() const { return afr_; }
|
||||
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 idle_active() const { return idle_; }
|
||||
bool sensor_fault() const { return fault_; }
|
||||
|
||||
private:
|
||||
float boost_target_{}, boost_duty_{}, boost_integral_{}, filtered_boost_{};
|
||||
bool boost_cut_{};
|
||||
float filtered_rpm_{};
|
||||
bool initialized_{};
|
||||
float opening_{}, target_{}, integral_{}, afr_{14.7f}, spark_{10.0f};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim/Engine.hpp"
|
||||
#include "FluidSim/OpenEndRadiation.hpp"
|
||||
#include "FluidSim/PipeSolver.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <deque>
|
||||
@@ -165,10 +167,14 @@ struct GasState {
|
||||
|
||||
class Pipe1D {
|
||||
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.
|
||||
struct Flux {
|
||||
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 damping_multiplier{0.0f}, energy_relaxation_rate{0.0f}, ambient_pressure{101325.0f};
|
||||
std::vector<float> cell_areas, cell_lengths;
|
||||
@@ -180,7 +186,7 @@ public:
|
||||
Pipe1D(std::vector<float> areas, std::vector<float> lengths)
|
||||
: 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()),
|
||||
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())
|
||||
throw std::invalid_argument("pipe profiles must be nonempty and have equal length");
|
||||
for (std::size_t i = 0; i < cells.size(); ++i)
|
||||
@@ -219,6 +225,12 @@ public:
|
||||
throw std::runtime_error("nonphysical pipe state in preparation");
|
||||
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;
|
||||
return dt;
|
||||
}
|
||||
@@ -251,6 +263,70 @@ public:
|
||||
static Flux riemann(const GasState &a, const GasState &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) {
|
||||
if (dt <= 0)
|
||||
return;
|
||||
@@ -259,12 +335,27 @@ public:
|
||||
const std::size_t n = cells.size();
|
||||
if (!prepared_)
|
||||
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] =
|
||||
right_override_ ? right_flux_ : scaled(riemann(states_[n - 1], right_ghost), cell_areas[n - 1]);
|
||||
for (std::size_t i = 1; i < n; ++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]));
|
||||
right_override_ ? right_flux_ : scaled(numerical_flux(states_[n - 1], right_ghost, solver), cell_areas[n - 1]);
|
||||
if (solver == PipeSolver::Rusanov) {
|
||||
for (std::size_t i = 1; i < n; ++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;
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
const auto fl = faces_[i], fr = faces_[i + 1];
|
||||
@@ -283,6 +374,11 @@ public:
|
||||
rho = 1e-12f;
|
||||
++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)
|
||||
mom *= std::exp(-.1f * damping_multiplier * dt / cells[i].rho);
|
||||
if (energy_relaxation_rate > 0)
|
||||
@@ -315,6 +411,7 @@ private:
|
||||
std::vector<GasState> states_;
|
||||
std::vector<float> waves_;
|
||||
std::vector<Flux> faces_;
|
||||
std::vector<RoeState> roe_states_;
|
||||
Flux left_flux_{}, right_flux_{};
|
||||
bool left_override_{}, right_override_{}, prepared_{};
|
||||
static std::vector<float> uniform(float v, int n) {
|
||||
@@ -387,6 +484,19 @@ public:
|
||||
claim(pipe, left);
|
||||
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.
|
||||
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() ||
|
||||
@@ -404,7 +514,23 @@ public:
|
||||
claim(b, 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_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; }
|
||||
std::uint64_t limited_fluxes{}, nonlinear_iterations{};
|
||||
void validate_topology() const {
|
||||
@@ -421,12 +547,52 @@ public:
|
||||
for (auto &o : orifices_)
|
||||
resolve_orifice(o, dt);
|
||||
for (auto &o : opens_)
|
||||
resolve_open(o);
|
||||
resolve_open(o, dt);
|
||||
for (auto &o : connections_)
|
||||
resolve_connection(o);
|
||||
for(auto& node:nodes_) resolve_junction(node,dt);
|
||||
}
|
||||
|
||||
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 {
|
||||
float *air{}, *exhaust{}, *fuel{}, *energy{};
|
||||
float mass{}, heat{};
|
||||
@@ -443,7 +609,10 @@ private:
|
||||
struct Open {
|
||||
int pipe;
|
||||
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 {
|
||||
int a, b;
|
||||
@@ -630,7 +799,7 @@ private:
|
||||
if (!from)
|
||||
v.fuel_fraction = s.fuel;
|
||||
}
|
||||
void resolve_open(Open &o) {
|
||||
void resolve_open(Open &o, float dt) {
|
||||
auto &p = pipes_[o.pipe];
|
||||
int i = end(p, o.left);
|
||||
auto s = p.state(i);
|
||||
@@ -638,6 +807,40 @@ private:
|
||||
if (o.left ? s.velocity >= c : s.velocity <= -c)
|
||||
throw std::runtime_error(
|
||||
"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 rho = std::pow(o.pressure / entropy, 1 / gamma);
|
||||
const float cb = std::sqrt(gamma * o.pressure / rho);
|
||||
@@ -653,6 +856,7 @@ private:
|
||||
f.p += s.pressure * (p.cell_area(i) - a);
|
||||
p.set_boundary_flux(o.left, f);
|
||||
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);
|
||||
}
|
||||
void resolve_connection(const Connection &o) {
|
||||
@@ -664,7 +868,7 @@ private:
|
||||
sa.velocity *= da;
|
||||
sb.velocity *= db;
|
||||
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,
|
||||
{da * f.m, f.p + sa.pressure * (a.cell_area(ia) - area), da * f.e,
|
||||
da * f.y, da * f.fuel});
|
||||
|
||||
34
include/FluidSim/Junction.hpp
Normal file
34
include/FluidSim/Junction.hpp
Normal 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_{};
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
@@ -10,11 +10,15 @@ class MotorcycleTest {
|
||||
public:
|
||||
Vehicle vehicle{};
|
||||
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:
|
||||
void step_motorcycle(Crankshaft& crank, float dt, float 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);
|
||||
if(starter_engaged) crank.add_torque(std::max(starter_torque,0.f));
|
||||
crank.step(dt);
|
||||
crank.drivetrain_torque = vehicle.update(crank.omega, crank.inertia, dt);
|
||||
}
|
||||
|
||||
27
include/FluidSim/OpenEndRadiation.hpp
Normal file
27
include/FluidSim/OpenEndRadiation.hpp
Normal 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
|
||||
@@ -1,12 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim/AudioProcessing.hpp"
|
||||
#include "FluidSim/OutdoorExhaustReverb.hpp"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
enum class OutdoorScene { Dry, Field, Wall, Woodland };
|
||||
enum class OutdoorScene { Dry, Field, Wall, Woodland, Tunnel };
|
||||
const char *scene_name(OutdoorScene scene);
|
||||
|
||||
struct OutdoorAudioConfig {
|
||||
@@ -28,9 +30,11 @@ struct OutdoorAudioConfig {
|
||||
class OutdoorAudio {
|
||||
public:
|
||||
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);
|
||||
void set_scene(OutdoorScene scene) { scene_ = scene; }
|
||||
OutdoorScene scene() const { return scene_; }
|
||||
void set_scene(OutdoorScene scene) { requested_scene_.store(scene, std::memory_order_relaxed); }
|
||||
OutdoorScene scene() const { return requested_scene_.load(std::memory_order_relaxed); }
|
||||
void set_monitor_filter(bool enabled) { monitor_filter_ = enabled; } // Set before playback/capture.
|
||||
AudioFrame dry_frame() const { return last_dry_; }
|
||||
AudioFrame unfiltered_frame() const { return last_raw_; }
|
||||
@@ -44,7 +48,7 @@ private:
|
||||
std::size_t delay_left{}, delay_right{};
|
||||
float fraction_left{}, fraction_right{}, gain_left{}, gain_right{}, alpha{1};
|
||||
float state_left{}, state_right{};
|
||||
int group{}; // direct, ground, wall, trees
|
||||
int group{}; // direct, ground, wall, trees, tunnel
|
||||
};
|
||||
struct Emitter {
|
||||
std::vector<float> filtered, raw;
|
||||
@@ -53,22 +57,17 @@ private:
|
||||
float previous_flow{};
|
||||
LowPass filter;
|
||||
};
|
||||
struct TailLine {
|
||||
std::vector<float> buffer;
|
||||
std::size_t position{};
|
||||
float gain{}, state{};
|
||||
};
|
||||
int rate_{};
|
||||
OutdoorAudioConfig config_;
|
||||
OutdoorScene scene_{OutdoorScene::Wall};
|
||||
std::atomic<OutdoorScene> requested_scene_{OutdoorScene::Wall};
|
||||
OutdoorScene active_scene_{OutdoorScene::Wall};
|
||||
bool monitor_filter_{true};
|
||||
std::array<Emitter, 3> emitters_;
|
||||
std::array<TailLine, 4> tail_;
|
||||
std::array<float, 4> weights_{1, 0, 0, 0};
|
||||
float scene_alpha_{}, tail_alpha_{}, tail_mix_{};
|
||||
OutdoorExhaustReverb late_reverb_;
|
||||
std::array<float, 5> weights_{1, 0, 0, 0, 0};
|
||||
float scene_alpha_{}, tail_mix_{};
|
||||
AudioFrame last_dry_{}, last_raw_{};
|
||||
void configure_emitter(Emitter &emitter, Vec3 position);
|
||||
AudioFrame render(Emitter &emitter, float flow, AudioFrame &dry, AudioFrame &raw);
|
||||
AudioFrame diffuse(float input);
|
||||
};
|
||||
} // namespace fluidsim::port
|
||||
|
||||
@@ -19,6 +19,7 @@ public:
|
||||
explicit OutdoorExhaustReverb(int sample_rate = 44100);
|
||||
AudioFrame process_stereo(float input);
|
||||
float process(float input);
|
||||
void set_decay_times(float low_seconds, float mid_seconds, float high_seconds);
|
||||
|
||||
private:
|
||||
class DelayLine {
|
||||
@@ -40,7 +41,7 @@ private:
|
||||
};
|
||||
|
||||
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<OnePoleLowPass> low_filters_, mid_filters_;
|
||||
std::vector<float> read_buffer_, mix_buffer_;
|
||||
@@ -49,6 +50,7 @@ private:
|
||||
float fast_envelope_{}, slow_envelope_{}, bloom_{};
|
||||
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);
|
||||
};
|
||||
|
||||
|
||||
20
include/FluidSim/PipeSolver.hpp
Normal file
20
include/FluidSim/PipeSolver.hpp
Normal 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
|
||||
@@ -1,16 +1,17 @@
|
||||
#pragma once
|
||||
#include "FluidSim/Silencer.hpp"
|
||||
#include "FluidSim/Carburetor.hpp"
|
||||
#include <string>
|
||||
#include "FluidSim/Junction.hpp"
|
||||
|
||||
#include "FluidSim/AudioProcessing.hpp"
|
||||
#include "FluidSim/Atmosphere.hpp"
|
||||
#include "FluidSim/Engine.hpp"
|
||||
#include "FluidSim/Ecu.hpp"
|
||||
#include "FluidSim/Fluid.hpp"
|
||||
#include "FluidSim/MechanicalSoundProcessor.hpp"
|
||||
#include "FluidSim/OutdoorAudio.hpp"
|
||||
#include "FluidSim/Solver.hpp"
|
||||
#include "FluidSim/Vehicle.hpp"
|
||||
#include "FluidSim/MotorcycleTest.hpp"
|
||||
#include "FluidSim/Turbocharger.hpp"
|
||||
#include <memory>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
@@ -19,34 +20,60 @@
|
||||
|
||||
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 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 {
|
||||
bool turbo_enabled{};
|
||||
float boost_target{}, wastegate_duty{};
|
||||
TurboSnapshot turbo{};
|
||||
std::string example_name;
|
||||
EngineLayout layout{EngineLayout::Inline};
|
||||
float bank_angle{};
|
||||
bool v8_headers{};
|
||||
PipeSolver pipe_solver{PipeSolver::Rusanov};
|
||||
int cylinder_count{2};
|
||||
float rev_limit_rpm{10000};
|
||||
std::array<float, 8> cylinder_pressures{}, piston_fractions{}, intake_areas{}, exhaust_areas{};
|
||||
std::uint64_t solver_substeps{}, boundary_iterations{};
|
||||
std::array<float, 12> cylinder_angles{};
|
||||
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 cylinder_pressure1{}, cylinder_pressure2{};
|
||||
float cylinder_temperature1{}, cylinder_temperature2{};
|
||||
float cylinder_piston_fraction1{}, cylinder_piston_fraction2{};
|
||||
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{};
|
||||
bool idle_control{}, fuel_cut{}, two_stroke{}, reed_open{};
|
||||
bool racing_exhaust{};
|
||||
bool silencers{}, h_pipe{};
|
||||
ExhaustConfig exhaust_config{};
|
||||
float crankcase_pressure{};
|
||||
OutdoorScene audio_scene{OutdoorScene::Wall};
|
||||
int gear{};
|
||||
bool rev_limit1{}, rev_limit2{}, mechanical_sound_enabled{};
|
||||
std::array<std::vector<GasState>, 22> pipe_states;
|
||||
std::array<std::vector<float>, 22> pipe_areas, pipe_lengths;
|
||||
bool rev_limit1{}, rev_limit2{};
|
||||
std::array<std::vector<GasState>, 32> pipe_states;
|
||||
std::array<std::vector<float>, 32> pipe_areas, pipe_lengths;
|
||||
void capture_pipes(const std::vector<Pipe1D>& pipes) {
|
||||
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_lengths[k] = pipes[k].cell_lengths;
|
||||
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.
|
||||
class FourStrokeScenario : public MotorcycleTest {
|
||||
public:
|
||||
explicit FourStrokeScenario(int sample_rate = 44100, FourStrokePreset preset = FourStrokePreset::Twin,
|
||||
bool turbo_enabled = false);
|
||||
explicit FourStrokeScenario(int sample_rate = 44100, FourStrokePreset preset = FourStrokePreset::Twin, ExhaustConfig exhaust = {}, V8ExhaustConfig v8_exhaust = {}, EngineConfig engine = {});
|
||||
const FourStrokePreset preset;
|
||||
int cylinder_count() const { return preset == FourStrokePreset::AmericanV8 ? 8 : preset == FourStrokePreset::SaabB205 ? 4 : 2; }
|
||||
std::array<FourStrokeCylinder*, 8> cylinders() { return {&cylinder1, &cylinder2, &cylinder3, &cylinder4,
|
||||
&cylinder5, &cylinder6, &cylinder7, &cylinder8}; }
|
||||
const ExhaustConfig exhaust_config;
|
||||
const V8ExhaustConfig v8_exhaust_config;
|
||||
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();
|
||||
void set_controls(float throttle_value, float load_value, float clutch_value);
|
||||
void toggle_mechanical_sound();
|
||||
void set_audio_scene(OutdoorScene scene);
|
||||
void set_pipe_solver(PipeSolver solver);
|
||||
void set_monitor_filter(bool enabled);
|
||||
void set_starter(bool engaged);
|
||||
AudioFrame dry_audio() const;
|
||||
AudioFrame unfiltered_audio() const;
|
||||
void shift_up();
|
||||
@@ -80,34 +110,31 @@ public:
|
||||
ScenarioSnapshot snapshot() const;
|
||||
|
||||
float throttle{};
|
||||
bool mechanical_sound_enabled{};
|
||||
float mechanical_mix{0.02f};
|
||||
Ecu ecu{};
|
||||
std::unique_ptr<Turbocharger> turbo;
|
||||
Crankshaft crankshaft{};
|
||||
FourStrokeCylinder cylinder1{}, cylinder2{}, cylinder3{}, cylinder4{};
|
||||
FourStrokeCylinder cylinder5{}, cylinder6{}, cylinder7{}, cylinder8{};
|
||||
Atmosphere intake_atmosphere{};
|
||||
FourStrokeCylinder cylinder9{}, cylinder10{}, cylinder11{}, cylinder12{};
|
||||
Volume0D intake_plenum{1.0e-3f, 101325.0f, 300.0f};
|
||||
Volume0D exhaust_collector{0.15e-3f, 101325.0f, 700.0f};
|
||||
Volume0D second_bank_collector{1.5e-3f, 101325.0f, 700.0f};
|
||||
Junction exhaust_collector{0.15e-3f, 101325.0f, 700.0f};
|
||||
std::vector<Pipe1D> pipes;
|
||||
float intake_flow{}, exhaust_flow{};
|
||||
float last_mix_l{}, last_mix_r{};
|
||||
std::uint64_t sample_count{};
|
||||
|
||||
private:
|
||||
float intake_boundary_area_{}, throttle_area_{}, runner_area_{}, header_area_{}, tail_area_{};
|
||||
std::array<float, 8> intake_valve_areas_{}, exhaust_valve_areas_{};
|
||||
float throttle_area_{}, runner_area_{}, header_area_{}, tail_area_{};
|
||||
std::array<float, 12> intake_valve_areas_{}, exhaust_valve_areas_{};
|
||||
float dt_{};
|
||||
BoundarySystem boundaries_;
|
||||
Solver solver_;
|
||||
OutdoorAudio audio_;
|
||||
MechanicalSoundProcessor mechanical_sound_;
|
||||
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 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;
|
||||
@@ -116,17 +143,10 @@ public:
|
||||
explicit AmericanV8Scenario(int sample_rate = 44100)
|
||||
: 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 {
|
||||
public:
|
||||
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
|
||||
|
||||
83
include/FluidSim/ScenarioCatalog.hpp
Normal file
83
include/FluidSim/ScenarioCatalog.hpp
Normal 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;
|
||||
}
|
||||
}
|
||||
54
include/FluidSim/Silencer.hpp
Normal file
54
include/FluidSim/Silencer.hpp
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -13,7 +13,10 @@ struct TwoStrokeConfig {
|
||||
float trapped_compression{8.2f}; // exhaust-closure volume / clearance volume
|
||||
float transfer_duration{130}, exhaust_duration{190};
|
||||
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 idle_slide_opening{.045f}; // fixed mechanical stop, normalized 0..1 (4.5%)
|
||||
float minimum_burn_seconds{.0008f};
|
||||
// Empirical mechanical FMEP = A + B*mean_piston_speed + C*speed^2.
|
||||
// Generic correlation estimates, not a measured YZ125 coast-down fit.
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "FluidSim/Carburetor.hpp"
|
||||
#include "FluidSim/Fluid.hpp"
|
||||
#include "FluidSim/OutdoorAudio.hpp"
|
||||
#include "FluidSim/Atmosphere.hpp"
|
||||
#include "FluidSim/Ecu.hpp"
|
||||
#include "FluidSim/ReedValve.hpp"
|
||||
#include "FluidSim/MotorcycleTest.hpp"
|
||||
@@ -36,6 +35,7 @@ public:
|
||||
void shift_up() { vehicle.shift_up(); }
|
||||
void shift_down() { vehicle.shift_down(); }
|
||||
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_monitor_filter(bool enabled) { audio_.set_monitor_filter(enabled); }
|
||||
AudioFrame dry_audio() const { return audio_.dry_frame(); }
|
||||
@@ -47,7 +47,6 @@ public:
|
||||
Ecu ecu{};
|
||||
Carburetor carburetor;
|
||||
ReedValve reed{};
|
||||
Atmosphere intake_atmosphere{};
|
||||
float intake_flow{}, exhaust_flow{};
|
||||
Crankshaft crankshaft{};
|
||||
TwoStrokeCylinder cylinder;
|
||||
@@ -56,7 +55,7 @@ public:
|
||||
std::vector<Pipe1D> pipes;
|
||||
|
||||
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_{};
|
||||
bool reed_open_{};
|
||||
std::uint64_t step_count_{};
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#include "FluidSim/Engine.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
@@ -20,67 +19,4 @@ float SoundProcessor::process(float mass_flow_out) {
|
||||
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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "FluidSim/DesktopScenario.hpp"
|
||||
#include "FluidSim/SoundEngine.hpp"
|
||||
#include "FluidSim/DynoCurve.hpp"
|
||||
#include "FluidSim/Desktop/DesktopUi.hpp"
|
||||
#include "FluidSim/Desktop/ScenarioMenu.hpp"
|
||||
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <algorithm>
|
||||
@@ -18,510 +19,41 @@
|
||||
namespace {
|
||||
using namespace fluidsim::port;
|
||||
|
||||
constexpr unsigned int window_width = 1280;
|
||||
constexpr unsigned int window_height = 720;
|
||||
constexpr unsigned int window_width = 1440;
|
||||
constexpr unsigned int window_height = 900;
|
||||
constexpr std::size_t ring_capacity = 8192;
|
||||
constexpr std::size_t target_fill = 4096;
|
||||
constexpr std::size_t preroll_samples = 4096;
|
||||
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
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
using namespace fluidsim::port;
|
||||
ScenarioKind selected_kind=ScenarioKind::Twin;
|
||||
bool scenario_from_args=false;
|
||||
bool turbo_from_args=false;
|
||||
std::string turbo_name;
|
||||
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;
|
||||
for(int i=1;i<argc;++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];
|
||||
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;}
|
||||
selected_kind=value=="v8" ? ScenarioKind::AmericanV8 : value=="b205" ? ScenarioKind::SaabB205 :
|
||||
(value=="two-stroke" ? ScenarioKind::TwoStroke : ScenarioKind::Twin);
|
||||
scenario_from_args=true;
|
||||
}else if(option=="--turbo"){
|
||||
if(value!="none"&&value!="vz21"&&value!="gt17"){std::cerr<<"Choose none, vz21 or gt17 turbo\n";return 2;}
|
||||
turbo_name=value;
|
||||
turbo_from_args=value!="none";
|
||||
}else if(option=="--solver"){
|
||||
try { selected_solver=parse_pipe_solver(value); }
|
||||
catch(const std::exception& e) { std::cerr<<e.what()<<"\n"; return 2; }
|
||||
}else if(option=="--exhaust"){
|
||||
if(value=="racing") two_stroke_config.exhaust=TwoStrokeExhaust::Racing;
|
||||
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=="wall")selected_scene=OutdoorScene::Wall;
|
||||
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}
|
||||
: 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.setFramerateLimit(60);
|
||||
window.setKeyRepeatEnabled(false);
|
||||
@@ -555,25 +90,22 @@ int main(int argc, char** argv) {
|
||||
const sf::Font* font_ptr=has_font?&font:nullptr;
|
||||
|
||||
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;
|
||||
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.setView(sf::View(sf::FloatRect({0, 0}, {float(window_width), float(window_height)})));
|
||||
}
|
||||
if(turbo_from_args) {
|
||||
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" :
|
||||
window.setTitle(selected_kind == ScenarioKind::AmericanV8 ? "FluidSim - 1980s American 5.0 V8" : selected_kind == ScenarioKind::SaabB205 ? "FluidSim - Saab B205 / 9-5 wagon" :
|
||||
(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);
|
||||
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);
|
||||
for (std::size_t i = 0; i < preroll_samples; ++i) {
|
||||
const AudioFrame audio = scenario.process();
|
||||
@@ -598,15 +130,17 @@ int main(int argc, char** argv) {
|
||||
double current_speed = desired_speed;
|
||||
double last_normal_speed = 1.0;
|
||||
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 clutch_current = 0.0f;
|
||||
bool throttle_held = false, clutch_held = false;
|
||||
DynoCurve dyno_curve;
|
||||
bool throttle_held = false, throttle_down = false, clutch_held = false;
|
||||
desktop::DesktopUi ui;
|
||||
auto previous_frame = std::chrono::steady_clock::now();
|
||||
|
||||
while (window.isOpen()) {
|
||||
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 (const auto* key = event->getIf<sf::Event::KeyPressed>()) {
|
||||
switch (key->code) {
|
||||
@@ -614,33 +148,41 @@ int main(int argc, char** argv) {
|
||||
time_warp = !time_warp;
|
||||
if (!time_warp) { desired_speed = last_normal_speed; }
|
||||
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::V: scenario.set_starter(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::Down: throttle_target = std::max(0.0f, throttle_target - 0.05f); break;
|
||||
case sf::Keyboard::Key::Up: ui.nudge_pedal(.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::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::A: scenario.shift_down(); 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:
|
||||
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);
|
||||
break;
|
||||
case sf::Keyboard::Key::M: scenario.toggle_mechanical_sound(); break;
|
||||
case sf::Keyboard::Key::G: audio.set_auto_gain(!audio.auto_gain()); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
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::S) throttle_down = 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();
|
||||
@@ -650,15 +192,18 @@ int main(int argc, char** argv) {
|
||||
current_speed += (target_speed - current_speed) * (1.0 - std::exp(-8.0 * frame_dt));
|
||||
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.
|
||||
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;
|
||||
clutch_current += (wanted_clutch - clutch_current) * (1.0f - std::exp(-throttle_lerp_rate * frame_dt));
|
||||
scenario.set_controls(throttle_current, brake_current, clutch_current);
|
||||
|
||||
draw_scene(window, font_ptr, scenario.snapshot(), throttle_target, brake_current,
|
||||
current_speed, ring.available_samples(), ring_capacity, time_warp, dyno_curve, audio.underrun_blocks(), audio.auto_gain());
|
||||
const desktop::PlaybackInfo playback{throttle_current,brake_current,current_speed,
|
||||
ring.available_samples(),ring_capacity,audio.underrun_blocks(),audio.auto_gain(),time_warp};
|
||||
ui.draw(window,font_ptr,scenario.snapshot(),playback);
|
||||
|
||||
}
|
||||
|
||||
simulation.request_stop();
|
||||
|
||||
119
src/desktop_ui.cpp
Normal file
119
src/desktop_ui.cpp
Normal 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
128
src/dyno_renderer.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
28
src/ecu.cpp
28
src/ecu.cpp
@@ -3,27 +3,6 @@
|
||||
#include <cmath>
|
||||
|
||||
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) {
|
||||
fault_ = !std::isfinite(dt) || dt <= 0.0f || !std::isfinite(pedal) || !std::isfinite(rpm) || rpm < 0.0f;
|
||||
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));
|
||||
idle_ = pedal <= 0.005f && !overrun_;
|
||||
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 command = idle_base_opening + idle_kp * error + proposed;
|
||||
// Conditional integration prevents windup at either actuator limit.
|
||||
@@ -48,7 +28,7 @@ void Ecu::step(float dt, float pedal, float rpm) {
|
||||
} else {
|
||||
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;
|
||||
target_ = std::max(pedal, idle_command);
|
||||
const bool closing = target_ < opening_;
|
||||
@@ -71,7 +51,7 @@ void Ecu::step(float dt, float pedal, float rpm) {
|
||||
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 {
|
||||
|
||||
254
src/engine_renderer_3d.cpp
Normal file
254
src/engine_renderer_3d.cpp
Normal 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
227
src/engine_schematic.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ HelmholtzScenario::HelmholtzScenario(int sample_rate)
|
||||
pipes_.emplace_back(neck_area_, 0.050f, 20);
|
||||
cavity_port_ = &cavity_.create_port();
|
||||
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_.add_component(cavity_);
|
||||
}
|
||||
|
||||
27
src/main.cpp
27
src/main.cpp
@@ -16,18 +16,15 @@ int main(int argc, char** argv) {
|
||||
float throttle = 0.35f;
|
||||
std::string scenario_name = "twin";
|
||||
TwoStrokeConfig two_stroke_config;
|
||||
bool turbo_enabled = false;
|
||||
std::string turbo_name;
|
||||
PipeSolver pipe_solver=PipeSolver::Rusanov;
|
||||
for (int i = 1; i + 1 < argc; ++i) {
|
||||
const std::string option = argv[i];
|
||||
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 == "--scenario") scenario_name = argv[++i];
|
||||
else if (option == "--turbo") {
|
||||
const std::string value = argv[++i];
|
||||
if (value != "none" && value != "vz21" && value != "gt17") { std::cerr << "Choose none, vz21 or gt17 turbo\n"; return 2; }
|
||||
turbo_name = value;
|
||||
turbo_enabled = value != "none";
|
||||
else if (option == "--solver") {
|
||||
try { pipe_solver=parse_pipe_solver(argv[++i]); }
|
||||
catch(const std::exception& e) { std::cerr<<e.what()<<"\n"; return 2; }
|
||||
}
|
||||
else if (option == "--exhaust") {
|
||||
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;
|
||||
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);
|
||||
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;
|
||||
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";
|
||||
@@ -63,17 +55,12 @@ int main(int argc, char** argv) {
|
||||
<< scenario.cylinder2.pressure / 1000.0f << " kPa"
|
||||
<< " intake=" << scenario.intake_plenum.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';
|
||||
}
|
||||
}
|
||||
} else if (scenario_name == "two-stroke") {
|
||||
TwoStrokeScenario scenario(sample_rate, two_stroke_config);
|
||||
scenario.set_pipe_solver(pipe_solver);
|
||||
scenario.set_throttle(throttle);
|
||||
std::cout << "FluidSimCpp | 125 cc crankcase-scavenged two-stroke | throttle "
|
||||
<< std::fixed << std::setprecision(0) << throttle * 100.0f << "%\n";
|
||||
|
||||
@@ -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
|
||||
@@ -22,6 +22,8 @@ const char *scene_name(OutdoorScene scene) {
|
||||
return "Nearby wall";
|
||||
case OutdoorScene::Woodland:
|
||||
return "Woodland";
|
||||
case OutdoorScene::Tunnel:
|
||||
return "Tunnel";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
@@ -35,7 +37,7 @@ float OutdoorAudio::LowPass::process(float x) {
|
||||
z2 = 0;
|
||||
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 ||
|
||||
!std::isfinite(config.monitor_cutoff_hz) || config.monitor_cutoff_hz <= 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_[1], config_.exhaust_position);
|
||||
if (config_.dual_exhaust) configure_emitter(emitters_[2], config_.second_exhaust_position);
|
||||
constexpr std::array<int, 4> lengths{1499, 2111, 2633, 3251};
|
||||
for (std::size_t i = 0; i < tail_.size(); ++i) {
|
||||
auto &line = tail_[i];
|
||||
line.buffer.resize(std::max(1, int(std::lround(lengths[i] * rate_ / 44100.0))));
|
||||
line.gain = std::pow(10.0f, -3.0f * (float(line.buffer.size()) / rate_) / 0.35f);
|
||||
}
|
||||
late_reverb_.dry_mix = 0;
|
||||
late_reverb_.early_mix = 0;
|
||||
late_reverb_.tail_mix = 1;
|
||||
late_reverb_.set_decay_times(.75f, .45f, .22f);
|
||||
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) {
|
||||
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.fraction_left = sl - p.delay_left;
|
||||
p.fraction_right = sr - p.delay_right;
|
||||
p.gain_left = coefficient / (dl * source_distance);
|
||||
p.gain_right = coefficient / (dr * source_distance);
|
||||
// A scatterer adds path length before the listener leg. Use spreading
|
||||
// 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;
|
||||
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);
|
||||
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 w = 2 * pi * cutoff / rate_, cs = std::cos(w), alpha = std::sin(w) / std::sqrt(2.0f),
|
||||
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) {
|
||||
const float pressure = (flow - e.previous_flow) * rate_ * config_.pressure_gain / (4 * pi);
|
||||
e.previous_flow = flow;
|
||||
const float filtered = e.filter.process(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 i = (e.position + data.size() - delay) & (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);
|
||||
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) {
|
||||
if (!std::isfinite(intake) || !std::isfinite(exhaust) || !std::isfinite(second_exhaust))
|
||||
throw std::runtime_error("nonfinite acoustic source flow");
|
||||
const bool wet = scene_ != OutdoorScene::Dry;
|
||||
const std::array<float, 4> target{1, wet ? 1.f : 0.f, scene_ == OutdoorScene::Wall ? 1.f : 0.f,
|
||||
scene_ == OutdoorScene::Woodland ? 1.f : 0.f};
|
||||
for (std::size_t i = 0; i < 4; ++i)
|
||||
const OutdoorScene scene = requested_scene_.load(std::memory_order_relaxed);
|
||||
if (scene != active_scene_) {
|
||||
active_scene_ = scene;
|
||||
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_;
|
||||
const float target_tail =
|
||||
scene_ == OutdoorScene::Wall
|
||||
? .14f
|
||||
: (scene_ == OutdoorScene::Woodland ? .25f : (scene_ == OutdoorScene::Field ? .025f : 0));
|
||||
// Keep the diffuse tail present between sharp events; the prior mix values
|
||||
// made it perceptible mainly after large transients.
|
||||
const float target_tail = scene == OutdoorScene::Wall ? .75f
|
||||
: scene == OutdoorScene::Woodland ? 1.0f
|
||||
: scene == OutdoorScene::Tunnel ? 1.15f
|
||||
: scene == OutdoorScene::Field ? .30f : 0.f;
|
||||
tail_mix_ += (target_tail - tail_mix_) * scene_alpha_;
|
||||
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 c = config_.dual_exhaust ? render(emitters_[2], second_exhaust, last_dry_, last_raw_) : AudioFrame{};
|
||||
const auto tail = diffuse((last_dry_.left + last_dry_.right) * .5f);
|
||||
const auto c = config_.dual_exhaust
|
||||
? 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_};
|
||||
}
|
||||
} // namespace fluidsim::port
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
@@ -38,23 +39,26 @@ OutdoorExhaustReverb::OutdoorExhaustReverb(int sample_rate)
|
||||
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_),
|
||||
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);
|
||||
float average_delay_seconds = 0.0f;
|
||||
fdn_.reserve(lengths.size());
|
||||
low_filters_.reserve(lengths.size());
|
||||
mid_filters_.reserve(lengths.size());
|
||||
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));
|
||||
low_filters_.emplace_back(band_low_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>(lengths.size());
|
||||
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);
|
||||
average_delay_seconds_ = average_delay_seconds;
|
||||
update_feedback_gains();
|
||||
read_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));
|
||||
}
|
||||
|
||||
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) {
|
||||
const float magnitude = std::abs(input);
|
||||
fast_envelope_ += (magnitude > fast_envelope_ ? fast_attack_ : fast_release_) * (magnitude - fast_envelope_);
|
||||
|
||||
@@ -22,7 +22,7 @@ PulseTestScenario::PulseTestScenario(int sample_rate)
|
||||
{bubble_density, 0.0f, bubble_pressure / (gamma - 1.0f), 1.0f};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
203
src/scenario.cpp
203
src/scenario.cpp
@@ -1,29 +1,32 @@
|
||||
#include "FluidSim/Scenario.hpp"
|
||||
#include "FluidSim/Constants.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selected_preset, bool turbo_enabled)
|
||||
: preset(selected_preset),
|
||||
intake_plenum(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),
|
||||
pipes(create_pipes(selected_preset, turbo_enabled)), dt_(1.f / std::max(sample_rate, 1)),
|
||||
FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selected_preset, ExhaustConfig exhaust, V8ExhaustConfig v8_exhaust, EngineConfig engine)
|
||||
: preset(selected_preset), exhaust_config(exhaust), v8_exhaust_config(v8_exhaust), engine_config(engine),
|
||||
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(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, exhaust, v8_exhaust, engine)), dt_(1.f / std::max(sample_rate, 1)),
|
||||
boundaries_(pipes), solver_(pipes, boundaries_), audio_(sample_rate, [&] {
|
||||
OutdoorAudioConfig config;
|
||||
config.dual_exhaust = selected_preset == FourStrokePreset::AmericanV8;
|
||||
return config;
|
||||
}()), mechanical_sound_(sample_rate) {
|
||||
}()) {
|
||||
const bool saab = preset == FourStrokePreset::SaabB205;
|
||||
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();
|
||||
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.inertia = saab ? .18f : .04f;
|
||||
crankshaft.friction_constant = saab ? 12.f : 5.f;
|
||||
crankshaft.friction_viscous = saab ? .012f : .003f;
|
||||
if (saab) {
|
||||
solver_.minimum_substeps = 2; // Acoustic CFL may subdivide further.
|
||||
// Rough 9-5 wagon with driver; FM55 B05 overall reductions from WIS.
|
||||
vehicle.mass = 1600 + 75;
|
||||
vehicle.primary_reduction = 1;
|
||||
@@ -57,10 +60,6 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selecte
|
||||
vehicle.drag_coefficient = .4f;
|
||||
vehicle.frontal_area = 2.f;
|
||||
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_spark_gain = .02f;
|
||||
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.overrun_cut_rpm = 2000;
|
||||
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;
|
||||
}
|
||||
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.clutch_input = clutch;
|
||||
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};
|
||||
for (int i = 0; i < count; ++i) {
|
||||
auto& c = *engines[i];
|
||||
c.phase = phases[i];
|
||||
c.phase = phases[i%4];
|
||||
if (v8) {
|
||||
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);
|
||||
@@ -105,21 +122,28 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selecte
|
||||
c.intake_valve_diameter = .045f; c.exhaust_valve_diameter = .037f;
|
||||
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_rpm = ecu.rev_limit_rpm;
|
||||
c.rev_limit_hysteresis = 250;
|
||||
c.rev_limit_cut_time = .020f;
|
||||
c.ecu = &ecu;
|
||||
c.initialize(crankshaft);
|
||||
if (saab || v8) {
|
||||
if (saab || v8 || engine.cylinders) {
|
||||
// Initialize each phased cylinder at its actual piston position.
|
||||
c.volume = c.compute_volume(c.phase);
|
||||
c.air_mass = ambient_density * c.volume;
|
||||
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);
|
||||
}
|
||||
}
|
||||
intake_boundary_area_ = pipes.front().area;
|
||||
runner_area_ = pipes[1].area;
|
||||
header_area_ = pipes[1+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;
|
||||
}
|
||||
auto& throttle_port = intake_plenum.create_port();
|
||||
solver_.add_component(intake_atmosphere);
|
||||
throttle_port_=&throttle_port;
|
||||
solver_.add_component(intake_plenum);
|
||||
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_);
|
||||
}
|
||||
if(!v8) solver_.add_component(exhaust_collector);
|
||||
boundaries_.add_orifice(throttle_port, 0, false, throttle_area_, .7f);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
auto& feed = intake_plenum.create_port();
|
||||
// 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]);
|
||||
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]->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();
|
||||
boundaries_.add_orifice(collector_out, 1+2*count, true, tail_area_);
|
||||
const int exhaust_exit = turbo_enabled ? 3+2*count : 1+2*count;
|
||||
boundaries_.add_open_end(exhaust_exit, false, 101325, pipes[exhaust_exit].area);
|
||||
if (v8) {
|
||||
auto& outlet = second_bank_collector.create_port();
|
||||
boundaries_.add_orifice(outlet, 18, true, tail_area_);
|
||||
boundaries_.add_open_end(18, false, 101325, pipes[18].area);
|
||||
if(v8) {
|
||||
auto junction=[&](std::initializer_list<JunctionEnd> ends) {
|
||||
auto fitting=std::make_unique<Junction>(v8_exhaust.junction_volume,101325,700);
|
||||
fitting->connect(boundaries_,pipes,std::span(ends.begin(),ends.size()));
|
||||
solver_.add_component(*fitting);exhaust_junctions_.push_back(std::move(fitting));
|
||||
};
|
||||
// Within each bank pair cylinders with the widest available firing gaps.
|
||||
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() {
|
||||
const std::lock_guard guard(mutex_);
|
||||
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));
|
||||
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();
|
||||
for (int i = 0; i < cylinder_count(); ++i) {
|
||||
auto& c = *engines[i];
|
||||
@@ -182,19 +212,30 @@ AudioFrame FourStrokeScenario::process() {
|
||||
}
|
||||
const float opening = ecu.throttle_opening();
|
||||
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_);
|
||||
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);
|
||||
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;
|
||||
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;
|
||||
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) {
|
||||
@@ -210,6 +251,11 @@ AudioFrame FourStrokeScenario::unfiltered_audio() const {
|
||||
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) {
|
||||
const std::lock_guard<std::mutex> guard(mutex_);
|
||||
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);
|
||||
}
|
||||
|
||||
void FourStrokeScenario::toggle_mechanical_sound() {
|
||||
const std::lock_guard<std::mutex> guard(mutex_);
|
||||
mechanical_sound_enabled = !mechanical_sound_enabled;
|
||||
}
|
||||
|
||||
void FourStrokeScenario::shift_up() {
|
||||
const std::lock_guard<std::mutex> guard(mutex_);
|
||||
vehicle.shift_up();
|
||||
@@ -245,10 +286,14 @@ void FourStrokeScenario::set_first_gear() {
|
||||
ScenarioSnapshot FourStrokeScenario::snapshot() const {
|
||||
const std::lock_guard<std::mutex> guard(mutex_);
|
||||
ScenarioSnapshot result;
|
||||
result.turbo_enabled = bool(turbo);
|
||||
result.boost_target = ecu.boost_target();
|
||||
result.wastegate_duty = ecu.wastegate_duty();
|
||||
if (turbo) result.turbo = turbo->snapshot();
|
||||
result.layout=engine_config.cylinders?engine_config.layout:preset==FourStrokePreset::AmericanV8?EngineLayout::V:EngineLayout::Inline;
|
||||
result.bank_angle=engine_config.cylinders?engine_config.bank_angle:56;
|
||||
result.v8_headers=preset==FourStrokePreset::AmericanV8;
|
||||
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.boundary_iterations = boundaries_.nonlinear_iterations;
|
||||
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_exhaust_area = exhaust_valve_areas_[1];
|
||||
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.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.fuel_cut = ecu.fuel_cut();
|
||||
result.audio_scene = audio_.scene();
|
||||
@@ -280,12 +326,12 @@ ScenarioSnapshot FourStrokeScenario::snapshot() const {
|
||||
result.gear = vehicle.current_gear;
|
||||
result.rev_limit1 = cylinder1.rev_limit_active;
|
||||
result.rev_limit2 = cylinder2.rev_limit_active;
|
||||
result.mechanical_sound_enabled = mechanical_sound_enabled;
|
||||
result.cylinder_count = cylinder_count();
|
||||
result.rev_limit_rpm = ecu.rev_limit_rpm;
|
||||
const std::array<const FourStrokeCylinder*, 8> engines{&cylinder1, &cylinder2, &cylinder3, &cylinder4,
|
||||
&cylinder5, &cylinder6, &cylinder7, &cylinder8};
|
||||
const std::array<const FourStrokeCylinder*, 12> engines{&cylinder1, &cylinder2, &cylinder3, &cylinder4,
|
||||
&cylinder5, &cylinder6, &cylinder7, &cylinder8,&cylinder9,&cylinder10,&cylinder11,&cylinder12};
|
||||
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.piston_fractions[i] = (engines[i]->volume-engines[i]->clearance_volume())/engines[i]->swept_volume();
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
if (preset == FourStrokePreset::AmericanV8) {
|
||||
// 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.
|
||||
std::vector<Pipe1D> result;
|
||||
result.emplace_back(area_for_diameter(.070f), .18f, 3);
|
||||
for (int i = 0; i < 8; ++i)
|
||||
result.emplace_back(area_for_diameter(.038f), .12f, 2);
|
||||
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(.050f), 1.8f, 36);
|
||||
result.emplace_back(area_for_diameter(.050f), 1.95f, 39);
|
||||
result.emplace_back(area_for_diameter(v8_exhaust.primary_diameter),v8_exhaust.primary_length,int(std::ceil(v8_exhaust.primary_length/.05f)));
|
||||
for(int i=0;i<4;++i) result.emplace_back(area_for_diameter(.045f),.40f,8);
|
||||
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;
|
||||
}
|
||||
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);
|
||||
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 ? .060f : .028f), turbo_enabled ? .12f : (saab ? 1.4f : .40f), saab ? (turbo_enabled ? 8 : 40) : 10);
|
||||
if (turbo_enabled) {
|
||||
result.emplace_back(area_for_diameter(saab ? .060f : .0275f), .10f, 8);
|
||||
result.emplace_back(area_for_diameter(saab ? .060f : .032f), .60f, 24);
|
||||
}
|
||||
result.emplace_back(area_for_diameter(saab ? .060f : .028f), saab ? 1.4f : .40f, saab ? 40 : 10);
|
||||
if (saab) result.back() = make_silenced_exhaust(exhaust);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
83
src/scenario_menu.cpp
Normal file
83
src/scenario_menu.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "FluidSim/TwoStrokeScenario.hpp"
|
||||
#include "FluidSim/Constants.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
@@ -10,13 +11,22 @@ TwoStrokeScenario::TwoStrokeScenario(int sample_rate, TwoStrokeConfig config)
|
||||
carburetor(config.carburetor_diameter),
|
||||
cylinder(config.bore, config.stroke, config.rod, config.geometric_compression(),
|
||||
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)),
|
||||
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),
|
||||
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.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 piston_speed_per_omega = config.stroke / pi;
|
||||
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.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_spark_gain=.04f;
|
||||
ecu.idle_spark_gain=0;
|
||||
ecu.overrun_cut_rpm = 5000;
|
||||
ecu.overrun_resume_rpm = 3500;
|
||||
ecu.overrun_fuel_cut_enabled = false;
|
||||
@@ -69,10 +79,9 @@ TwoStrokeScenario::TwoStrokeScenario(int sample_rate, TwoStrokeConfig config)
|
||||
plenum_inlet_ = &intake_plenum.create_port();
|
||||
plenum_outlet_ = &intake_plenum.create_port();
|
||||
|
||||
intake_boundary_area_ = pipes[0].area;
|
||||
boundaries_.add_orifice(intake_atmosphere.port, 0, true, intake_boundary_area_);
|
||||
boundaries_.add_open_end(3, false, 101325.0f,
|
||||
boundaries_.add_radiating_open_end(3, false, 101325.0f,
|
||||
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_outlet_, 1, true, reed_in_area_);
|
||||
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.exhaust, 3, true, exhaust_valve_area_);
|
||||
solver_.minimum_substeps = 4;
|
||||
solver_.add_component(intake_atmosphere);
|
||||
solver_.add_component(intake_plenum);
|
||||
solver_.add_component(crankcase);
|
||||
solver_.add_component(cylinder);
|
||||
@@ -105,9 +113,10 @@ AudioFrame TwoStrokeScenario::process() {
|
||||
solver_.step(dt_);
|
||||
++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);
|
||||
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); }
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#include <span>
|
||||
|
||||
using namespace fluidsim::port;
|
||||
namespace {
|
||||
@@ -14,7 +15,8 @@ void check(bool condition, const char* message) {
|
||||
}
|
||||
int main() {
|
||||
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(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");
|
||||
|
||||
37
tests/desktop_ui_tests.cpp
Normal file
37
tests/desktop_ui_tests.cpp
Normal 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";
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
#include "FluidSim/Atmosphere.hpp"
|
||||
#include "FluidSim/Crankcase.hpp"
|
||||
#include "FluidSim/Fluid.hpp"
|
||||
#include "FluidSim/Junction.hpp"
|
||||
#include "FluidSim/Solver.hpp"
|
||||
#include <cmath>
|
||||
#include <complex>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
using namespace fluidsim::port;
|
||||
namespace {
|
||||
PipeSolver selected_solver=PipeSolver::Rusanov;
|
||||
void check(bool b, const std::string &s) {
|
||||
if (!b)
|
||||
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};
|
||||
}
|
||||
void lossless(Pipe1D &p) {
|
||||
p.solver=selected_solver;
|
||||
p.damping_multiplier = 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); }
|
||||
} // namespace
|
||||
int main() {
|
||||
int main(int argc,char** argv) {
|
||||
if(argc>1) selected_solver=parse_pipe_solver(argv[1]);
|
||||
int failed = 0, total = 0;
|
||||
auto test = [&](const char *n, std::function<void()> fn) {
|
||||
++total;
|
||||
@@ -48,6 +53,163 @@ int main() {
|
||||
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", [] {
|
||||
for (float fraction : {1e-40f, 1e-30f, .5f}) {
|
||||
Pipe1D p(.001f, 1, 8);
|
||||
|
||||
@@ -34,8 +34,6 @@ int main() {
|
||||
"fresh charge heats progressively after repeated blips");
|
||||
check(inlet_temperature > 280 && inlet_temperature < 320,
|
||||
"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);
|
||||
const auto recovered = s.snapshot();
|
||||
check(recovered.rpm > 1000 && recovered.rpm < 2600, "engine did not recover idle after second blip");
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
#include "FluidSim/OutdoorExhaustReverb.hpp"
|
||||
#include "FluidSim/HelmholtzScenario.hpp"
|
||||
#include "FluidSim/PulseTestScenario.hpp"
|
||||
#include "FluidSim/ThreadLoadTracker.hpp"
|
||||
#include "FluidSim/Units.hpp"
|
||||
#include "FluidSim/Desktop/PedalControl.hpp"
|
||||
#include "FluidSim/Desktop/PipePath.hpp"
|
||||
#include "FluidSim/Desktop/PressurePalette.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
@@ -28,7 +30,64 @@ void require(bool condition, const char* message) {
|
||||
int main() {
|
||||
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;
|
||||
controller.throttle_time_constant=.08f;controller.throttle_rate=2;
|
||||
constexpr float ecu_dt = 1.0f / 44100.0f;
|
||||
controller.step(ecu_dt, 1.0f, 2000.0f);
|
||||
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_curve.reset();
|
||||
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;
|
||||
FourStrokeCylinder cylinder;
|
||||
cylinder.initialize(crank);
|
||||
@@ -264,14 +308,11 @@ int main() {
|
||||
require(idle_state.throttle > 0.0f && idle_state.pedal == 0.0f,
|
||||
"idle control must open the butterfly independently of the pedal");
|
||||
|
||||
scenario.toggle_mechanical_sound();
|
||||
for (int i = 0; i < 1000; ++i) {
|
||||
const AudioFrame frame = scenario.process();
|
||||
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";
|
||||
return 0;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
33
tests/scenario_examples.cpp
Normal file
33
tests/scenario_examples.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -53,12 +53,11 @@ int main() {
|
||||
std::to_string(maximum_rpm) + ", idle=" + std::to_string(final.rpm));
|
||||
if (!reed_open_seen || !reed_closed_seen)
|
||||
throw std::runtime_error("Reed did not open and close");
|
||||
if (final.rpm < 2000 || final.rpm > 4000)
|
||||
throw std::runtime_error("Two-stroke idle did not recover after throttle release: " +
|
||||
std::to_string(final.rpm) + " RPM, coast firings=" +
|
||||
std::to_string(one_second_coast_firings) + ", carb AFR=" +
|
||||
std::to_string(scenario.carburetor.indicated_afr()) +
|
||||
", cylinder fuel=" + std::to_string(scenario.cylinder.fuel_mass));
|
||||
if (std::abs(final.throttle-scenario.configuration.idle_slide_opening)>1e-5f)
|
||||
throw std::runtime_error("Two-stroke did not return to its configured fixed carb stop");
|
||||
if (final.rpm < 300 || final.rpm >= release_rpm)
|
||||
throw std::runtime_error("Two-stroke failed to coast down and remain running at fixed idle stop: " +
|
||||
std::to_string(final.rpm));
|
||||
if (peak >= 1)
|
||||
throw std::runtime_error("Two-stroke audio exceeds device range");
|
||||
for (const auto &pipe : scenario.pipes)
|
||||
@@ -97,7 +96,7 @@ int main() {
|
||||
if (pipe.positivity_repairs != 0)
|
||||
throw std::runtime_error("Motorcycle launch required pipe positivity repairs");
|
||||
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
|
||||
<< ", first coast-second firings=" << one_second_coast_firings << '\n';
|
||||
}
|
||||
|
||||
@@ -65,17 +65,23 @@ struct Metrics {
|
||||
}
|
||||
};
|
||||
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;
|
||||
if constexpr (std::is_same_v<Scenario, TwoStrokeScenario>) s = std::make_unique<Scenario>(44100, config);
|
||||
else s = std::make_unique<Scenario>();
|
||||
s->set_pipe_solver(solver);
|
||||
s->throttle = pedal;
|
||||
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),
|
||||
raw(prefix + "-unfiltered.wav", frames);
|
||||
Metrics m, d, r;
|
||||
int cuts = 0;
|
||||
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();
|
||||
m.add(f);
|
||||
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) {
|
||||
try {
|
||||
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;
|
||||
bool turbo_enabled = false;
|
||||
std::string turbo_name;
|
||||
PipeSolver solver=PipeSolver::Rusanov;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
std::string key = argv[i];
|
||||
if (i + 1 >= argc)
|
||||
@@ -113,11 +118,9 @@ int main(int argc, char **argv) {
|
||||
seconds = std::stof(value);
|
||||
else if (key == "--throttle")
|
||||
pedal = std::stof(value);
|
||||
else if (key == "--turbo") {
|
||||
if (value != "none" && value != "vz21" && value != "gt17") throw std::runtime_error("choose none, vz21 or gt17 turbo");
|
||||
turbo_name = value;
|
||||
turbo_enabled = value != "none";
|
||||
}
|
||||
else if (key == "--warmup") warmup=std::stof(value);
|
||||
else if (key == "--rpm") fixed_rpm=std::stof(value);
|
||||
else if (key == "--solver") solver=parse_pipe_solver(value);
|
||||
else if (key == "--exhaust") {
|
||||
if (value == "racing") config.exhaust = TwoStrokeExhaust::Racing;
|
||||
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 ||
|
||||
pedal > 1)
|
||||
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;
|
||||
if (scene == "dry")
|
||||
selected = OutdoorScene::Dry;
|
||||
@@ -140,27 +146,21 @@ int main(int argc, char **argv) {
|
||||
selected = OutdoorScene::Wall;
|
||||
else if (scene == "woodland")
|
||||
selected = OutdoorScene::Woodland;
|
||||
else if (scene == "tunnel")
|
||||
selected = OutdoorScene::Tunnel;
|
||||
else
|
||||
throw std::runtime_error("invalid scene");
|
||||
if (!std::filesystem::path(prefix).parent_path().empty())
|
||||
std::filesystem::create_directories(std::filesystem::path(prefix).parent_path());
|
||||
const int frames = int(std::lround(seconds * 44100));
|
||||
if (turbo_name.empty() && scenario == "b205") turbo_enabled = true;
|
||||
if (!turbo_name.empty() && turbo_name!="none" &&
|
||||
!((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);
|
||||
if (scenario == "twin")
|
||||
capture<FourStrokeTwinScenario>(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup);
|
||||
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")
|
||||
capture<TwoStrokeScenario>(prefix, frames, pedal, selected, config);
|
||||
capture<TwoStrokeScenario>(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup);
|
||||
else if (scenario == "v8")
|
||||
capture<AmericanV8Scenario>(prefix, frames, pedal, selected, config);
|
||||
capture<AmericanV8Scenario>(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup);
|
||||
else
|
||||
throw std::runtime_error("choose twin, two-stroke, b205 or v8");
|
||||
} catch (const std::exception &e) {
|
||||
|
||||
@@ -2,12 +2,31 @@
|
||||
#include "FluidSim/TwoStrokeScenario.hpp"
|
||||
#include "FluidSim/HelmholtzScenario.hpp"
|
||||
#include "FluidSim/PulseTestScenario.hpp"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <numeric>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
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) {
|
||||
double checksum = 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;
|
||||
if constexpr (requires { s.pipes; }) {
|
||||
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& cell : pipe.cells)
|
||||
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
|
||||
<< " limiting_pipe=" << limiting_pipe << " cfl_us=" << stable_dt*1e6f << '\n';
|
||||
previous_substeps = state.solver_substeps;
|
||||
unsigned long long repairs = 0;
|
||||
for (const auto& pipe : s.pipes) repairs += pipe.positivity_repairs;
|
||||
unsigned long long repairs = 0, fallbacks = 0;
|
||||
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
|
||||
<< " peak=" << peak << " pipe_repairs=" << repairs << '\n';
|
||||
<< " peak=" << peak << " hllc_fallbacks=" << fallbacks << " pipe_repairs=" << repairs << '\n';
|
||||
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 std::string mode = argc > 2 ? argv[2] : "pedal";
|
||||
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) {
|
||||
return mode == "idle" ? 0.0f : mode == "coast" ? (second >= 1 && second < 5 ? .7f : 0.f) :
|
||||
mode == "transient" && (second == 0 || second >= seconds-2) ? 0.0f : .7f;
|
||||
};
|
||||
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;
|
||||
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)=="turbo" || std::string(name)=="b205-gt17");
|
||||
std::string(name)=="v8" ? FourStrokePreset::AmericanV8 : std::string(name)=="b205" ? FourStrokePreset::SaabB205 : FourStrokePreset::Twin);
|
||||
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;
|
||||
run(name, *s, seconds, [&](int 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}) {
|
||||
@@ -82,6 +112,7 @@ int main(int argc, char** argv) {
|
||||
TwoStrokeConfig config;
|
||||
config.exhaust = racing ? TwoStrokeExhaust::Racing : TwoStrokeExhaust::Straight;
|
||||
TwoStrokeScenario s(44100, config);
|
||||
s.set_pipe_solver(selected_solver);
|
||||
s.set_throttle(.7f);
|
||||
run(racing ? "two-stroke-racing" : "two-stroke-straight", s, seconds, [&](int second) {
|
||||
s.set_throttle(pedal(second));
|
||||
|
||||
@@ -3,13 +3,31 @@
|
||||
#include "FluidSim/TwoStrokeScenario.hpp"
|
||||
#include "FluidSim/HelmholtzScenario.hpp"
|
||||
#include "FluidSim/PulseTestScenario.hpp"
|
||||
#include <algorithm>
|
||||
#include <barrier>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <numeric>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
using namespace fluidsim::port;
|
||||
int main() {
|
||||
for (std::string name : {"twin", "turbo", "b205", "two-stroke-racing", "two-stroke-straight", "helmholtz", "pulse", "large"}) {
|
||||
static void set_uniform_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 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;
|
||||
if (name == "large") {
|
||||
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;
|
||||
TwoStrokeScenario s(44100, config); geometry = s.pipes;
|
||||
} 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;
|
||||
}
|
||||
if (name == "v8" && uniform_cells_per_meter > 0)
|
||||
set_uniform_resolution(geometry, uniform_cells_per_meter);
|
||||
const int steps = 4000;
|
||||
for (int workers : {1, 2, 4}) {
|
||||
auto p = geometry;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
sound at 44.1 kHz. No synthetic bang, saturation, or combustion event is added
|
||||
by the audio renderer. Acoustic processing does not feed energy into the solver.
|
||||
The desktop and capture tool produce sound at 44.1 kHz. Intake and exhaust
|
||||
ambient-density-scaled volume-flow derivatives drive the compact-monopole source, with static
|
||||
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.
|
||||
Each intake/exhaust mass-flow derivative drives a compact monopole. Separate
|
||||
left/right paths use inverse distance and fractional-sample delays at 343 m/s.
|
||||
The listener and sources are static. Default listener position is (1.6, 1, 2) m;
|
||||
intake is (-0.2, .7, 0), exhaust (.55, .5, 0), ground y=0, wall x=-1.5.
|
||||
Ground and wall reflection coefficients are .35 and .65. These are configurable
|
||||
assumptions rather than measurements of a particular location.
|
||||
`OutdoorAudio` renders direct sound, image-source ground and wall reflections,
|
||||
and six approximate woodland scatter paths. The tunnel profile adds image
|
||||
paths for a nominally 6 m wide, 4 m high hard-surface enclosure. Its late tail
|
||||
uses the live eight-line feedback-delay network in `OutdoorExhaustReverb`. The
|
||||
five scene profiles set different reflection paths, tail levels, and
|
||||
frequency-dependent decay times. Scene changes smooth over 20 ms. These
|
||||
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;
|
||||
it is linear and does not create afterfire. Early ground/wall reflections use
|
||||
image paths. Woodland uses six approximate scatterers. A stable four-line
|
||||
feedback-delay network supplies an empirical diffuse tail; it does not solve
|
||||
outdoor wave propagation or vegetation scattering. Scene changes smooth over
|
||||
20 ms. The old `OutdoorExhaustReverb` remains for legacy tests but is inactive
|
||||
in both engine scenarios.
|
||||
The direct source remains prominent, so scene differences are clearest in the
|
||||
reflections and decay after transients. Ground effects depend on surface,
|
||||
geometry, and frequency; the current coefficients are illustrative settings,
|
||||
not measured material data. The feedback-delay network gives a diffuse late
|
||||
tail, but does not simulate outdoor wave propagation or vegetation in detail.
|
||||
|
||||
Pipe-end radiation impedance, calibrated directivity, alias-free acquisition,
|
||||
and exhaust afterfire chemistry remain unresolved. PCM output is not calibrated
|
||||
speaker SPL. The monitoring filter cannot recover information lost to aliasing.
|
||||
Calibrated directivity, alias-free acquisition,
|
||||
and exhaust afterfire chemistry remain unresolved. Filtering cannot restore
|
||||
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
|
||||
and rendered signals come from the same simulation run. Measurements are
|
||||
before PCM clipping; the raw signal includes direct spatial propagation.
|
||||
|
||||
| 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:
|
||||
Run the desktop with `--scenario v8 --scene tunnel` and press **R** to cycle
|
||||
through Dry, Field, Wall, Woodland, and Tunnel. Use 1x playback when judging
|
||||
the audio.
|
||||
To write comparable WAVs for one scene, build and run:
|
||||
|
||||
```sh
|
||||
cmake --build build-audit --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-audit/fluidsim_audio_capture --scenario two-stroke --out build/audio/two-stroke-wall --seconds 4 --throttle 1 --scene wall
|
||||
cmake --build build-desktop --target fluidsim_audio_capture -j 4
|
||||
./build-desktop/fluidsim_audio_capture --scenario v8 --scene field --seconds 1 --throttle .7 --out /tmp/v8-field
|
||||
```
|
||||
|
||||
Each produces `-rendered.wav`, `-dry.wav` (filtered direct paths) and
|
||||
`-unfiltered.wav` for comparison. Capture files are build artifacts.
|
||||
|
||||
## 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.
|
||||
The capture tool writes rendered, dry-filtered, and unfiltered WAV files. The
|
||||
dry and unfiltered outputs omit the outdoor reflections and reverb, which helps
|
||||
separate source filtering from scene processing.
|
||||
|
||||
## Reference basis
|
||||
|
||||
- [Image source geometry](https://www.acs.psu.edu/noahparker/acs/img-src/img-src.html)
|
||||
- [Feedback delay networks](https://msp.ucsd.edu/techniques/latest/book-html/node111.html)
|
||||
- [Beam theory](https://mechanics.ju.se/SolidMechanics/BeamTheory.html), including uniform-load cantilever deflection used for reed compliance.
|
||||
- [FHWA ground-effect guidance](https://www.fhwa.dot.gov/Environment/noise/noise_barriers/design_construction/design/design03.cfm)
|
||||
- [Feedback delay networks in artificial reverberation](https://theses.eurasip.org/document/feedback-delay-networks-in-artificial-reverberation-and-reverberation-enhancement/)
|
||||
|
||||
The later YZ125 baseline changes these engine calibrations and measurements; see
|
||||
[YZ125 baseline verification](YZ125_BASELINE.md) for current results.
|
||||
## Pipe mouth boundary
|
||||
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
thresholds. These are scenario calibrations, not a factory Trionic map.
|
||||
|
||||
There is no compressor, turbine, intercooler, wastegate, catalyst or muffler in
|
||||
this first version. Intake boundary pressure is ambient. Consequently factory
|
||||
turbo torque/power and factory exhaust sound are not reproduced. Thermal,
|
||||
This naturally aspirated first version has ambient intake boundary pressure and
|
||||
does not reproduce factory boosted torque or exhaust sound. Thermal,
|
||||
combustion and single-zone mixing limitations from the fluid audit still apply.
|
||||
|
||||
## Verification
|
||||
|
||||
@@ -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
|
||||
39% of sampled executable time to pipe advancement, 38% to orifice boundary
|
||||
solves, and 13% to Solver::step (including inlined preparation). Turbo mechanics
|
||||
were under 1% and outdoor emitter rendering about 1%. These are sampled gprof
|
||||
results, not precise attribution of time inside dynamically linked math functions.
|
||||
solves, and 13% to Solver::step (including inlined preparation). Outdoor emitter
|
||||
rendering was about 1%. These are sampled gprof results, not precise attribution
|
||||
of time inside dynamically linked math functions.
|
||||
|
||||
`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
|
||||
@@ -52,7 +52,6 @@ Mean wall seconds per simulated second, six-second sustained-load runs:
|
||||
| Scenario | Reference | Optimized | Time reduction | SIMD disabled |
|
||||
|---|---:|---:|---:|---:|
|
||||
| Twin | .528 | .405 | 23% | .423 |
|
||||
| Turbo twin | 1.368 | .649 | 53% | .684 |
|
||||
| B205 | .884 | .689 | 22% | .737 |
|
||||
| Two-stroke racing | .561 | .470 | 16% | .535 |
|
||||
| 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 |
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
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;
|
||||
@@ -79,8 +78,9 @@ performance for every user configuration or arbitrarily long simulation.
|
||||
|
||||
## Reproduction
|
||||
|
||||
Final validation: all nine CTest suites passed both with SIMD enabled (28.50 s)
|
||||
and disabled (30.86 s). The fluid validation executable includes 30 checks,
|
||||
Final validation at the time of this audit: all nine then-registered CTest
|
||||
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
|
||||
rebuilt successfully. This audit verifies desktop compilation and headless audio
|
||||
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
|
||||
```
|
||||
|
||||
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.
|
||||
`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;
|
||||
`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
|
||||
stderr. Inspection happens outside the timed sample-processing loop.
|
||||
|
||||
|
||||
@@ -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.
|
||||
124
verification/SOLVER_COMPARISON.md
Normal file
124
verification/SOLVER_COMPARISON.md
Normal 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)
|
||||
@@ -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.
|
||||
77
verification/V8_PIPE_PERFORMANCE.md
Normal file
77
verification/V8_PIPE_PERFORMANCE.md
Normal 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.
|
||||
828
verification/solver-comparison/diagnostics.log
Normal file
828
verification/solver-comparison/diagnostics.log
Normal 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
|
||||
|
||||
217
verification/solver-comparison/final.csv
Normal file
217
verification/solver-comparison/final.csv
Normal 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
|
||||
|
217
verification/solver-comparison/initial.csv
Normal file
217
verification/solver-comparison/initial.csv
Normal 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
|
||||
|
218
verification/solver-comparison/summary.json
Normal file
218
verification/solver-comparison/summary.json
Normal 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
|
||||
}
|
||||
]
|
||||
138
verification/solver-comparison/validation.log
Normal file
138
verification/solver-comparison/validation.log
Normal 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
|
||||
Reference in New Issue
Block a user