From 5607dcf2b58d5ebd6a9977370ac5ff52fb5c96ee Mon Sep 17 00:00:00 2001 From: grillkol Date: Fri, 23 Oct 2026 18:29:51 +0200 Subject: [PATCH] all of the changes --- CARB_PROGRESS.md | 63 -- CMakeLists.txt | 37 +- README.md | 260 ++++-- include/FluidSim/AudioProcessing.hpp | 34 +- include/FluidSim/Desktop/DesktopUi.hpp | 37 + include/FluidSim/Desktop/DynoRenderer.hpp | 6 + include/FluidSim/Desktop/EngineRenderer3D.hpp | 19 + include/FluidSim/Desktop/EngineSchematic.hpp | 6 + include/FluidSim/Desktop/OrbitCamera.hpp | 20 + include/FluidSim/Desktop/PedalControl.hpp | 15 + include/FluidSim/Desktop/PipePath.hpp | 46 + include/FluidSim/Desktop/PressurePalette.hpp | 12 + include/FluidSim/Desktop/ScenarioMenu.hpp | 7 + include/FluidSim/DesktopScenario.hpp | 18 +- include/FluidSim/Ecu.hpp | 14 +- include/FluidSim/Fluid.hpp | 224 ++++- include/FluidSim/Junction.hpp | 34 + include/FluidSim/MechanicalSoundProcessor.hpp | 49 -- include/FluidSim/MotorcycleTest.hpp | 4 + include/FluidSim/OpenEndRadiation.hpp | 27 + include/FluidSim/OutdoorAudio.hpp | 27 +- include/FluidSim/OutdoorExhaustReverb.hpp | 4 +- include/FluidSim/PipeSolver.hpp | 20 + include/FluidSim/Scenario.hpp | 96 +- include/FluidSim/ScenarioCatalog.hpp | 83 ++ include/FluidSim/Silencer.hpp | 54 ++ include/FluidSim/ThreadLoadTracker.hpp | 24 - include/FluidSim/Turbocharger.hpp | 81 -- include/FluidSim/TwoStrokeConfig.hpp | 3 + include/FluidSim/TwoStrokeScenario.hpp | 5 +- src/audio_processing.cpp | 64 -- src/desktop_main.cpp | 577 ++---------- src/desktop_ui.cpp | 119 +++ src/dyno_renderer.cpp | 128 +++ src/ecu.cpp | 28 +- src/engine_renderer_3d.cpp | 254 ++++++ src/engine_schematic.cpp | 227 +++++ src/helmholtz_scenario.cpp | 2 +- src/main.cpp | 27 +- src/mechanical_sound_processor.cpp | 132 --- src/outdoor_audio.cpp | 109 ++- src/outdoor_exhaust_reverb.cpp | 30 +- src/pulse_test_scenario.cpp | 2 +- src/scenario.cpp | 203 +++-- src/scenario_menu.cpp | 83 ++ src/turbocharger.cpp | 200 ----- src/two_stroke_scenario.cpp | 29 +- tests/b205_regression.cpp | 4 +- tests/desktop_ui_tests.cpp | 37 + tests/fluid_validation.cpp | 164 +++- tests/intake_regression.cpp | 2 - tests/physics_tests.cpp | 81 +- tests/saab_turbo_regression.cpp | 46 - tests/scenario_examples.cpp | 33 + tests/turbo_physics.cpp | 115 --- tests/turbo_twin_regression.cpp | 43 - tests/two_stroke_regression.cpp | 13 +- tools/audio_capture.cpp | 44 +- tools/scenario_benchmark.cpp | 45 +- tools/thread_benchmark.cpp | 26 +- tools/turbo_benchmark.cpp | 48 - verification/AUDIO_RENDERING.md | 179 ++-- verification/B205_BASELINE.md | 5 +- verification/OPTIMIZATION.md | 20 +- verification/SAAB_GT17.md | 46 - verification/SOLVER_COMPARISON.md | 124 +++ verification/TURBO_VZ21.md | 111 --- verification/V8_PIPE_PERFORMANCE.md | 77 ++ .../solver-comparison/diagnostics.log | 828 ++++++++++++++++++ verification/solver-comparison/final.csv | 217 +++++ verification/solver-comparison/initial.csv | 217 +++++ verification/solver-comparison/summary.json | 218 +++++ verification/solver-comparison/validation.log | 138 +++ 73 files changed, 4365 insertions(+), 2059 deletions(-) delete mode 100644 CARB_PROGRESS.md create mode 100644 include/FluidSim/Desktop/DesktopUi.hpp create mode 100644 include/FluidSim/Desktop/DynoRenderer.hpp create mode 100644 include/FluidSim/Desktop/EngineRenderer3D.hpp create mode 100644 include/FluidSim/Desktop/EngineSchematic.hpp create mode 100644 include/FluidSim/Desktop/OrbitCamera.hpp create mode 100644 include/FluidSim/Desktop/PedalControl.hpp create mode 100644 include/FluidSim/Desktop/PipePath.hpp create mode 100644 include/FluidSim/Desktop/PressurePalette.hpp create mode 100644 include/FluidSim/Desktop/ScenarioMenu.hpp create mode 100644 include/FluidSim/Junction.hpp delete mode 100644 include/FluidSim/MechanicalSoundProcessor.hpp create mode 100644 include/FluidSim/OpenEndRadiation.hpp create mode 100644 include/FluidSim/PipeSolver.hpp create mode 100644 include/FluidSim/ScenarioCatalog.hpp create mode 100644 include/FluidSim/Silencer.hpp delete mode 100644 include/FluidSim/ThreadLoadTracker.hpp delete mode 100644 include/FluidSim/Turbocharger.hpp create mode 100644 src/desktop_ui.cpp create mode 100644 src/dyno_renderer.cpp create mode 100644 src/engine_renderer_3d.cpp create mode 100644 src/engine_schematic.cpp delete mode 100644 src/mechanical_sound_processor.cpp create mode 100644 src/scenario_menu.cpp delete mode 100644 src/turbocharger.cpp create mode 100644 tests/desktop_ui_tests.cpp delete mode 100644 tests/saab_turbo_regression.cpp create mode 100644 tests/scenario_examples.cpp delete mode 100644 tests/turbo_physics.cpp delete mode 100644 tests/turbo_twin_regression.cpp delete mode 100644 tools/turbo_benchmark.cpp delete mode 100644 verification/SAAB_GT17.md create mode 100644 verification/SOLVER_COMPARISON.md delete mode 100644 verification/TURBO_VZ21.md create mode 100644 verification/V8_PIPE_PERFORMANCE.md create mode 100644 verification/solver-comparison/diagnostics.log create mode 100644 verification/solver-comparison/final.csv create mode 100644 verification/solver-comparison/initial.csv create mode 100644 verification/solver-comparison/summary.json create mode 100644 verification/solver-comparison/validation.log diff --git a/CARB_PROGRESS.md b/CARB_PROGRESS.md deleted file mode 100644 index 80b3425..0000000 --- a/CARB_PROGRESS.md +++ /dev/null @@ -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` diff --git a/CMakeLists.txt b/CMakeLists.txt index cdc3ba7..5f76de5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/README.md b/README.md index 633cffa..c83b7e5 100644 --- a/README.md +++ b/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`). diff --git a/include/FluidSim/AudioProcessing.hpp b/include/FluidSim/AudioProcessing.hpp index 1ca507d..d40ca86 100644 --- a/include/FluidSim/AudioProcessing.hpp +++ b/include/FluidSim/AudioProcessing.hpp @@ -1,13 +1,11 @@ #pragma once -#include - 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 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& buffer, std::size_t offset); -}; - } // namespace fluidsim::port diff --git a/include/FluidSim/Desktop/DesktopUi.hpp b/include/FluidSim/Desktop/DesktopUi.hpp new file mode 100644 index 0000000..c9d8de2 --- /dev/null +++ b/include/FluidSim/Desktop/DesktopUi.hpp @@ -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 +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}; +}; +} diff --git a/include/FluidSim/Desktop/DynoRenderer.hpp b/include/FluidSim/Desktop/DynoRenderer.hpp new file mode 100644 index 0000000..1c9b7f1 --- /dev/null +++ b/include/FluidSim/Desktop/DynoRenderer.hpp @@ -0,0 +1,6 @@ +#pragma once +#include "FluidSim/DynoCurve.hpp" +#include +namespace fluidsim::port::desktop { +void draw_dyno_curve(sf::RenderWindow&,const sf::Font*,const DynoCurve&,float,float,float,float,float,float); +} diff --git a/include/FluidSim/Desktop/EngineRenderer3D.hpp b/include/FluidSim/Desktop/EngineRenderer3D.hpp new file mode 100644 index 0000000..112375f --- /dev/null +++ b/include/FluidSim/Desktop/EngineRenderer3D.hpp @@ -0,0 +1,19 @@ +#pragma once +#include "FluidSim/Scenario.hpp" +#include "FluidSim/Desktop/OrbitCamera.hpp" +#include +#include +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_; +}; +} diff --git a/include/FluidSim/Desktop/EngineSchematic.hpp b/include/FluidSim/Desktop/EngineSchematic.hpp new file mode 100644 index 0000000..28f591a --- /dev/null +++ b/include/FluidSim/Desktop/EngineSchematic.hpp @@ -0,0 +1,6 @@ +#pragma once +#include "FluidSim/Scenario.hpp" +#include +namespace fluidsim::port::desktop { +void draw_schematic(sf::RenderWindow&, const sf::Font*, const ScenarioSnapshot&); +} diff --git a/include/FluidSim/Desktop/OrbitCamera.hpp b/include/FluidSim/Desktop/OrbitCamera.hpp new file mode 100644 index 0000000..37c2334 --- /dev/null +++ b/include/FluidSim/Desktop/OrbitCamera.hpp @@ -0,0 +1,20 @@ +#pragma once +#include "FluidSim/AudioProcessing.hpp" +#include +#include +#include +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);} +}; +} diff --git a/include/FluidSim/Desktop/PedalControl.hpp b/include/FluidSim/Desktop/PedalControl.hpp new file mode 100644 index 0000000..de94778 --- /dev/null +++ b/include/FluidSim/Desktop/PedalControl.hpp @@ -0,0 +1,15 @@ +#pragma once +#include +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_{}; +}; +} diff --git a/include/FluidSim/Desktop/PipePath.hpp b/include/FluidSim/Desktop/PipePath.hpp new file mode 100644 index 0000000..0db5349 --- /dev/null +++ b/include/FluidSim/Desktop/PipePath.hpp @@ -0,0 +1,46 @@ +#pragma once +#include "FluidSim/AudioProcessing.hpp" +#include +#include +#include +#include +#include +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 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;ianchors_[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(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 anchors_; +}; +} diff --git a/include/FluidSim/Desktop/PressurePalette.hpp b/include/FluidSim/Desktop/PressurePalette.hpp new file mode 100644 index 0000000..3e0d9e3 --- /dev/null +++ b/include/FluidSim/Desktop/PressurePalette.hpp @@ -0,0 +1,12 @@ +#pragma once +#include "FluidSim/AudioProcessing.hpp" +#include +#include +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}; +} +} diff --git a/include/FluidSim/Desktop/ScenarioMenu.hpp b/include/FluidSim/Desktop/ScenarioMenu.hpp new file mode 100644 index 0000000..d4bd9d4 --- /dev/null +++ b/include/FluidSim/Desktop/ScenarioMenu.hpp @@ -0,0 +1,7 @@ +#pragma once +#include "FluidSim/ScenarioCatalog.hpp" +#include +#include +namespace fluidsim::port::desktop { +std::optional choose_scenario(sf::RenderWindow&,const sf::Font*,const std::vector&); +} diff --git a/include/FluidSim/DesktopScenario.hpp b/include/FluidSim/DesktopScenario.hpp index 0c3e835..1303947 100644 --- a/include/FluidSim/DesktopScenario.hpp +++ b/include/FluidSim/DesktopScenario.hpp @@ -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(44100, config); else twin_ = std::make_unique(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 twin_; std::unique_ptr two_; mutable std::mutex mutex_; diff --git a/include/FluidSim/Ecu.hpp b/include/FluidSim/Ecu.hpp index 04661a1..ee15dfb 100644 --- a/include/FluidSim/Ecu.hpp +++ b/include/FluidSim/Ecu.hpp @@ -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 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}; diff --git a/include/FluidSim/Fluid.hpp b/include/FluidSim/Fluid.hpp index ad48b95..3fc8b64 100644 --- a/include/FluidSim/Fluid.hpp +++ b/include/FluidSim/Fluid.hpp @@ -1,6 +1,8 @@ #pragma once #include "FluidSim/Engine.hpp" +#include "FluidSim/OpenEndRadiation.hpp" +#include "FluidSim/PipeSolver.hpp" #include #include #include @@ -165,10 +167,14 @@ struct GasState { class Pipe1D { public: + std::vector linear_resistance, quadratic_resistance; // Integrated face fluxes: kg/s, N, W, kg/s of fresh air and fuel, positive along +x. struct Flux { float m{}, p{}, e{}, y{}, fuel{}; }; + 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 cell_areas, cell_lengths; @@ -180,7 +186,7 @@ public: Pipe1D(std::vector areas, std::vector 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 + 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) || !(sl0) || !(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(a,b,ca,cb,fallback); + if(solver==PipeSolver::Hlle) return flux_kernel(a,b,ca,cb,fallback); + return flux_kernel(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=[&]() { + for (std::size_t i = 1; i < n; ++i) { + bool fallback=false; + faces_[i] = scaled(flux_kernel(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()(); + else fill_faces.template operator()(); + } 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 states_; std::vector waves_; std::vector faces_; + std::vector roe_states_; Flux left_flux_{}, right_flux_{}; bool left_override_{}, right_override_{}, prepared_{}; static std::vector 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(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(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 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 ends; + std::vector states; + }; + std::vector 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= 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}); diff --git a/include/FluidSim/Junction.hpp b/include/FluidSim/Junction.hpp new file mode 100644 index 0000000..1985ce5 --- /dev/null +++ b/include/FluidSim/Junction.hpp @@ -0,0 +1,34 @@ +#pragma once +#include "FluidSim/Fluid.hpp" +#include + +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& pipes, + std::span 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=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 areas_; // stable addresses for boundary area references + bool connected_{}; +}; +} diff --git a/include/FluidSim/MechanicalSoundProcessor.hpp b/include/FluidSim/MechanicalSoundProcessor.hpp deleted file mode 100644 index 4c48027..0000000 --- a/include/FluidSim/MechanicalSoundProcessor.hpp +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -#include "FluidSim/Engine.hpp" -#include - -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 unit_{-1.0f, 1.0f}; - - float random_sample(); - static bool crossed_fraction(float previous, float current, float target); -}; - -} // namespace fluidsim::port diff --git a/include/FluidSim/MotorcycleTest.hpp b/include/FluidSim/MotorcycleTest.hpp index b55601b..9bc2a0c 100644 --- a/include/FluidSim/MotorcycleTest.hpp +++ b/include/FluidSim/MotorcycleTest.hpp @@ -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); } diff --git a/include/FluidSim/OpenEndRadiation.hpp b/include/FluidSim/OpenEndRadiation.hpp new file mode 100644 index 0000000..36ea1f4 --- /dev/null +++ b/include/FluidSim/OpenEndRadiation.hpp @@ -0,0 +1,27 @@ +#pragma once +#include + +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 diff --git a/include/FluidSim/OutdoorAudio.hpp b/include/FluidSim/OutdoorAudio.hpp index 1cf42bb..2408ce4 100644 --- a/include/FluidSim/OutdoorAudio.hpp +++ b/include/FluidSim/OutdoorAudio.hpp @@ -1,12 +1,14 @@ #pragma once #include "FluidSim/AudioProcessing.hpp" +#include "FluidSim/OutdoorExhaustReverb.hpp" #include +#include #include #include 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 filtered, raw; @@ -53,22 +57,17 @@ private: float previous_flow{}; LowPass filter; }; - struct TailLine { - std::vector buffer; - std::size_t position{}; - float gain{}, state{}; - }; int rate_{}; OutdoorAudioConfig config_; - OutdoorScene scene_{OutdoorScene::Wall}; + std::atomic requested_scene_{OutdoorScene::Wall}; + OutdoorScene active_scene_{OutdoorScene::Wall}; bool monitor_filter_{true}; std::array emitters_; - std::array tail_; - std::array weights_{1, 0, 0, 0}; - float scene_alpha_{}, tail_alpha_{}, tail_mix_{}; + OutdoorExhaustReverb late_reverb_; + std::array 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 diff --git a/include/FluidSim/OutdoorExhaustReverb.hpp b/include/FluidSim/OutdoorExhaustReverb.hpp index 7289d40..15851ee 100644 --- a/include/FluidSim/OutdoorExhaustReverb.hpp +++ b/include/FluidSim/OutdoorExhaustReverb.hpp @@ -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 fdn_; std::vector low_filters_, mid_filters_; std::vector 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); }; diff --git a/include/FluidSim/PipeSolver.hpp b/include/FluidSim/PipeSolver.hpp new file mode 100644 index 0000000..5fd0289 --- /dev/null +++ b/include/FluidSim/PipeSolver.hpp @@ -0,0 +1,20 @@ +#pragma once +#include +#include +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 diff --git a/include/FluidSim/Scenario.hpp b/include/FluidSim/Scenario.hpp index 939002a..29ee749 100644 --- a/include/FluidSim/Scenario.hpp +++ b/include/FluidSim/Scenario.hpp @@ -1,16 +1,17 @@ #pragma once +#include "FluidSim/Silencer.hpp" +#include "FluidSim/Carburetor.hpp" +#include +#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 #include #include @@ -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 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 cylinder_pressures{}, piston_fractions{}, intake_areas{}, exhaust_areas{}; - std::uint64_t solver_substeps{}, boundary_iterations{}; + std::array cylinder_angles{}; + std::array 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 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, 22> pipe_states; - std::array, 22> pipe_areas, pipe_lengths; + bool rev_limit1{}, rev_limit2{}; + std::array, 32> pipe_states; + std::array, 32> pipe_areas, pipe_lengths; void capture_pipes(const std::vector& 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 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 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 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 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 intake_valve_areas_{}, exhaust_valve_areas_{}; + float throttle_area_{}, runner_area_{}, header_area_{}, tail_area_{}; + std::array intake_valve_areas_{}, exhaust_valve_areas_{}; float dt_{}; BoundarySystem boundaries_; Solver solver_; OutdoorAudio audio_; - MechanicalSoundProcessor mechanical_sound_; mutable std::mutex mutex_; + std::vector> exhaust_junctions_; + std::unique_ptr carb_; + Port* throttle_port_{}; static float area_for_diameter(float diameter); - static std::vector create_pipes(FourStrokePreset preset, bool turbo_enabled); + static std::vector 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 diff --git a/include/FluidSim/ScenarioCatalog.hpp b/include/FluidSim/ScenarioCatalog.hpp new file mode 100644 index 0000000..8b4c018 --- /dev/null +++ b/include/FluidSim/ScenarioCatalog.hpp @@ -0,0 +1,83 @@ +#pragma once +#include "FluidSim/Scenario.hpp" +#include "FluidSim/TwoStrokeConfig.hpp" +#include +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 scenario_examples() { + std::vector 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; +} +} diff --git a/include/FluidSim/Silencer.hpp b/include/FluidSim/Silencer.hpp new file mode 100644 index 0000000..691d210 --- /dev/null +++ b/include/FluidSim/Silencer.hpp @@ -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 sections; + std::vector 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 - -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 load_percent_{}; - std::atomic last_busy_ms_{}; -}; - -} // namespace fluidsim::port diff --git a/include/FluidSim/Turbocharger.hpp b/include/FluidSim/Turbocharger.hpp deleted file mode 100644 index 0912dfd..0000000 --- a/include/FluidSim/Turbocharger.hpp +++ /dev/null @@ -1,81 +0,0 @@ -#pragma once - -#include "FluidSim/Fluid.hpp" -#include -#include - -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 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& 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 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 internal_; - std::array 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 diff --git a/include/FluidSim/TwoStrokeConfig.hpp b/include/FluidSim/TwoStrokeConfig.hpp index f6e5a38..3d2f59b 100644 --- a/include/FluidSim/TwoStrokeConfig.hpp +++ b/include/FluidSim/TwoStrokeConfig.hpp @@ -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. diff --git a/include/FluidSim/TwoStrokeScenario.hpp b/include/FluidSim/TwoStrokeScenario.hpp index 677f55e..24a018b 100644 --- a/include/FluidSim/TwoStrokeScenario.hpp +++ b/include/FluidSim/TwoStrokeScenario.hpp @@ -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 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_{}; diff --git a/src/audio_processing.cpp b/src/audio_processing.cpp index de16d04..9e490c1 100644 --- a/src/audio_processing.cpp +++ b/src/audio_processing.cpp @@ -2,7 +2,6 @@ #include "FluidSim/Engine.hpp" #include -#include 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(std::max(sample_rate, 1))) { - const std::size_t max_delay = static_cast(std::max(1.0f, - 100.0f / std::max(speed_of_sound, 1.0f) * static_cast(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(left_distance / speed_of_sound / dt_)); - right_offset_ = std::min(right_delay_.size() - 1, static_cast(right_distance / speed_of_sound / dt_)); - - const auto delay_sample = [this, filtered](std::vector& 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(sample_rate_); - const float alpha = 1.0f - std::exp(-angular); - state += alpha * (input - state); - return state; -} - } // namespace fluidsim::port diff --git a/src/desktop_main.cpp b/src/desktop_main.cpp index a3ebd0d..616f149 100644 --- a/src/desktop_main.cpp +++ b/src/desktop_main.cpp @@ -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 #include @@ -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(255.0f * f), static_cast(255.0f * (1.0f - f))}; - } - const float f = std::min((bar - 1.0f) / 9.0f, 1.0f); - return {static_cast(255.0f * f), static_cast(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(255.0f * f), static_cast(255.0f * (1.0f - f))}; - } - const float f = (t - 300.0f) / 1700.0f; - return {static_cast(255.0f * f), static_cast(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 choose_scenario(sf::RenderWindow& window, const sf::Font* font) { - const std::array 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 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 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()) return std::nullopt; - if (const auto* key = event->getIf()) { - 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(selected); - default: break; - } - } - if (const auto* move = event->getIf()) { - 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(); - 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(i); - if (start_button.contains(point)) return static_cast(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(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(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(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(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(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(index) / 9.0f; - const float x = plot_x + static_cast(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(index - 1) + 0.5f) * DynoCurve::rpm_bin_size; - const float rpm1 = (static_cast(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(state.rpm)) + - " Power " + std::to_string(state.average_power / 1000.0f) + " kW" + - " Vehicle kinetic energy " + std::to_string(static_cast(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< 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;ikind;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<((static_cast(selected_scene)+1)%4); + selected_scene=static_cast((static_cast(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()) { + 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(); 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(); diff --git a/src/desktop_ui.cpp b/src/desktop_ui.cpp new file mode 100644 index 0000000..3444ce2 --- /dev/null +++ b/src/desktop_ui.cpp @@ -0,0 +1,119 @@ +#include "FluidSim/Desktop/DesktopUi.hpp" +#include "FluidSim/Desktop/EngineSchematic.hpp" +#include "FluidSim/Desktop/DynoRenderer.hpp" +#include +#include +#include +#include +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<()) { + 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() || event.is()) { + if(pedal_drag_ || event.is())pedal_.set(0); + dragging_=false;pedal_drag_=false; + } + if(const auto* p=event.getIf()) { + 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();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()) { + 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;i0: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(); +} +} diff --git a/src/dyno_renderer.cpp b/src/dyno_renderer.cpp new file mode 100644 index 0000000..6f2248f --- /dev/null +++ b/src/dyno_renderer.cpp @@ -0,0 +1,128 @@ +#include "FluidSim/Desktop/DynoRenderer.hpp" +#include "FluidSim/Constants.hpp" +#include +#include +#include +#include +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(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(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(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(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(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(index) / 9.0f; + const float x = plot_x + static_cast(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(index - 1) + 0.5f) * DynoCurve::rpm_bin_size; + const float rpm1 = (static_cast(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); + } +} + +} diff --git a/src/ecu.cpp b/src/ecu.cpp index 2731319..6f6960e 100644 --- a/src/ecu.cpp +++ b/src/ecu.cpp @@ -3,27 +3,6 @@ #include 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 { diff --git a/src/engine_renderer_3d.cpp b/src/engine_renderer_3d.cpp new file mode 100644 index 0000000..41fe531 --- /dev/null +++ b/src/engine_renderer_3d.cpp @@ -0,0 +1,254 @@ +#include "FluidSim/Desktop/EngineRenderer3D.hpp" +#include "FluidSim/Desktop/PressurePalette.hpp" +#include "FluidSim/Desktop/PipePath.hpp" +#include +#include +#include +#include +#include +#include +namespace fluidsim::port::desktop { +namespace { +constexpr float pi3=3.14159265359f; +struct Vertex {float x,y,z,nx,ny,nz;}; +struct Mesh { + std::vector vertices; + std::vector indices; + void draw(const std::vector* 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(vertices.data())+offsetof(Vertex,nx)); + glDrawElements(GL_TRIANGLES,static_cast(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(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 normals{{{1,0,0},{-1,0,0},{0,1,0},{0,-1,0},{0,0,1},{0,0,-1}}}; + const std::array,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(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 points{};std::array 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 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 colors; + std::vector rings; + std::vector 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;i1e-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 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 anchors; + for(unsigned i=0;i(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(mesh.vertices.size()); + if(!rings.empty())for(unsigned j=0;j& 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 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 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()){} +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(); +} +} diff --git a/src/engine_schematic.cpp b/src/engine_schematic.cpp new file mode 100644 index 0000000..9dd2bf2 --- /dev/null +++ b/src/engine_schematic.cpp @@ -0,0 +1,227 @@ +#include "FluidSim/Desktop/EngineSchematic.hpp" +#include "FluidSim/Desktop/PressurePalette.hpp" +#include +#include +#include +#include +namespace fluidsim::port::desktop { +sf::Color pressure_color(float pressure) { + auto c=pressure_tint(pressure); + return {static_cast(255*c.x),static_cast(255*c.y),static_cast(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< 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); +} +} diff --git a/src/helmholtz_scenario.cpp b/src/helmholtz_scenario.cpp index ffbf429..cece95e 100644 --- a/src/helmholtz_scenario.cpp +++ b/src/helmholtz_scenario.cpp @@ -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_); } diff --git a/src/main.cpp b/src/main.cpp index fd4ecdd..3997191 100644 --- a/src/main.cpp +++ b/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<(duration_seconds * sample_rate); const auto report_interval = static_cast(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"; diff --git a/src/mechanical_sound_processor.cpp b/src/mechanical_sound_processor.cpp deleted file mode 100644 index a4fcaee..0000000 --- a/src/mechanical_sound_processor.cpp +++ /dev/null @@ -1,132 +0,0 @@ -#include "FluidSim/MechanicalSoundProcessor.hpp" - -#include -#include - -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(std::max(sample_rate, 1))), - intake_tick_(4200.0f, 20.0f, static_cast(std::max(sample_rate, 1))), - exhaust_tick_(3400.0f, 20.0f, static_cast(std::max(sample_rate, 1))), - slap_filter_(450.0f, static_cast(std::max(sample_rate, 1))), - combustion_filter_(600.0f, static_cast(std::max(sample_rate, 1))), - chain_filter_(1500.0f, static_cast(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 diff --git a/src/outdoor_audio.cpp b/src/outdoor_audio.cpp index fa97ad4..cd29658 100644 --- a/src/outdoor_audio.cpp +++ b/src/outdoor_audio.cpp @@ -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 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 &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 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 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 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 diff --git a/src/outdoor_exhaust_reverb.cpp b/src/outdoor_exhaust_reverb.cpp index 2674e51..f05da1d 100644 --- a/src/outdoor_exhaust_reverb.cpp +++ b/src/outdoor_exhaust_reverb.cpp @@ -4,6 +4,7 @@ #include #include #include +#include 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 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 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(size)); low_filters_.emplace_back(band_low_hz, sample_rate_); mid_filters_.emplace_back(band_high_hz, sample_rate_); average_delay_seconds += static_cast(size) / static_cast(sample_rate_); } average_delay_seconds /= static_cast(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_); diff --git a/src/pulse_test_scenario.cpp b/src/pulse_test_scenario.cpp index 3f146ab..0b4fbc7 100644 --- a/src/pulse_test_scenario.cpp +++ b/src/pulse_test_scenario.cpp @@ -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; } diff --git a/src/scenario.cpp b/src/scenario.cpp index c86a7bb..0e71a31 100644 --- a/src/scenario.cpp +++ b/src/scenario.cpp @@ -1,29 +1,32 @@ #include "FluidSim/Scenario.hpp" +#include "FluidSim/Constants.hpp" #include #include 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(.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 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(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 ends) { + auto fitting=std::make_unique(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 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 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 guard(mutex_); - mechanical_sound_enabled = !mechanical_sound_enabled; -} - void FourStrokeScenario::shift_up() { const std::lock_guard guard(mutex_); vehicle.shift_up(); @@ -245,10 +286,14 @@ void FourStrokeScenario::set_first_gear() { ScenarioSnapshot FourStrokeScenario::snapshot() const { const std::lock_guard 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{pipes[21].state(0).pressure,pipes[22].state(0).pressure} : std::array{}; + 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 engines{&cylinder1, &cylinder2, &cylinder3, &cylinder4, - &cylinder5, &cylinder6, &cylinder7, &cylinder8}; + const std::array 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 FourStrokeScenario::create_pipes(FourStrokePreset preset, bool turbo_enabled) { +std::vector 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 result; + result.emplace_back(area_for_diameter(.075f),.20f,4); + for(int i=0;i 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 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 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; } diff --git a/src/scenario_menu.cpp b/src/scenario_menu.cpp new file mode 100644 index 0000000..847b18a --- /dev/null +++ b/src/scenario_menu.cpp @@ -0,0 +1,83 @@ +#include "FluidSim/Desktop/ScenarioMenu.hpp" +#include +namespace fluidsim::port::desktop { +std::optional choose_scenario(sf::RenderWindow& window,const sf::Font* font,const std::vector& examples) { + if(examples.empty())return std::nullopt; + int selected=0,first=0; + std::vector categories; + for(const auto& e:examples) if(std::find(categories.begin(),categories.end(),e.category)==categories.end())categories.push_back(e.category); + std::string category; + struct Item { std::string title,description;std::size_t value; }; + + auto label=[&](const std::string& str,float x,float y,unsigned size,sf::Color color=sf::Color::White) { + if(!font)return; + sf::Text text(*font,str,size);text.setPosition({x,y});text.setFillColor(color);window.draw(text); + }; + while(window.isOpen()) { + const auto size=window.getSize();const float w=float(size.x),h=float(size.y); + std::vector items; + if(category.empty()) for(std::size_t i=0;istd::optional { + if(category.empty()) {category=categories[items[selected].value];selected=0;first=0;navigating=true;return std::nullopt;} + return items[selected].value; + }; + auto back=[&] {category.clear();selected=0;first=0;navigating=true;}; + auto reveal=[&] {if(selected=first+rows)first=selected-rows+1;}; + while(auto event=window.pollEvent()) { + if(event->is())return std::nullopt; + if(auto key=event->getIf()) { + switch(key->code) { + case sf::Keyboard::Key::Escape:if(category.empty())return std::nullopt;back();break; + case sf::Keyboard::Key::Backspace:case sf::Keyboard::Key::Left:if(!category.empty())back();break; + case sf::Keyboard::Key::Enter:if(auto choice=activate())return choice;break; + case sf::Keyboard::Key::Up:selected=std::max(0,selected-1);reveal();break; + case sf::Keyboard::Key::Down:case sf::Keyboard::Key::Tab:selected=std::min(count-1,selected+1);reveal();break; + case sf::Keyboard::Key::PageUp:selected=std::max(0,selected-rows);reveal();break; + case sf::Keyboard::Key::PageDown:selected=std::min(count-1,selected+rows);reveal();break; + case sf::Keyboard::Key::Home:selected=0;reveal();break; + case sf::Keyboard::Key::End:selected=count-1;reveal();break; + default:break; + } + } + if(auto wheel=event->getIf()) { + first=std::clamp(first+(wheel->delta<0?1:-1),0,std::max(0,count-rows)); + selected=std::clamp(selected,first,std::min(count-1,first+rows-1)); + } + if(auto click=event->getIf();click && click->button==sf::Mouse::Button::Left) { + sf::Vector2f point{float(click->position.x),float(click->position.y)}; + if(!category.empty() && sf::FloatRect({24,54},{150,36}).contains(point))back(); + if(start.contains(point)) {if(auto choice=activate())return choice;} + for(int row=0;rowrows) { + const float track=rows*86.f, thumb=track*std::min(1.f,float(rows)/count); + sf::RectangleShape bar({4,thumb});bar.setPosition({w-21,105+(track-thumb)*first/std::max(1,count-rows)});bar.setFillColor({170,170,170});window.draw(bar); + } + sf::RectangleShape button(start.size);button.setPosition(start.position);button.setFillColor({55,55,55});window.draw(button); + label(category.empty()?"Browse variants [Enter]":"Start selected [Enter]",38,h-76,16); + label("Wheel / Up / Down / Page keys | "+std::to_string(selected+1)+" / "+std::to_string(count),24,h-28,12,{170,170,170}); + window.display(); + } + return std::nullopt; +} +} diff --git a/src/turbocharger.cpp b/src/turbocharger.cpp deleted file mode 100644 index c78706d..0000000 --- a/src/turbocharger.cpp +++ /dev/null @@ -1,200 +0,0 @@ -#include "FluidSim/Turbocharger.hpp" -#include -#include -#include - -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& 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 indices{ci, co, ti, to}; - const std::array 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 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 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 diff --git a/src/two_stroke_scenario.cpp b/src/two_stroke_scenario.cpp index 1092d17..6c7418f 100644 --- a/src/two_stroke_scenario.cpp +++ b/src/two_stroke_scenario.cpp @@ -1,4 +1,5 @@ #include "FluidSim/TwoStrokeScenario.hpp" +#include "FluidSim/Constants.hpp" #include #include @@ -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(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(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); } diff --git a/tests/b205_regression.cpp b/tests/b205_regression.cpp index b97f651..c5ce2ff 100644 --- a/tests/b205_regression.cpp +++ b/tests/b205_regression.cpp @@ -5,6 +5,7 @@ #include #include #include +#include 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 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"); diff --git a/tests/desktop_ui_tests.cpp b/tests/desktop_ui_tests.cpp new file mode 100644 index 0000000..d5017ad --- /dev/null +++ b/tests/desktop_ui_tests.cpp @@ -0,0 +1,37 @@ +#include "FluidSim/Desktop/DesktopUi.hpp" +#include +#include +#include +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=-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"; +} diff --git a/tests/fluid_validation.cpp b/tests/fluid_validation.cpp index 43247b0..579fe9f 100644 --- a/tests/fluid_validation.cpp +++ b/tests/fluid_validation.cpp @@ -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 +#include #include #include #include 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 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 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(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 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 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 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 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 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 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 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 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 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 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); diff --git a/tests/intake_regression.cpp b/tests/intake_regression.cpp index 517dbb3..27b87ad 100644 --- a/tests/intake_regression.cpp +++ b/tests/intake_regression.cpp @@ -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"); diff --git a/tests/physics_tests.cpp b/tests/physics_tests.cpp index 173819f..d6c1505 100644 --- a/tests/physics_tests.cpp +++ b/tests/physics_tests.cpp @@ -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 #include @@ -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; diff --git a/tests/saab_turbo_regression.cpp b/tests/saab_turbo_regression.cpp deleted file mode 100644 index 8ec1563..0000000 --- a/tests/saab_turbo_regression.cpp +++ /dev/null @@ -1,46 +0,0 @@ -#include "FluidSim/Scenario.hpp" -#include -#include -#include -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<snapshot().rpm +#include +#include +#include +#include +using namespace fluidsim::port; +int main() { + std::set 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< -#include - -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 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 areas; - const std::array 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"; -} diff --git a/tests/turbo_twin_regression.cpp b/tests/turbo_twin_regression.cpp deleted file mode 100644 index bf06e97..0000000 --- a/tests/turbo_twin_regression.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include "FluidSim/Scenario.hpp" -#include -#include -#include -#include -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"; -} diff --git a/tests/two_stroke_regression.cpp b/tests/two_stroke_regression.cpp index d713c56..5bc529f 100644 --- a/tests/two_stroke_regression.cpp +++ b/tests/two_stroke_regression.cpp @@ -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'; } diff --git a/tools/audio_capture.cpp b/tools/audio_capture.cpp index 5ebb442..992358a 100644 --- a/tools/audio_capture.cpp +++ b/tools/audio_capture.cpp @@ -65,17 +65,23 @@ struct Metrics { } }; template -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 s; if constexpr (std::is_same_v) s = std::make_unique(44100, config); else s = std::make_unique(); + s->set_pipe_solver(solver); s->throttle = pedal; s->set_audio_scene(scene); + for(int i=0; i0) 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(prefix, frames, pedal, selected, config); - else if (scenario == "twin") - capture(prefix, frames, pedal, selected, config); - else if (scenario == "b205" && turbo_enabled) - capture(prefix, frames, pedal, selected, config); + if (scenario == "twin") + capture(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup); else if (scenario == "b205") - capture(prefix, frames, pedal, selected, config); + capture(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup); else if (scenario == "two-stroke") - capture(prefix, frames, pedal, selected, config); + capture(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup); else if (scenario == "v8") - capture(prefix, frames, pedal, selected, config); + capture(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) { diff --git a/tools/scenario_benchmark.cpp b/tools/scenario_benchmark.cpp index 4802c09..f936028 100644 --- a/tools/scenario_benchmark.cpp +++ b/tools/scenario_benchmark.cpp @@ -2,12 +2,31 @@ #include "FluidSim/TwoStrokeScenario.hpp" #include "FluidSim/HelmholtzScenario.hpp" #include "FluidSim/PulseTestScenario.hpp" +#include #include +#include #include #include #include +#include +#include #include +#include using namespace fluidsim::port; +static void set_uniform_pipe_resolution(std::vector& 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(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 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 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< 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(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)); diff --git a/tools/thread_benchmark.cpp b/tools/thread_benchmark.cpp index c93a5a6..77974b9 100644 --- a/tools/thread_benchmark.cpp +++ b/tools/thread_benchmark.cpp @@ -3,13 +3,31 @@ #include "FluidSim/TwoStrokeScenario.hpp" #include "FluidSim/HelmholtzScenario.hpp" #include "FluidSim/PulseTestScenario.hpp" +#include #include #include +#include #include +#include +#include #include +#include 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& 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(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 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; diff --git a/tools/turbo_benchmark.cpp b/tools/turbo_benchmark.cpp deleted file mode 100644 index 02d1094..0000000 --- a/tools/turbo_benchmark.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include "FluidSim/Scenario.hpp" -#include -#include -#include -#include -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(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; - } - } -} diff --git a/verification/AUDIO_RENDERING.md b/verification/AUDIO_RENDERING.md index 89376fb..18ebb8a 100644 --- a/verification/AUDIO_RENDERING.md +++ b/verification/AUDIO_RENDERING.md @@ -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. diff --git a/verification/B205_BASELINE.md b/verification/B205_BASELINE.md index 27eb374..daed8ca 100644 --- a/verification/B205_BASELINE.md +++ b/verification/B205_BASELINE.md @@ -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 diff --git a/verification/OPTIMIZATION.md b/verification/OPTIMIZATION.md index d56cdab..d33a80c 100644 --- a/verification/OPTIMIZATION.md +++ b/verification/OPTIMIZATION.md @@ -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. diff --git a/verification/SAAB_GT17.md b/verification/SAAB_GT17.md deleted file mode 100644 index 1e031f7..0000000 --- a/verification/SAAB_GT17.md +++ /dev/null @@ -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. diff --git a/verification/SOLVER_COMPARISON.md b/verification/SOLVER_COMPARISON.md new file mode 100644 index 0000000..f33210b --- /dev/null +++ b/verification/SOLVER_COMPARISON.md @@ -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) diff --git a/verification/TURBO_VZ21.md b/verification/TURBO_VZ21.md deleted file mode 100644 index 9ecf460..0000000 --- a/verification/TURBO_VZ21.md +++ /dev/null @@ -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. diff --git a/verification/V8_PIPE_PERFORMANCE.md b/verification/V8_PIPE_PERFORMANCE.md new file mode 100644 index 0000000..1dddfca --- /dev/null +++ b/verification/V8_PIPE_PERFORMANCE.md @@ -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 `. 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. diff --git a/verification/solver-comparison/diagnostics.log b/verification/solver-comparison/diagnostics.log new file mode 100644 index 0000000..1ff7ed9 --- /dev/null +++ b/verification/solver-comparison/diagnostics.log @@ -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 + diff --git a/verification/solver-comparison/final.csv b/verification/solver-comparison/final.csv new file mode 100644 index 0000000..5f30c18 --- /dev/null +++ b/verification/solver-comparison/final.csv @@ -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 diff --git a/verification/solver-comparison/initial.csv b/verification/solver-comparison/initial.csv new file mode 100644 index 0000000..fce053f --- /dev/null +++ b/verification/solver-comparison/initial.csv @@ -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 diff --git a/verification/solver-comparison/summary.json b/verification/solver-comparison/summary.json new file mode 100644 index 0000000..8ffdb27 --- /dev/null +++ b/verification/solver-comparison/summary.json @@ -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 + } +] diff --git a/verification/solver-comparison/validation.log b/verification/solver-comparison/validation.log new file mode 100644 index 0000000..0dc3df3 --- /dev/null +++ b/verification/solver-comparison/validation.log @@ -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