Load engines from TOML and improve scenario audio and carb tuning

Migrate 32 engine definitions into validated startup-loaded TOML files and separate configuration and snapshots from scenario assembly. Add camera-based stereo propagation from shared pipe geometry, preserve exhaust character, tune carburetor mixtures and idle, use ignition cuts for carb engines, and support returning to the scenario menu.

Validation: full Windows build and all 14 CTest checks passed.
This commit is contained in:
max
2026-10-24 03:24:35 +02:00
parent 5607dcf2b5
commit bf8621ad39
82 changed files with 24084 additions and 648 deletions

3
.gitignore vendored
View File

@@ -5,3 +5,6 @@
CMakeUserPresets.json CMakeUserPresets.json
/verification/bin/ /verification/bin/
/verification/obj/ /verification/obj/
# Generated compiler debug databases
*.pdb

View File

@@ -5,6 +5,7 @@ option(FLUIDSIM_BUILD_DESKTOP "Build the SFML windowed simulator with keyboard c
set(FLUIDSIM_FONT_FILE "" CACHE FILEPATH "Optional font file copied beside the desktop executable") set(FLUIDSIM_FONT_FILE "" CACHE FILEPATH "Optional font file copied beside the desktop executable")
add_library(fluidsim_core add_library(fluidsim_core
src/engine_config.cpp
src/carburetor.cpp src/carburetor.cpp
src/ecu.cpp src/ecu.cpp
src/engine.cpp src/engine.cpp
@@ -17,11 +18,15 @@ add_library(fluidsim_core
src/outdoor_audio.cpp src/outdoor_audio.cpp
src/outdoor_exhaust_reverb.cpp src/outdoor_exhaust_reverb.cpp
src/scenario.cpp src/scenario.cpp
src/scenario_snapshot.cpp
src/scenario_geometry.cpp
src/solver.cpp src/solver.cpp
src/vehicle.cpp src/vehicle.cpp
src/simulation_ring_buffer.cpp src/simulation_ring_buffer.cpp
) )
target_include_directories(fluidsim_core PUBLIC include) target_include_directories(fluidsim_core PUBLIC include)
target_include_directories(fluidsim_core SYSTEM PRIVATE third_party/tomlplusplus)
target_compile_definitions(fluidsim_core PRIVATE FLUIDSIM_ENGINE_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}/engines")
target_compile_features(fluidsim_core PUBLIC cxx_std_20) target_compile_features(fluidsim_core PUBLIC cxx_std_20)
option(FLUIDSIM_ENABLE_SIMD "Allow compiler SIMD in fluid kernels without fast-math" ON) option(FLUIDSIM_ENABLE_SIMD "Allow compiler SIMD in fluid kernels without fast-math" ON)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
@@ -109,6 +114,9 @@ endif()
include(CTest) include(CTest)
if(BUILD_TESTING) if(BUILD_TESTING)
add_executable(fluidsim_engine_config_tests tests/engine_config_tests.cpp)
target_link_libraries(fluidsim_engine_config_tests PRIVATE fluidsim_core)
add_test(NAME fluidsim_engine_config_tests COMMAND fluidsim_engine_config_tests)
add_executable(fluidsim_scenario_examples tests/scenario_examples.cpp) add_executable(fluidsim_scenario_examples tests/scenario_examples.cpp)
target_link_libraries(fluidsim_scenario_examples PRIVATE fluidsim_core) target_link_libraries(fluidsim_scenario_examples PRIVATE fluidsim_core)
add_test(NAME fluidsim_scenario_examples COMMAND fluidsim_scenario_examples) add_test(NAME fluidsim_scenario_examples COMMAND fluidsim_scenario_examples)
@@ -167,4 +175,24 @@ if(BUILD_TESTING)
add_test(NAME fluidsim_b205_regression COMMAND fluidsim_b205_regression) add_test(NAME fluidsim_b205_regression COMMAND fluidsim_b205_regression)
endif() endif()
add_executable(fluidsim_carb_calibration tools/carb_calibration.cpp)
target_link_libraries(fluidsim_carb_calibration PRIVATE fluidsim_core)
if(BUILD_TESTING)
add_test(NAME fluidsim_carb_calibration COMMAND fluidsim_carb_calibration --check)
endif()
include(cmake/MinGWRuntime.cmake) include(cmake/MinGWRuntime.cmake)
# Keep definitions beside every built executable, including tests and CLI tools.
add_custom_target(fluidsim_engine_assets ALL
COMMAND ${CMAKE_COMMAND}
"-DSOURCE=${CMAKE_CURRENT_SOURCE_DIR}/engines"
"-DDESTINATION=${CMAKE_CURRENT_BINARY_DIR}/engines"
-P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/CopyEngineConfigs.cmake")
get_property(fluidsim_targets DIRECTORY PROPERTY BUILDSYSTEM_TARGETS)
foreach(target IN LISTS fluidsim_targets)
get_target_property(target_type ${target} TYPE)
if(target_type STREQUAL "EXECUTABLE")
add_dependencies(${target} fluidsim_engine_assets)
endif()
endforeach()

View File

@@ -39,7 +39,6 @@
"CMAKE_BUILD_TYPE": "RelWithDebInfo", "CMAKE_BUILD_TYPE": "RelWithDebInfo",
"CMAKE_C_COMPILER": "$penv{LOCALAPPDATA}/FluidSimTools/mingw64/bin/gcc.exe", "CMAKE_C_COMPILER": "$penv{LOCALAPPDATA}/FluidSimTools/mingw64/bin/gcc.exe",
"CMAKE_CXX_COMPILER": "$penv{LOCALAPPDATA}/FluidSimTools/mingw64/bin/g++.exe", "CMAKE_CXX_COMPILER": "$penv{LOCALAPPDATA}/FluidSimTools/mingw64/bin/g++.exe",
"CMAKE_MAKE_PROGRAM": "$penv{LOCALAPPDATA}/FluidSimTools/mingw64/bin/ninja.exe",
"FLUIDSIM_BUILD_DESKTOP": "ON", "FLUIDSIM_BUILD_DESKTOP": "ON",
"BUILD_TESTING": "ON" "BUILD_TESTING": "ON"
} }

107
README.md
View File

@@ -123,18 +123,27 @@ The primary source is a linear compact-monopole approximation,
`p(r,t) = rho_ambient * (d volume_flow/dt)/(4*pi*r)`. Mouth volume flow `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 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. actual mass flux; hot/cold density changes are excluded from the acoustic source.
Intake and exhaust have separate static Intake and exhaust sources sit at the open ends of the same world routes used
positions, fractional-sample propagation delays, inverse-distance spreading, to draw the pipes, including both outlets of dual exhausts. The desktop camera
and left/right ear paths. Playback calibration is 200 Pa per device full scale; is the listener: orbit, pan and zoom change its position and ear orientation.
Left and right ears are 18 cm apart, with separate fractional-sample travel
delays at 343 m/s and inverse-distance spreading. Camera updates smooth over
20 ms while retaining propagation history and reflection filter state.
Playback calibration is 200 Pa per device full scale;
this is not calibrated speaker SPL. A second-order 6 kHz monitoring filter this is not calibrated speaker SPL. A second-order 6 kHz monitoring filter
reduces sharp sample-to-sample transients without saturation or synthetic bangs. reduces sharp sample-to-sample transients without saturation or synthetic bangs.
A roughly 3.5 Hz device DC blocker and final PCM clipping protection remain. A roughly 3.5 Hz device DC blocker and final PCM clipping protection remain.
Use 1x playback for acoustic evaluation. Use 1x playback for acoustic evaluation. Toggle **G** off to hear the full
distance-related loudness change; automatic gain otherwise compensates for it.
The default environment is a nearby wall. Press **R** to cycle dry, field, The default environment is a nearby wall. Press **R** to cycle dry, field,
wall, woodland, and tunnel. Ground/wall image paths provide early reflections; wall, woodland, and tunnel. Ground/wall image paths provide early reflections;
the live eight-line feedback-delay network supplies a scene-dependent late the live eight-line feedback-delay network supplies a scene-dependent late
tail. Woodland scatterers and the hard-surface tunnel images are approximate. tail. Early reflections follow the listener geometry, with plane reflections
disabled when the listener crosses to the other side. The diffuse tail receives
a delayed centre signal rather than summing the ears, avoiding ear-delay comb
filtering in the reverb input. Woodland scatterers and the hard-surface tunnel
images are approximate.
These are listening profiles, not validated acoustics. Mechanical sounds are These are listening profiles, not validated acoustics. Mechanical sounds are
not implemented. not implemented.
Open pipe mouths now use a causal unflanged radiation boundary with frequency- Open pipe mouths now use a causal unflanged radiation boundary with frequency-
@@ -149,7 +158,9 @@ for capture commands, current renderer details and limitations.
Launching `fluidsim_desktop` without `--scenario` opens a small scenario menu. Launching `fluidsim_desktop` without `--scenario` opens a small scenario menu.
Choose the twin, YZ125, Saab B205, or American V8 card, or select with Up/Down Choose the twin, YZ125, Saab B205, or American V8 card, or select with Up/Down
and press Enter. and press Enter.
Escape closes the menu. Simulation and audio start after selection. Passing Escape returns from a running scenario to the menu, stopping its simulation and
audio so another scenario can be selected. Escape closes the menu at the category
screen. Simulation and audio start after selection. Passing
`--scenario twin`, `--scenario two-stroke`, `--scenario b205`, or `--scenario v8` `--scenario twin`, `--scenario two-stroke`, `--scenario b205`, or `--scenario v8`
bypasses the menu; `--scene` and `--exhaust` options also apply bypasses the menu; `--scene` and `--exhaust` options also apply
when choosing through the menu. when choosing through the menu.
@@ -353,11 +364,12 @@ catalyst, cold-start temperature model or injector electrical model. Idle
control can still stall under excessive load and does not impose crankshaft control can still stall under excessive load and does not impose crankshaft
speed or add artificial sustaining torque. Existing combustion/thermal models speed or add artificial sustaining torque. Existing combustion/thermal models
remain approximations; ECU changes do not establish their physical accuracy. remain approximations; ECU changes do not establish their physical accuracy.
The two-stroke uses a fixed mechanical carb slide stop instead of RPM-controlled The two-stroke uses a fixed mechanical carb slide stop. With the pedal released,
idle throttle or idle ignition trim. Each carb scenario config sets the ECU trims ignition timing around the configured idle RPM instead of moving
`TwoStrokeConfig::idle_slide_opening` (normalized 0..1); the default is 0.045, the carb stop. Each carb scenario config sets
or 4.5% slide travel. Pedal demand above that stop opens the slide further. `TwoStrokeConfig::idle_slide_opening` (normalized 0..1); the catalog 125 cc
The resulting idle RPM depends on the simulation and load. Its scenarios now use a calibrated 3% stop. Pedal demand above that stop opens the
slide further. The resulting idle RPM depends on the simulation and load. Its
reed is a damped lumped cantilever model with finite opening time and seating reed is a damped lumped cantilever model with finite opening time and seating
contacts. Cylinder and crankcase moving-volume work use adiabatic energy changes. contacts. Cylinder and crankcase moving-volume work use adiabatic energy changes.
Jet dimensions are effective flow calibrations rather than measured Mikuni Jet dimensions are effective flow calibrations rather than measured Mikuni
@@ -492,7 +504,8 @@ 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 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 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. plenum also remains a storage volume.
`V8ExhaustConfig` configures primary length/diameter and enables the H crossover; The TOML `primaries` pipe group configures primary length/diameter, while
`[junctions] h_pipe` enables the H crossover;
pass it as the fourth `FourStrokeScenario` constructor argument. The renderer 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, 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. using local pipe diameter and pressure colors without collector boxes.
@@ -505,16 +518,25 @@ The previous header optimization measurements applied to the earlier finite-volu
The startup browser now scrolls with the mouse wheel, Up/Down, Page Up/Down, 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 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 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 `engines/*.toml`, loaded at startup and grouped into categories with selectable variants. These are configuration comparisons of
the existing engine models, not measurements of additional production engines. the existing engine models, not measurements of additional production engines.
The V8 currently uses port-coupled finite-volume fittings, configured by The V8 currently uses port-coupled finite-volume fittings, configured by
`V8ExhaustConfig::junction_volume` (default 50 cc per fitting). They use the `JunctionConfig::junction_volume` (default 50 cc per fitting). They use the
existing conservative component/port exchange and have no volume boxes in the existing conservative component/port exchange and have no volume boxes in the
renderer. The earlier zero-storage Rusanov junction remains available for renderer. The earlier zero-storage Rusanov junction remains available for
comparison through `BoundarySystem::add_junction`, but is not used by the V8. 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. The 4-2-1 piping and the H crossover geometry remain the same.
### TOML engine definitions
Every engine is defined in its own file under `engines/`. Copy an existing
`.toml` file, change its `id`, name and calibration, then restart to load it.
No rebuild is required when editing definitions in the active directory.
Unknown keys, invalid dimensions, malformed timing and duplicate IDs produce
a file-specific startup error. See [the engine configuration guide](docs/ENGINE_CONFIG.md)
for units, directory discovery, sections and pipe topology ordering.
### Engine families and inspired builds ### Engine families and inspired builds
The browser opens at engine categories; click a category to see variants, then The browser opens at engine categories; click a category to see variants, then
@@ -531,7 +553,7 @@ estimated calibrations and simplified common-plenum/exhaust networks: they are
not factory replicas, measured torque curves, or turbocharged configurations. not factory replicas, measured torque curves, or turbocharged configurations.
The 8.0 L big-block variant transports carburetor fuel through its intake into 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`. the cylinders. Its fixed idle stop is configured with `[ecu] idle_base_opening` in its TOML file.
It reuses the lumped carb model with scaled effective jets; multiple carb barrels, It reuses the lumped carb model with scaled effective jets; multiple carb barrels,
floats and cylinder-specific fuel distribution are not resolved. Larger engine floats and cylinder-specific fuel distribution are not resolved. Larger engine
geometry requires its own mixture and idle calibration. geometry requires its own mixture and idle calibration.
@@ -544,5 +566,56 @@ chamber is a geometry experiment, not a guaranteed RPM or power improvement.
Launch a specific variant directly with `--example big-carb`, `--example flat6`, Launch a specific variant directly with `--example big-carb`, `--example flat6`,
`--example fifty` or another catalog ID. Carb jet scaling is configurable per `--example fifty` or another catalog ID. Carb jet scaling is configurable per
engine (`EngineConfig::carb_jet_scale`, `carb_pilot_scale`, or engine: four-strokes use effective jet diameters in `[fuel]`; two-strokes
`TwoStrokeConfig::jet_scale`). use jet multipliers in `[two_stroke]`.
### Pulse-preserving custom exhausts
The custom inline three, F20 four, inline five and inline six examples use
unequal-length primaries, 50 cc junction collectors and secondary pipes rather
than a single large mixing volume. Their pipe damping matches the undamped Saab
path. Inline fours/fives/sixes use physical firing orders so collector grouping
preserves pulse timing; combustion remains even-fire.
The custom big-block V8 uses the American V8's cross-plane 1-5-4-2-6-3-7-8 firing
order and 4-2-1 headers with an H crossover, retaining its own displacement and
carburetor. V10/V12 examples retain separate banks through junction collectors,
secondary pipes and two radiating outlets, including both outlets in the spatial
audio renderer. The existing Saab, American V8, single and twin presets retain
their exhaust paths.
Capture any catalog entry at controlled RPM for comparison:
```powershell
./out/build/windows-gcc/fluidsim_audio_capture.exe --example v12 --rpm 3000 --warmup 0.6 --seconds 2 --throttle 0.55 --scene dry --out out/audio/v12
```
See [custom exhaust verification](verification/CUSTOM_EXHAUST_CHARACTER.md) for
fixed-RPM comparison results.
### Carburetor calibration
All seven two-stroke catalog variants and both carbureted four-strokes have
separate pilot, main-jet and needle-taper calibrations. Progressive pilot metering
fills the 5% throttle transition after lowering the 125 idle stops. The smaller four-stroke
single (`single-450`) now uses a 40 mm carburetor, transported fuel and milder
355/565-degree intake and 145/370-degree exhaust timing. The 650 cc single keeps
EFI. The big-block idle opening is 7%; the straight-pipe 125 uses 3%. The racing
125 and its chamber variants use the calibrated 3% mechanical slide stop.
Needle annulus area is interpolated continuously across slide positions; liquid
fuel still comes from the simulated jet pressure drop and float bowl. There is
no AFR feedback or cylinder-side fuel correction. The displayed carb AFR is the
uncapped ratio of filtered air flow to delivered fuel flow.
`fluidsim_carb_calibration --check` tests every carbureted catalog scenario at
idle, 5%, 10%, 15%, 25%, 50%, 75% and full pedal on an imposed-speed dyno, then
releases the dyno for an idle run. It fails on AFR outside the calibration
bounds, stalling, nonfinite RPM/AFR, pipe positivity repairs, a changed racing
125 stop, or loss of transported carb fuel in the 450 single. Run it through
`ctest --preset windows -R carb_calibration --output-on-failure`.
The tool also prints CSV diagnostics: run without arguments for the four main
operating points, or use `--sweep single-450` for a denser sweep of one scenario.
See [carb calibration results](verification/CARB_CALIBRATION.md) for operating
conditions, AFR values and the distinction between carb and recirculated charge.

View File

@@ -0,0 +1,14 @@
file(MAKE_DIRECTORY "${DESTINATION}")
file(GLOB definitions "${SOURCE}/*.toml")
foreach(definition IN LISTS definitions)
get_filename_component(name "${definition}" NAME)
configure_file("${definition}" "${DESTINATION}/${name}" COPYONLY)
endforeach()
# Remove only stale definitions in the build's asset directory.
file(GLOB old_definitions "${DESTINATION}/*.toml")
foreach(definition IN LISTS old_definitions)
get_filename_component(name "${definition}" NAME)
if(NOT EXISTS "${SOURCE}/${name}")
file(REMOVE "${definition}")
endif()
endforeach()

126
docs/ENGINE_CONFIG.md Normal file
View File

@@ -0,0 +1,126 @@
# Engine definitions
Each `engines/*.toml` file defines one engine and its default vehicle/load
calibration. Files are loaded once when the program starts. Add a file, give it
a unique `id`, then restart to see it in the browser. Editing a definition does
not require recompiling the engine simulator.
Start by copying a similar engine, such as `single-450.toml`, `five.toml`,
`v8-h.toml` or `yz-racing.toml`. Existing definitions contain their effective
calibration values, including the previous carb and idle tunes.
```toml
schema_version = 1
id = "my-single"
name = "My four-stroke single"
category = "Singles / four-stroke"
description = "My calibration"
order = 100
cycle = "four_stroke"
```
`id`, `name`, `category`, `cycle` and `schema_version` are required. IDs use
lowercase letters, digits, `-` and `_`. `order` controls menu order; ties sort by
ID. Duplicate IDs, unknown keys, unsupported versions, incorrect types and
invalid physical values stop loading with a diagnostic identifying the file
and field. TOML syntax errors also report their location.
## File discovery and distribution
The first available directory is used:
1. `FLUIDSIM_ENGINE_DIR`, when set. A missing override directory is an error.
2. `engines/` under the working directory.
3. `engines/` beside the executable on Windows and Linux.
4. The source checkout's `engines/` directory as a development fallback.
Definitions are cached for the process lifetime. The selected directory is
loaded as one catalog; it is not merged with the bundled files. In a repository
working directory, source definitions take priority over build copies.
CMake synchronizes the definitions into the build output when building an
executable. Distribute that `engines/` directory with the executable. The TOML
parser is vendored, so loading definitions needs no network connection.
```powershell
$env:FLUIDSIM_ENGINE_DIR = "C:\MyEngines"
.\fluidsim_desktop.exe --example my-single
```
## Units and sections
File keys state dimensional units. `_mm` is millimetres, `_litres` is litres,
`_cc` is cubic centimetres, `_deg` is crank degrees, `_s` is seconds, `_nm` is
newton metres and `_kg_m2` is rotational inertia. The loader converts lengths
and volumes to SI units. Openings and mixture/taper multipliers are normalized
fractions, not percentages: `0.03` means 3%.
Four-stroke definitions use these sections:
| Section | Contents |
| --- | --- |
| `[engine]` | Cylinder count, layout, bank angle, phases, intake volumes, throttle, startup, load, and exhaust topology |
| `[cylinder]` | Bore/stroke/rod, compression, valve geometry and timing, combustion and heat transfer |
| `[crankshaft]` | Inertia and mechanical friction |
| `[ecu]` | Idle, actuator response, ignition curve, limiter and overrun thresholds |
| `[vehicle]` | Vehicle mass, gears, clutch, wheel geometry and road resistance |
| `[fuel]` | `system = "injection"` or `"carburetor"`, jet diameters and taper/film calibration |
| `[junctions]` | Fitting volume and H crossover selection |
| `[silencers]` | Connecting pipes, resonator and muffler profiles, when used |
| `[[pipes]]` | Ordered pipe groups for the selected exhaust topology |
`engine.phase_degrees` contains one phase per cylinder in cylinder index order,
within `[0,720)`. It records the actual crank offsets rather than a firing-order
label. `ecu.ignition_map` is an array of `[RPM, degrees BTDC]` pairs with strictly
increasing RPM. Negative advance means ignition after TDC.
For a carbureted four-stroke, use `ecu.idle_control = "fixed"` and set
`ecu.idle_base_opening`. Its limiter and overrun control cut ignition while the
carburetor continues supplying fuel. Jet diameters in `[fuel]` are effective
diameters after the previous displacement and jet multipliers have been applied.
Two-stroke definitions use `[two_stroke]`, `[ecu]` and `[vehicle]`.
`two_stroke.idle_slide_opening` sets the mechanical stop;
`idle_spark_gain` and `idle_minimum_advance_deg` set its idle spark trim.
Two-stroke jet multipliers retain their existing meaning.
`chamber_sections_mm` contains six `[length, inlet diameter, outlet diameter]`
sections: header, two diffusers, belly, baffle cone and stinger.
Required cylinder geometry must be explicit. Optional calibration fields use
the typed config defaults; a misspelled field is always an error. Existing files
include explicit values so their behavior does not depend on future defaults.
## Pipe groups and exhaust topologies
These definitions configure the existing topology builders. They do not yet
describe arbitrary graphs of cylinders and junctions. A group's `id` must be
unique; `count` repeats its dimensions across adjacent pipes.
```toml
[[pipes]]
id = "primaries"
count = 8
diameter_mm = 38.0
length_mm = 750.0
cells = 15
```
The order is significant because the topology builder connects the expanded
groups to fixed cylinder and junction ports:
| `engine.topology` | Pipe order after expansion |
| --- | --- |
| `collector` | Intake, N runners, N primaries, one tailpipe |
| `staged` | Intake, N runners, N primaries, outlet pipe(s), secondary pipe(s) |
| `american_v8` | Intake, 8 runners, 8 primaries, 4 secondaries, 2 bank collectors, 2 tailpipes, H crossover |
The staged inline triple has one secondary; other staged layouts have two.
Staged V engines have two independent outlets. The American topology requires
eight cylinders in a V layout; setting `junctions.h_pipe = false` closes both
ends of the unused crossover and leaves the banks independent.
A collector outlet can instead use `profile = "silenced"`. Its dimensions and
mesh then come from `[silencers]`, `[silencers.resonator]` and
`[silencers.muffler]`; omit the group's uniform pipe dimensions. The loader
checks expanded pipe counts and mesh limits before construction. Pipe positions
continue to use the shared scene routes for rendering and sound sources.

163
engines/big-carb.toml Normal file
View File

@@ -0,0 +1,163 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "big-carb"
name = "8.0 litre / carbureted big-block"
category = "V8 / big-block"
description = "Cross-plane firing / American 4-2-1 + H / carb fuel transport"
order = 29
cycle = "four_stroke"
[engine]
bank_angle_deg = 90.0
collector_volume_litres = 2.0
cylinders = 8
initial_rpm = 1120.0
initialize_phased_charge = true
layout = "v"
maximum_load_torque_nm = 30.0
minimum_substeps = 1
phase_degrees = [0.0, 450.0, 270.0, 540.0, 630.0, 360.0, 180.0, 89.99998]
pipe_damping = 1.0
plenum_volume_litres = 8.0
starter_torque_nm = 201.1122
throttle_diameter_mm = 85.0
topology = "american_v8"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 114.3
burn_duration_deg = 65.0
burn_start_deg = 5.0
compression = 9.0
energy_variation = 0.05
exhaust_close_deg = 370.0
exhaust_open_deg = 140.0
exhaust_valve_count = 1
exhaust_valve_diameter_mm = 36.576
exhaust_valve_lift_mm = 10.287
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 570.0
intake_open_deg = 350.0
intake_valve_count = 1
intake_valve_diameter_mm = 43.434
intake_valve_lift_mm = 11.43
misfire_probability = 0.0
rod_mm = 176.4
stroke_mm = 98.0
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 32.17795
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.03217795
inertia_kg_m2 = 0.9653385
[ecu]
idle_base_opening = 0.07
idle_control = "fixed"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -5.0
idle_rpm = 800.0
idle_spark_gain = 0.02
ignition_map = [[0.0, 10.0], [750.0, 12.0], [2500.0, 28.0], [4000.0, 32.0], [6000.0, 30.0]]
overrun_cut_rpm = 2000.0
overrun_fuel_cut_enabled = false
overrun_ignition_cut_enabled = true
overrun_resume_rpm = 1100.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = false
rev_limit_rpm = 7000.0
rev_resume_rpm = 6750.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 1769.787
clutch_engagement_rate = 8.0
drag_coefficient = 0.4
final_drive_ratio = 3.623529
frontal_area = 2.0
gear_ratios = [3.35, 1.93, 1.29, 1.0, 0.68]
gravity = 9.80665
mass_kg = 1400.0
primary_reduction = 1.0
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 1.2
wheel_radius_mm = 320.0
[fuel]
system = "carburetor"
pilot_jet_diameter_mm = 0.2337673
main_jet_diameter_mm = 4.524528
needle_jet_diameter_mm = 4.656493
needle_low_scale = 0.6
needle_mid_scale = 0.65
needle_off_idle_scale = 0.964
needle_quarter_scale = 0.64
needle_tip_scale = 0.782
needle_transition_scale = 0.89
needle_upper_scale = 0.76
bowl_capacity_kg = 0.1
bowl_refill_kg_per_s = 0.1
[junctions]
h_pipe = true
junction_volume_cc = 50.0
[[pipes]]
id = "intake"
cells = 3
diameter_mm = 70.0
length_mm = 180.0
[[pipes]]
id = "runners"
count = 8
cells = 2
diameter_mm = 38.0
length_mm = 120.0
[[pipes]]
id = "primaries"
count = 8
cells = 15
diameter_mm = 38.0
length_mm = 750.0
[[pipes]]
id = "secondaries"
count = 4
cells = 8
diameter_mm = 45.0
length_mm = 400.0
[[pipes]]
id = "bank_collectors"
count = 2
cells = 11
diameter_mm = 50.0
length_mm = 550.0
[[pipes]]
id = "tailpipes"
count = 2
cells = 25
diameter_mm = 50.0
length_mm = 1250.0
[[pipes]]
id = "crossover"
cells = 16
diameter_mm = 38.0
length_mm = 780.0

126
engines/busso.toml Normal file
View File

@@ -0,0 +1,126 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "busso"
name = "Busso-inspired / 3.0 V6"
category = "V6"
description = "Estimated concept calibration / EFI"
order = 26
cycle = "four_stroke"
[engine]
bank_angle_deg = 60.0
collector_volume_litres = 1.5
cylinders = 6
initial_rpm = 1400.0
initialize_phased_charge = true
layout = "v"
maximum_load_torque_nm = 30.0
minimum_substeps = 1
phase_degrees = [0.0, 600.0, 480.0, 360.0, 240.0, 120.0]
pipe_damping = 1.0
plenum_volume_litres = 6.0
starter_torque_nm = 73.97478
throttle_diameter_mm = 65.0
topology = "collector"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 93.0
burn_duration_deg = 60.0
burn_start_deg = 5.0
compression = 10.0
energy_variation = 0.05
exhaust_close_deg = 390.0
exhaust_open_deg = 130.0
exhaust_valve_count = 1
exhaust_valve_diameter_mm = 29.76
exhaust_valve_lift_mm = 8.370001
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 590.0
intake_open_deg = 340.0
intake_valve_count = 1
intake_valve_diameter_mm = 35.34
intake_valve_lift_mm = 9.3
misfire_probability = 0.0
rod_mm = 130.68
stroke_mm = 72.6
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 11.83596
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.01183596
inertia_kg_m2 = 0.3550789
[ecu]
idle_base_opening = 0.05
idle_control = "feedback"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -5.0
idle_rpm = 1000.0
idle_spark_gain = 0.02
ignition_map = [[0.0, 10.0], [15000.0, 40.0]]
overrun_cut_rpm = 3500.0
overrun_fuel_cut_enabled = true
overrun_ignition_cut_enabled = false
overrun_resume_rpm = 2400.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = true
rev_limit_rpm = 7000.0
rev_resume_rpm = 6750.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 650.978
clutch_engagement_rate = 8.0
drag_coefficient = 0.7
final_drive_ratio = 2.7
frontal_area = 0.65
gear_ratios = [4.1, 2.7, 2.0, 1.55, 1.25, 0.98]
gravity = 9.80665
mass_kg = 1400.0
primary_reduction = 1.8
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 1.2
wheel_radius_mm = 320.0
[fuel]
system = "injection"
[[pipes]]
id = "intake"
cells = 4
diameter_mm = 75.0
length_mm = 200.0
[[pipes]]
id = "runners"
count = 6
cells = 5
diameter_mm = 35.0
length_mm = 250.0
[[pipes]]
id = "primaries"
count = 6
cells = 13
diameter_mm = 38.0
length_mm = 650.0
[[pipes]]
id = "tailpipe"
cells = 30
diameter_mm = 60.0
length_mm = 1500.0

126
engines/ej20.toml Normal file
View File

@@ -0,0 +1,126 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "ej20"
name = "EJ20-inspired / 2.0 flat four NA"
category = "Boxer / flat four"
description = "Estimated concept calibration / EFI"
order = 27
cycle = "four_stroke"
[engine]
bank_angle_deg = 180.0
collector_volume_litres = 1.0
cylinders = 4
initial_rpm = 1400.0
initialize_phased_charge = true
layout = "boxer"
maximum_load_torque_nm = 30.0
minimum_substeps = 1
phase_degrees = [0.0, 540.0, 360.0, 180.0]
pipe_damping = 1.0
plenum_volume_litres = 4.0
starter_torque_nm = 49.85708
throttle_diameter_mm = 65.0
topology = "collector"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 92.0
burn_duration_deg = 60.0
burn_start_deg = 5.0
compression = 10.0
energy_variation = 0.05
exhaust_close_deg = 390.0
exhaust_open_deg = 130.0
exhaust_valve_count = 1
exhaust_valve_diameter_mm = 29.44
exhaust_valve_lift_mm = 8.280001
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 590.0
intake_open_deg = 340.0
intake_valve_count = 1
intake_valve_diameter_mm = 34.96
intake_valve_lift_mm = 9.2
misfire_probability = 0.0
rod_mm = 135.0
stroke_mm = 75.0
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 7.977133
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.007977134
inertia_kg_m2 = 0.239314
[ecu]
idle_base_opening = 0.05
idle_control = "feedback"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -5.0
idle_rpm = 1000.0
idle_spark_gain = 0.02
ignition_map = [[0.0, 10.0], [15000.0, 40.0]]
overrun_cut_rpm = 3500.0
overrun_fuel_cut_enabled = true
overrun_ignition_cut_enabled = false
overrun_resume_rpm = 2400.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = true
rev_limit_rpm = 7000.0
rev_resume_rpm = 6750.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 438.7423
clutch_engagement_rate = 8.0
drag_coefficient = 0.7
final_drive_ratio = 2.7
frontal_area = 0.65
gear_ratios = [4.1, 2.7, 2.0, 1.55, 1.25, 0.98]
gravity = 9.80665
mass_kg = 1400.0
primary_reduction = 1.8
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 1.2
wheel_radius_mm = 320.0
[fuel]
system = "injection"
[[pipes]]
id = "intake"
cells = 4
diameter_mm = 75.0
length_mm = 200.0
[[pipes]]
id = "runners"
count = 4
cells = 5
diameter_mm = 35.0
length_mm = 250.0
[[pipes]]
id = "primaries"
count = 4
cells = 13
diameter_mm = 38.0
length_mm = 650.0
[[pipes]]
id = "tailpipe"
cells = 30
diameter_mm = 60.0
length_mm = 1500.0

159
engines/f20.toml Normal file
View File

@@ -0,0 +1,159 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "f20"
name = "F20C-inspired / 2.0 high-rev four"
category = "Inline four / specials"
description = "Unequal primaries / small junctions / staged collectors / EFI"
order = 22
cycle = "four_stroke"
[engine]
bank_angle_deg = 60.0
collector_volume_litres = 1.0
cylinders = 4
initial_rpm = 1400.0
initialize_phased_charge = true
layout = "inline"
maximum_load_torque_nm = 30.0
minimum_substeps = 1
phase_degrees = [0.0, 180.0, 540.0, 360.0]
pipe_damping = 0.0
plenum_volume_litres = 4.0
starter_torque_nm = 49.9353
throttle_diameter_mm = 65.0
topology = "staged"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 87.0
burn_duration_deg = 60.0
burn_start_deg = 5.0
compression = 10.0
energy_variation = 0.05
exhaust_close_deg = 390.0
exhaust_open_deg = 130.0
exhaust_valve_count = 1
exhaust_valve_diameter_mm = 27.84
exhaust_valve_lift_mm = 7.83
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 590.0
intake_open_deg = 340.0
intake_valve_count = 1
intake_valve_diameter_mm = 33.06
intake_valve_lift_mm = 8.7
misfire_probability = 0.0
rod_mm = 151.2
stroke_mm = 84.0
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 7.989648
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.007989648
inertia_kg_m2 = 0.2396894
[ecu]
idle_base_opening = 0.05
idle_control = "feedback"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -5.0
idle_rpm = 1000.0
idle_spark_gain = 0.02
ignition_map = [[0.0, 10.0], [15000.0, 40.0]]
overrun_cut_rpm = 3500.0
overrun_fuel_cut_enabled = true
overrun_ignition_cut_enabled = false
overrun_resume_rpm = 2400.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = true
rev_limit_rpm = 9000.0
rev_resume_rpm = 8750.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 439.4306
clutch_engagement_rate = 8.0
drag_coefficient = 0.7
final_drive_ratio = 2.7
frontal_area = 0.65
gear_ratios = [4.1, 2.7, 2.0, 1.55, 1.25, 0.98]
gravity = 9.80665
mass_kg = 1400.0
primary_reduction = 1.8
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 1.2
wheel_radius_mm = 320.0
[fuel]
system = "injection"
[junctions]
h_pipe = true
junction_volume_cc = 50.0
[[pipes]]
id = "intake"
cells = 4
diameter_mm = 75.0
length_mm = 200.0
[[pipes]]
id = "runners"
count = 4
cells = 5
diameter_mm = 35.0
length_mm = 250.0
[[pipes]]
id = "primary_1"
cells = 11
diameter_mm = 38.0
length_mm = 520.0
[[pipes]]
id = "primary_2"
cells = 12
diameter_mm = 38.0
length_mm = 585.0
[[pipes]]
id = "primary_3"
cells = 13
diameter_mm = 38.0
length_mm = 650.0
[[pipes]]
id = "primary_4"
cells = 11
diameter_mm = 38.0
length_mm = 520.0
[[pipes]]
id = "tailpipe_9"
cells = 30
diameter_mm = 60.0
length_mm = 1500.0
[[pipes]]
id = "secondary_10"
cells = 9
diameter_mm = 48.0
length_mm = 420.0
[[pipes]]
id = "secondary_11"
cells = 11
diameter_mm = 48.0
length_mm = 540.0

68
engines/fifty.toml Normal file
View File

@@ -0,0 +1,68 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "fifty"
name = "50 cc / little screamer"
category = "Two-stroke / 50 cc"
description = "Concept single / transported carb fuel"
order = 15
cycle = "two_stroke"
[two_stroke]
initial_rpm = 3000.0
starter_torque_nm = 8.0
maximum_load_torque_nm = 10.0
crankshaft_inertia_kg_m2 = 0.004006997
bore_mm = 40.0
carburetor_diameter_mm = 19.0
crankcase_clearance_cc = 60.10495
exhaust = "racing"
exhaust_cell_length_mm = 15.0
exhaust_duration_deg = 190.0
exhaust_width_mm = 26.54048
friction_bar = 0.4
friction_bar_per_mps = 0.09
friction_bar_per_mps2 = 0.0009
idle_minimum_advance_deg = -10.0
idle_slide_opening = 0.045
idle_spark_gain = 0.05
jet_scale = 0.5
main_jet_scale = 1.6
minimum_burn_s = 0.0005897885
needle_jet_scale = 1.0
needle_low_scale = 2.72
needle_mid_scale = 0.8
needle_off_idle_scale = 12.0
needle_quarter_scale = 1.4
needle_tip_scale = 5.0
needle_transition_scale = 9.0
needle_upper_scale = 0.76
pilot_jet_scale = 0.7125
pilot_tip_scale = 1.0
pilot_transition_scale = 1.0
rod_mm = 81.09593
stroke_mm = 39.8
transfer_duration_deg = 130.0
transfer_width_mm = 36.86178
trapped_compression = 8.2
chamber_sections_mm = [[117.9577, 26.54048, 26.54048], [132.7024, 26.54048, 44.23414], [140.0748, 44.23414, 77.40974], [95.84063, 77.40974, 77.40974], [154.8195, 77.40974, 16.21918], [176.9366, 16.21918, 16.21918]]
[ecu]
idle_control = "fixed"
idle_rpm = 3000.0
idle_max_opening = 0.3
throttle_time_constant_s = 0.0002
throttle_rate = 5000.0
rev_limit_enabled = false
rev_limit_fuel_cut_enabled = false
overrun_cut_rpm = 5000.0
overrun_resume_rpm = 3500.0
overrun_fuel_cut_enabled = false
overrun_ignition_cut_enabled = true
ignition_map = [[0, 10], [3000, 18], [6000, 24], [9000, 18], [11000, 12], [13000, 6], [15000, 2], [18000, 0]]
[vehicle]
primary_reduction = 3.368421
final_drive_ratio = 3.769231
mass_kg = 170
wheel_radius_mm = 331.3
gear_ratios = [2.384615, 1.933333, 1.588235, 1.352941, 1.176471, 1.055556]

165
engines/five.toml Normal file
View File

@@ -0,0 +1,165 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "five"
name = "Audi-inspired / 2.2 inline five"
category = "Inline five"
description = "Unequal primaries / small junctions / staged collectors / EFI"
order = 23
cycle = "four_stroke"
[engine]
bank_angle_deg = 60.0
collector_volume_litres = 1.25
cylinders = 5
initial_rpm = 1400.0
initialize_phased_charge = true
layout = "inline"
maximum_load_torque_nm = 30.0
minimum_substeps = 1
phase_degrees = [0.0, 576.0, 144.0, 432.0, 288.0]
pipe_damping = 0.0
plenum_volume_litres = 5.0
starter_torque_nm = 55.65237
throttle_diameter_mm = 65.0
topology = "staged"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 81.0
burn_duration_deg = 60.0
burn_start_deg = 5.0
compression = 10.0
energy_variation = 0.05
exhaust_close_deg = 390.0
exhaust_open_deg = 130.0
exhaust_valve_count = 1
exhaust_valve_diameter_mm = 25.92
exhaust_valve_lift_mm = 7.29
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 590.0
intake_open_deg = 340.0
intake_valve_count = 1
intake_valve_diameter_mm = 30.78
intake_valve_lift_mm = 8.1
misfire_probability = 0.0
rod_mm = 155.52
stroke_mm = 86.4
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 8.90438
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.008904381
inertia_kg_m2 = 0.2671314
[ecu]
idle_base_opening = 0.05
idle_control = "feedback"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -5.0
idle_rpm = 1000.0
idle_spark_gain = 0.02
ignition_map = [[0.0, 10.0], [15000.0, 40.0]]
overrun_cut_rpm = 3500.0
overrun_fuel_cut_enabled = true
overrun_ignition_cut_enabled = false
overrun_resume_rpm = 2400.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = true
rev_limit_rpm = 7000.0
rev_resume_rpm = 6750.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 489.7409
clutch_engagement_rate = 8.0
drag_coefficient = 0.7
final_drive_ratio = 2.7
frontal_area = 0.65
gear_ratios = [4.1, 2.7, 2.0, 1.55, 1.25, 0.98]
gravity = 9.80665
mass_kg = 1400.0
primary_reduction = 1.8
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 1.2
wheel_radius_mm = 320.0
[fuel]
system = "injection"
[junctions]
h_pipe = true
junction_volume_cc = 50.0
[[pipes]]
id = "intake"
cells = 4
diameter_mm = 75.0
length_mm = 200.0
[[pipes]]
id = "runners"
count = 5
cells = 5
diameter_mm = 35.0
length_mm = 250.0
[[pipes]]
id = "primary_1"
cells = 11
diameter_mm = 38.0
length_mm = 520.0
[[pipes]]
id = "primary_2"
cells = 12
diameter_mm = 38.0
length_mm = 585.0
[[pipes]]
id = "primary_3"
cells = 13
diameter_mm = 38.0
length_mm = 650.0
[[pipes]]
id = "primary_4"
cells = 11
diameter_mm = 38.0
length_mm = 520.0
[[pipes]]
id = "primary_5"
cells = 12
diameter_mm = 38.0
length_mm = 585.0
[[pipes]]
id = "tailpipe_11"
cells = 30
diameter_mm = 60.0
length_mm = 1500.0
[[pipes]]
id = "secondary_12"
cells = 9
diameter_mm = 48.0
length_mm = 420.0
[[pipes]]
id = "secondary_13"
cells = 11
diameter_mm = 48.0
length_mm = 540.0

68
engines/fivehundred.toml Normal file
View File

@@ -0,0 +1,68 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "fivehundred"
name = "500 cc / big single"
category = "Two-stroke / 500 cc"
description = "Concept single / transported carb fuel"
order = 16
cycle = "two_stroke"
[two_stroke]
initial_rpm = 3000.0
starter_torque_nm = 8.0
maximum_load_torque_nm = 10.0
crankshaft_inertia_kg_m2 = 0.04002316
bore_mm = 86.0
carburetor_diameter_mm = 41.0
crankcase_clearance_cc = 600.3475
exhaust = "racing"
exhaust_cell_length_mm = 15.0
exhaust_duration_deg = 190.0
exhaust_width_mm = 57.15746
friction_bar = 0.4
friction_bar_per_mps = 0.09
friction_bar_per_mps2 = 0.0009
idle_minimum_advance_deg = -10.0
idle_slide_opening = 0.045
idle_spark_gain = 0.05
jet_scale = 1.0
main_jet_scale = 1.7504
minimum_burn_s = 0.001270166
needle_jet_scale = 1.06
needle_low_scale = 3.04
needle_mid_scale = 0.8
needle_off_idle_scale = 12.0
needle_quarter_scale = 1.4
needle_tip_scale = 5.0
needle_transition_scale = 9.0
needle_upper_scale = 0.76
pilot_jet_scale = 0.75
pilot_tip_scale = 1.0
pilot_transition_scale = 1.0
rod_mm = 174.6478
stroke_mm = 86.0
transfer_duration_deg = 130.0
transfer_width_mm = 79.38537
trapped_compression = 8.2
chamber_sections_mm = [[254.0332, 57.15746, 57.15746], [285.7873, 57.15746, 95.26245], [301.6644, 95.26245, 166.7093], [206.402, 166.7093, 166.7093], [333.4185, 166.7093, 34.92956], [381.0498, 34.92956, 34.92956]]
[ecu]
idle_control = "fixed"
idle_rpm = 3000.0
idle_max_opening = 0.3
throttle_time_constant_s = 0.0002
throttle_rate = 5000.0
rev_limit_enabled = false
rev_limit_fuel_cut_enabled = false
overrun_cut_rpm = 5000.0
overrun_resume_rpm = 3500.0
overrun_fuel_cut_enabled = false
overrun_ignition_cut_enabled = true
ignition_map = [[0, 10], [3000, 18], [6000, 24], [9000, 18], [11000, 12], [13000, 6], [15000, 2], [18000, 0]]
[vehicle]
primary_reduction = 3.368421
final_drive_ratio = 3.769231
mass_kg = 170
wheel_radius_mm = 331.3
gear_ratios = [2.384615, 1.933333, 1.588235, 1.352941, 1.176471, 1.055556]

126
engines/flat6.toml Normal file
View File

@@ -0,0 +1,126 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "flat6"
name = "911-inspired / 3.0 flat six"
category = "Boxer / flat six"
description = "Estimated concept calibration / EFI"
order = 28
cycle = "four_stroke"
[engine]
bank_angle_deg = 180.0
collector_volume_litres = 1.5
cylinders = 6
initial_rpm = 1400.0
initialize_phased_charge = true
layout = "boxer"
maximum_load_torque_nm = 30.0
minimum_substeps = 1
phase_degrees = [0.0, 600.0, 480.0, 360.0, 240.0, 120.0]
pipe_damping = 1.0
plenum_volume_litres = 6.0
starter_torque_nm = 74.85159
throttle_diameter_mm = 65.0
topology = "collector"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 95.0
burn_duration_deg = 60.0
burn_start_deg = 5.0
compression = 10.0
energy_variation = 0.05
exhaust_close_deg = 390.0
exhaust_open_deg = 130.0
exhaust_valve_count = 1
exhaust_valve_diameter_mm = 30.4
exhaust_valve_lift_mm = 8.55
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 590.0
intake_open_deg = 340.0
intake_valve_count = 1
intake_valve_diameter_mm = 36.1
intake_valve_lift_mm = 9.5
misfire_probability = 0.0
rod_mm = 126.72
stroke_mm = 70.4
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 11.97625
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.01197626
inertia_kg_m2 = 0.3592876
[ecu]
idle_base_opening = 0.05
idle_control = "feedback"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -5.0
idle_rpm = 1000.0
idle_spark_gain = 0.02
ignition_map = [[0.0, 10.0], [15000.0, 40.0]]
overrun_cut_rpm = 3500.0
overrun_fuel_cut_enabled = true
overrun_ignition_cut_enabled = false
overrun_resume_rpm = 2400.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = true
rev_limit_rpm = 7000.0
rev_resume_rpm = 6750.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 658.694
clutch_engagement_rate = 8.0
drag_coefficient = 0.7
final_drive_ratio = 2.7
frontal_area = 0.65
gear_ratios = [4.1, 2.7, 2.0, 1.55, 1.25, 0.98]
gravity = 9.80665
mass_kg = 1400.0
primary_reduction = 1.8
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 1.2
wheel_radius_mm = 320.0
[fuel]
system = "injection"
[[pipes]]
id = "intake"
cells = 4
diameter_mm = 75.0
length_mm = 200.0
[[pipes]]
id = "runners"
count = 6
cells = 5
diameter_mm = 35.0
length_mm = 250.0
[[pipes]]
id = "primaries"
count = 6
cells = 13
diameter_mm = 38.0
length_mm = 650.0
[[pipes]]
id = "tailpipe"
cells = 30
diameter_mm = 60.0
length_mm = 1500.0

126
engines/ltwin.toml Normal file
View File

@@ -0,0 +1,126 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "ltwin"
name = "916-inspired / 90-degree V twin"
category = "Twins / V"
description = "Estimated concept calibration / EFI"
order = 20
cycle = "four_stroke"
[engine]
bank_angle_deg = 90.0
collector_volume_litres = 0.5
cylinders = 2
initial_rpm = 1400.0
initialize_phased_charge = true
layout = "v"
maximum_load_torque_nm = 30.0
minimum_substeps = 1
phase_degrees = [0.0, 450.0]
pipe_damping = 1.0
plenum_volume_litres = 2.0
starter_torque_nm = 22.90127
throttle_diameter_mm = 65.0
topology = "collector"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 94.0
burn_duration_deg = 60.0
burn_start_deg = 5.0
compression = 10.0
energy_variation = 0.05
exhaust_close_deg = 390.0
exhaust_open_deg = 130.0
exhaust_valve_count = 1
exhaust_valve_diameter_mm = 30.08
exhaust_valve_lift_mm = 8.46
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 590.0
intake_open_deg = 340.0
intake_valve_count = 1
intake_valve_diameter_mm = 35.72
intake_valve_lift_mm = 9.4
misfire_probability = 0.0
rod_mm = 118.8
stroke_mm = 66.0
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 3.664203
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.003664203
inertia_kg_m2 = 0.1099261
[ecu]
idle_base_opening = 0.05
idle_control = "feedback"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -5.0
idle_rpm = 1000.0
idle_spark_gain = 0.02
ignition_map = [[0.0, 10.0], [15000.0, 40.0]]
overrun_cut_rpm = 3500.0
overrun_fuel_cut_enabled = true
overrun_ignition_cut_enabled = false
overrun_resume_rpm = 2400.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = true
rev_limit_rpm = 7000.0
rev_resume_rpm = 6750.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 201.5311
clutch_engagement_rate = 8.0
drag_coefficient = 0.7
final_drive_ratio = 2.7
frontal_area = 0.65
gear_ratios = [4.1, 2.7, 2.0, 1.55, 1.25, 0.98]
gravity = 9.80665
mass_kg = 240.0
primary_reduction = 1.8
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 1.2
wheel_radius_mm = 320.0
[fuel]
system = "injection"
[[pipes]]
id = "intake"
cells = 4
diameter_mm = 75.0
length_mm = 200.0
[[pipes]]
id = "runners"
count = 2
cells = 5
diameter_mm = 35.0
length_mm = 250.0
[[pipes]]
id = "primaries"
count = 2
cells = 13
diameter_mm = 38.0
length_mm = 650.0
[[pipes]]
id = "tailpipe"
cells = 30
diameter_mm = 60.0
length_mm = 1500.0

171
engines/rb26.toml Normal file
View File

@@ -0,0 +1,171 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "rb26"
name = "RB26-inspired / 2.6 inline six NA"
category = "Inline six"
description = "Unequal primaries / small junctions / staged collectors / EFI"
order = 24
cycle = "four_stroke"
[engine]
bank_angle_deg = 60.0
collector_volume_litres = 1.5
cylinders = 6
initial_rpm = 1400.0
initialize_phased_charge = true
layout = "inline"
maximum_load_torque_nm = 30.0
minimum_substeps = 1
phase_degrees = [0.0, 240.0, 480.0, 120.0, 600.0, 360.0]
pipe_damping = 0.0
plenum_volume_litres = 6.0
starter_torque_nm = 64.21635
throttle_diameter_mm = 65.0
topology = "staged"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 86.0
burn_duration_deg = 60.0
burn_start_deg = 5.0
compression = 10.0
energy_variation = 0.05
exhaust_close_deg = 390.0
exhaust_open_deg = 130.0
exhaust_valve_count = 1
exhaust_valve_diameter_mm = 27.52
exhaust_valve_lift_mm = 7.740001
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 590.0
intake_open_deg = 340.0
intake_valve_count = 1
intake_valve_diameter_mm = 32.68
intake_valve_lift_mm = 8.6
misfire_probability = 0.0
rod_mm = 132.66
stroke_mm = 73.7
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 10.27462
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.01027462
inertia_kg_m2 = 0.3082384
[ecu]
idle_base_opening = 0.05
idle_control = "feedback"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -5.0
idle_rpm = 1000.0
idle_spark_gain = 0.02
ignition_map = [[0.0, 10.0], [15000.0, 40.0]]
overrun_cut_rpm = 3500.0
overrun_fuel_cut_enabled = true
overrun_ignition_cut_enabled = false
overrun_resume_rpm = 2400.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = true
rev_limit_rpm = 7000.0
rev_resume_rpm = 6750.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 565.1038
clutch_engagement_rate = 8.0
drag_coefficient = 0.7
final_drive_ratio = 2.7
frontal_area = 0.65
gear_ratios = [4.1, 2.7, 2.0, 1.55, 1.25, 0.98]
gravity = 9.80665
mass_kg = 1400.0
primary_reduction = 1.8
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 1.2
wheel_radius_mm = 320.0
[fuel]
system = "injection"
[junctions]
h_pipe = true
junction_volume_cc = 50.0
[[pipes]]
id = "intake"
cells = 4
diameter_mm = 75.0
length_mm = 200.0
[[pipes]]
id = "runners"
count = 6
cells = 5
diameter_mm = 35.0
length_mm = 250.0
[[pipes]]
id = "primary_1"
cells = 11
diameter_mm = 38.0
length_mm = 520.0
[[pipes]]
id = "primary_2"
cells = 12
diameter_mm = 38.0
length_mm = 585.0
[[pipes]]
id = "primary_3"
cells = 13
diameter_mm = 38.0
length_mm = 650.0
[[pipes]]
id = "primary_4"
cells = 11
diameter_mm = 38.0
length_mm = 520.0
[[pipes]]
id = "primary_5"
cells = 12
diameter_mm = 38.0
length_mm = 585.0
[[pipes]]
id = "primary_6"
cells = 13
diameter_mm = 38.0
length_mm = 650.0
[[pipes]]
id = "tailpipe_13"
cells = 30
diameter_mm = 60.0
length_mm = 1500.0
[[pipes]]
id = "secondary_14"
cells = 9
diameter_mm = 48.0
length_mm = 420.0
[[pipes]]
id = "secondary_15"
cells = 11
diameter_mm = 48.0
length_mm = 540.0

152
engines/saab-chambered.toml Normal file
View File

@@ -0,0 +1,152 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "saab-chambered"
name = "Saab B205 / chambered"
category = "Inline four / Saab"
description = "Expansion resonator and two-chamber rear muffler"
order = 4
cycle = "four_stroke"
[engine]
bank_angle_deg = 56.0
collector_volume_litres = 0.6
cylinders = 4
initial_rpm = 1200.0
initialize_phased_charge = true
layout = "inline"
maximum_load_torque_nm = 200.0
minimum_substeps = 2
phase_degrees = [0.0, 180.0, 540.0, 360.0]
pipe_damping = 0.0
plenum_volume_litres = 3.0
starter_torque_nm = 65.0
throttle_diameter_mm = 52.0
topology = "collector"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 90.0
burn_duration_deg = 60.0
burn_start_deg = 5.0
compression = 8.8
energy_variation = 0.05
exhaust_close_deg = 390.0
exhaust_open_deg = 130.0
exhaust_valve_count = 2
exhaust_valve_diameter_mm = 28.0
exhaust_valve_lift_mm = 8.0
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 590.0
intake_open_deg = 340.0
intake_valve_count = 2
intake_valve_diameter_mm = 32.0
intake_valve_lift_mm = 8.5
misfire_probability = 0.0
rod_mm = 153.0
stroke_mm = 78.0
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 12.0
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.012
inertia_kg_m2 = 0.18
[ecu]
idle_base_opening = 0.08
idle_control = "feedback"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -10.0
idle_rpm = 850.0
idle_spark_gain = 0.025
ignition_map = [[0.0, 10.0], [15000.0, 40.0]]
overrun_cut_rpm = 2000.0
overrun_fuel_cut_enabled = true
overrun_ignition_cut_enabled = false
overrun_resume_rpm = 1300.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = true
rev_limit_rpm = 6500.0
rev_resume_rpm = 6250.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 300.0
clutch_engagement_rate = 8.0
drag_coefficient = 0.32
final_drive_ratio = 4.05
frontal_area = 2.2
gear_ratios = [3.382716, 1.758025, 1.177778, 0.8938271, 0.6592593]
gravity = 9.80665
mass_kg = 1675.0
primary_reduction = 1.0
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 3.6
wheel_radius_mm = 317.0
[fuel]
system = "injection"
[silencers]
cell_length_mm = 35.0
connecting_length_mm = 800.0
diameter_mm = 60.0
enabled = true
inlet_length_mm = 1400.0
outlet_length_mm = 350.0
[silencers.resonator]
chambers = 1
core_diameter_mm = 60.0
length_mm = 400.0
loss_coefficient = 0.1
passage_diameter_mm = 60.0
resistance = 2.0
type = "expansion"
volume_litres = 6.0
[silencers.muffler]
chambers = 2
core_diameter_mm = 60.0
length_mm = 650.0
loss_coefficient = 0.35
passage_diameter_mm = 60.0
resistance = 4.0
type = "chambered"
volume_litres = 18.0
[[pipes]]
id = "intake"
cells = 8
diameter_mm = 60.0
length_mm = 150.0
[[pipes]]
id = "runners"
count = 4
cells = 10
diameter_mm = 32.0
length_mm = 250.0
[[pipes]]
id = "primaries"
count = 4
cells = 10
diameter_mm = 34.0
length_mm = 280.0
[[pipes]]
id = "tailpipe"
profile = "silenced"

152
engines/saab-expansion.toml Normal file
View File

@@ -0,0 +1,152 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "saab-expansion"
name = "Saab B205 / expansion silencers"
category = "Inline four / Saab"
description = "Two single expansion chambers, same body volumes"
order = 5
cycle = "four_stroke"
[engine]
bank_angle_deg = 56.0
collector_volume_litres = 0.6
cylinders = 4
initial_rpm = 1200.0
initialize_phased_charge = true
layout = "inline"
maximum_load_torque_nm = 200.0
minimum_substeps = 2
phase_degrees = [0.0, 180.0, 540.0, 360.0]
pipe_damping = 0.0
plenum_volume_litres = 3.0
starter_torque_nm = 65.0
throttle_diameter_mm = 52.0
topology = "collector"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 90.0
burn_duration_deg = 60.0
burn_start_deg = 5.0
compression = 8.8
energy_variation = 0.05
exhaust_close_deg = 390.0
exhaust_open_deg = 130.0
exhaust_valve_count = 2
exhaust_valve_diameter_mm = 28.0
exhaust_valve_lift_mm = 8.0
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 590.0
intake_open_deg = 340.0
intake_valve_count = 2
intake_valve_diameter_mm = 32.0
intake_valve_lift_mm = 8.5
misfire_probability = 0.0
rod_mm = 153.0
stroke_mm = 78.0
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 12.0
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.012
inertia_kg_m2 = 0.18
[ecu]
idle_base_opening = 0.08
idle_control = "feedback"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -10.0
idle_rpm = 850.0
idle_spark_gain = 0.025
ignition_map = [[0.0, 10.0], [15000.0, 40.0]]
overrun_cut_rpm = 2000.0
overrun_fuel_cut_enabled = true
overrun_ignition_cut_enabled = false
overrun_resume_rpm = 1300.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = true
rev_limit_rpm = 6500.0
rev_resume_rpm = 6250.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 300.0
clutch_engagement_rate = 8.0
drag_coefficient = 0.32
final_drive_ratio = 4.05
frontal_area = 2.2
gear_ratios = [3.382716, 1.758025, 1.177778, 0.8938271, 0.6592593]
gravity = 9.80665
mass_kg = 1675.0
primary_reduction = 1.0
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 3.6
wheel_radius_mm = 317.0
[fuel]
system = "injection"
[silencers]
cell_length_mm = 35.0
connecting_length_mm = 800.0
diameter_mm = 60.0
enabled = true
inlet_length_mm = 1400.0
outlet_length_mm = 350.0
[silencers.resonator]
chambers = 1
core_diameter_mm = 60.0
length_mm = 400.0
loss_coefficient = 0.1
passage_diameter_mm = 60.0
resistance = 2.0
type = "expansion"
volume_litres = 6.0
[silencers.muffler]
chambers = 1
core_diameter_mm = 60.0
length_mm = 650.0
loss_coefficient = 0.35
passage_diameter_mm = 60.0
resistance = 4.0
type = "expansion"
volume_litres = 18.0
[[pipes]]
id = "intake"
cells = 8
diameter_mm = 60.0
length_mm = 150.0
[[pipes]]
id = "runners"
count = 4
cells = 10
diameter_mm = 32.0
length_mm = 250.0
[[pipes]]
id = "primaries"
count = 4
cells = 10
diameter_mm = 34.0
length_mm = 280.0
[[pipes]]
id = "tailpipe"
profile = "silenced"

152
engines/saab-open.toml Normal file
View File

@@ -0,0 +1,152 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "saab-open"
name = "Saab B205 / open exhaust"
category = "Inline four / Saab"
description = "Silencers bypassed; naturally aspirated baseline"
order = 6
cycle = "four_stroke"
[engine]
bank_angle_deg = 56.0
collector_volume_litres = 0.6
cylinders = 4
initial_rpm = 1200.0
initialize_phased_charge = true
layout = "inline"
maximum_load_torque_nm = 200.0
minimum_substeps = 2
phase_degrees = [0.0, 180.0, 540.0, 360.0]
pipe_damping = 0.0
plenum_volume_litres = 3.0
starter_torque_nm = 65.0
throttle_diameter_mm = 52.0
topology = "collector"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 90.0
burn_duration_deg = 60.0
burn_start_deg = 5.0
compression = 8.8
energy_variation = 0.05
exhaust_close_deg = 390.0
exhaust_open_deg = 130.0
exhaust_valve_count = 2
exhaust_valve_diameter_mm = 28.0
exhaust_valve_lift_mm = 8.0
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 590.0
intake_open_deg = 340.0
intake_valve_count = 2
intake_valve_diameter_mm = 32.0
intake_valve_lift_mm = 8.5
misfire_probability = 0.0
rod_mm = 153.0
stroke_mm = 78.0
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 12.0
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.012
inertia_kg_m2 = 0.18
[ecu]
idle_base_opening = 0.08
idle_control = "feedback"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -10.0
idle_rpm = 850.0
idle_spark_gain = 0.025
ignition_map = [[0.0, 10.0], [15000.0, 40.0]]
overrun_cut_rpm = 2000.0
overrun_fuel_cut_enabled = true
overrun_ignition_cut_enabled = false
overrun_resume_rpm = 1300.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = true
rev_limit_rpm = 6500.0
rev_resume_rpm = 6250.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 300.0
clutch_engagement_rate = 8.0
drag_coefficient = 0.32
final_drive_ratio = 4.05
frontal_area = 2.2
gear_ratios = [3.382716, 1.758025, 1.177778, 0.8938271, 0.6592593]
gravity = 9.80665
mass_kg = 1675.0
primary_reduction = 1.0
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 3.6
wheel_radius_mm = 317.0
[fuel]
system = "injection"
[silencers]
cell_length_mm = 35.0
connecting_length_mm = 800.0
diameter_mm = 60.0
enabled = false
inlet_length_mm = 1400.0
outlet_length_mm = 350.0
[silencers.resonator]
chambers = 1
core_diameter_mm = 60.0
length_mm = 400.0
loss_coefficient = 0.1
passage_diameter_mm = 60.0
resistance = 2.0
type = "straight_through"
volume_litres = 6.0
[silencers.muffler]
chambers = 1
core_diameter_mm = 60.0
length_mm = 650.0
loss_coefficient = 0.35
passage_diameter_mm = 60.0
resistance = 4.0
type = "straight_through"
volume_litres = 18.0
[[pipes]]
id = "intake"
cells = 8
diameter_mm = 60.0
length_mm = 150.0
[[pipes]]
id = "runners"
count = 4
cells = 10
diameter_mm = 32.0
length_mm = 250.0
[[pipes]]
id = "primaries"
count = 4
cells = 10
diameter_mm = 34.0
length_mm = 280.0
[[pipes]]
id = "tailpipe"
profile = "silenced"

152
engines/saab-straight.toml Normal file
View File

@@ -0,0 +1,152 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "saab-straight"
name = "Saab B205 / straight-through"
category = "Inline four / Saab"
description = "6 L resonator and 18 L straight-through rear muffler"
order = 3
cycle = "four_stroke"
[engine]
bank_angle_deg = 56.0
collector_volume_litres = 0.6
cylinders = 4
initial_rpm = 1200.0
initialize_phased_charge = true
layout = "inline"
maximum_load_torque_nm = 200.0
minimum_substeps = 2
phase_degrees = [0.0, 180.0, 540.0, 360.0]
pipe_damping = 0.0
plenum_volume_litres = 3.0
starter_torque_nm = 65.0
throttle_diameter_mm = 52.0
topology = "collector"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 90.0
burn_duration_deg = 60.0
burn_start_deg = 5.0
compression = 8.8
energy_variation = 0.05
exhaust_close_deg = 390.0
exhaust_open_deg = 130.0
exhaust_valve_count = 2
exhaust_valve_diameter_mm = 28.0
exhaust_valve_lift_mm = 8.0
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 590.0
intake_open_deg = 340.0
intake_valve_count = 2
intake_valve_diameter_mm = 32.0
intake_valve_lift_mm = 8.5
misfire_probability = 0.0
rod_mm = 153.0
stroke_mm = 78.0
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 12.0
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.012
inertia_kg_m2 = 0.18
[ecu]
idle_base_opening = 0.08
idle_control = "feedback"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -10.0
idle_rpm = 850.0
idle_spark_gain = 0.025
ignition_map = [[0.0, 10.0], [15000.0, 40.0]]
overrun_cut_rpm = 2000.0
overrun_fuel_cut_enabled = true
overrun_ignition_cut_enabled = false
overrun_resume_rpm = 1300.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = true
rev_limit_rpm = 6500.0
rev_resume_rpm = 6250.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 300.0
clutch_engagement_rate = 8.0
drag_coefficient = 0.32
final_drive_ratio = 4.05
frontal_area = 2.2
gear_ratios = [3.382716, 1.758025, 1.177778, 0.8938271, 0.6592593]
gravity = 9.80665
mass_kg = 1675.0
primary_reduction = 1.0
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 3.6
wheel_radius_mm = 317.0
[fuel]
system = "injection"
[silencers]
cell_length_mm = 35.0
connecting_length_mm = 800.0
diameter_mm = 60.0
enabled = true
inlet_length_mm = 1400.0
outlet_length_mm = 350.0
[silencers.resonator]
chambers = 1
core_diameter_mm = 60.0
length_mm = 400.0
loss_coefficient = 0.1
passage_diameter_mm = 60.0
resistance = 2.0
type = "straight_through"
volume_litres = 6.0
[silencers.muffler]
chambers = 1
core_diameter_mm = 60.0
length_mm = 650.0
loss_coefficient = 0.35
passage_diameter_mm = 60.0
resistance = 4.0
type = "straight_through"
volume_litres = 18.0
[[pipes]]
id = "intake"
cells = 8
diameter_mm = 60.0
length_mm = 150.0
[[pipes]]
id = "runners"
count = 4
cells = 10
diameter_mm = 32.0
length_mm = 250.0
[[pipes]]
id = "primaries"
count = 4
cells = 10
diameter_mm = 34.0
length_mm = 280.0
[[pipes]]
id = "tailpipe"
profile = "silenced"

136
engines/single-450.toml Normal file
View File

@@ -0,0 +1,136 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "single-450"
name = "450 cc / carbureted four-stroke thumper"
category = "Singles / four-stroke"
description = "40 mm carb / mild valve timing / fixed idle stop"
order = 17
cycle = "four_stroke"
[engine]
bank_angle_deg = 60.0
collector_volume_litres = 0.25
cylinders = 1
initial_rpm = 1960.0
initialize_phased_charge = true
layout = "inline"
maximum_load_torque_nm = 30.0
minimum_substeps = 1
phase_degrees = [0.0]
pipe_damping = 1.0
plenum_volume_litres = 1.0
starter_torque_nm = 11.21926
throttle_diameter_mm = 40.0
topology = "collector"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 96.0
burn_duration_deg = 60.0
burn_start_deg = 5.0
compression = 10.0
energy_variation = 0.05
exhaust_close_deg = 370.0
exhaust_open_deg = 145.0
exhaust_valve_count = 1
exhaust_valve_diameter_mm = 30.72
exhaust_valve_lift_mm = 8.640001
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 565.0
intake_open_deg = 355.0
intake_valve_count = 1
intake_valve_diameter_mm = 36.48
intake_valve_lift_mm = 9.6
misfire_probability = 0.0
rod_mm = 111.6
stroke_mm = 62.0
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 1.795081
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.001795081
inertia_kg_m2 = 0.05385243
[ecu]
idle_base_opening = 0.045
idle_control = "fixed"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -5.0
idle_rpm = 1400.0
idle_spark_gain = 0.02
ignition_map = [[0.0, 10.0], [15000.0, 40.0]]
overrun_cut_rpm = 3500.0
overrun_fuel_cut_enabled = false
overrun_ignition_cut_enabled = true
overrun_resume_rpm = 2400.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = false
rev_limit_rpm = 7000.0
rev_resume_rpm = 6750.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 98.72945
clutch_engagement_rate = 8.0
drag_coefficient = 0.7
final_drive_ratio = 2.7
frontal_area = 0.65
gear_ratios = [4.1, 2.7, 2.0, 1.55, 1.25, 0.98]
gravity = 9.80665
mass_kg = 240.0
primary_reduction = 1.8
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 1.2
wheel_radius_mm = 320.0
[fuel]
system = "carburetor"
pilot_jet_diameter_mm = 0.09349564
main_jet_diameter_mm = 2.091829
needle_jet_diameter_mm = 2.15284
needle_low_scale = 0.944
needle_mid_scale = 0.564
needle_off_idle_scale = 0.995
needle_quarter_scale = 0.835
needle_tip_scale = 0.97
needle_transition_scale = 1.6
needle_upper_scale = 0.65
bowl_capacity_kg = 0.1
bowl_refill_kg_per_s = 0.1
[[pipes]]
id = "intake"
cells = 4
diameter_mm = 75.0
length_mm = 200.0
[[pipes]]
id = "runners"
cells = 5
diameter_mm = 35.0
length_mm = 250.0
[[pipes]]
id = "primaries"
cells = 13
diameter_mm = 38.0
length_mm = 650.0
[[pipes]]
id = "tailpipe"
cells = 30
diameter_mm = 60.0
length_mm = 1500.0

124
engines/single-650.toml Normal file
View File

@@ -0,0 +1,124 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "single-650"
name = "650 cc / big thumper"
category = "Singles / four-stroke"
description = "Estimated concept calibration / EFI"
order = 18
cycle = "four_stroke"
[engine]
bank_angle_deg = 60.0
collector_volume_litres = 0.25
cylinders = 1
initial_rpm = 1400.0
initialize_phased_charge = true
layout = "inline"
maximum_load_torque_nm = 30.0
minimum_substeps = 1
phase_degrees = [0.0]
pipe_damping = 1.0
plenum_volume_litres = 1.0
starter_torque_nm = 16.29701
throttle_diameter_mm = 65.0
topology = "collector"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 100.0
burn_duration_deg = 60.0
burn_start_deg = 5.0
compression = 10.0
energy_variation = 0.05
exhaust_close_deg = 390.0
exhaust_open_deg = 130.0
exhaust_valve_count = 1
exhaust_valve_diameter_mm = 32.0
exhaust_valve_lift_mm = 9.000001
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 590.0
intake_open_deg = 340.0
intake_valve_count = 1
intake_valve_diameter_mm = 38.0
intake_valve_lift_mm = 10.0
misfire_probability = 0.0
rod_mm = 149.4
stroke_mm = 83.0
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 2.607522
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.002607522
inertia_kg_m2 = 0.07822565
[ecu]
idle_base_opening = 0.05
idle_control = "feedback"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -5.0
idle_rpm = 1000.0
idle_spark_gain = 0.02
ignition_map = [[0.0, 10.0], [15000.0, 40.0]]
overrun_cut_rpm = 3500.0
overrun_fuel_cut_enabled = true
overrun_ignition_cut_enabled = false
overrun_resume_rpm = 2400.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = true
rev_limit_rpm = 6500.0
rev_resume_rpm = 6250.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 143.4137
clutch_engagement_rate = 8.0
drag_coefficient = 0.7
final_drive_ratio = 2.7
frontal_area = 0.65
gear_ratios = [4.1, 2.7, 2.0, 1.55, 1.25, 0.98]
gravity = 9.80665
mass_kg = 240.0
primary_reduction = 1.8
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 1.2
wheel_radius_mm = 320.0
[fuel]
system = "injection"
[[pipes]]
id = "intake"
cells = 4
diameter_mm = 75.0
length_mm = 200.0
[[pipes]]
id = "runners"
cells = 5
diameter_mm = 35.0
length_mm = 250.0
[[pipes]]
id = "primaries"
cells = 13
diameter_mm = 38.0
length_mm = 650.0
[[pipes]]
id = "tailpipe"
cells = 30
diameter_mm = 60.0
length_mm = 1500.0

171
engines/six-long.toml Normal file
View File

@@ -0,0 +1,171 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "six-long"
name = "3.0 litre / long-stroke inline six"
category = "Inline six"
description = "Unequal primaries / small junctions / staged collectors / EFI"
order = 25
cycle = "four_stroke"
[engine]
bank_angle_deg = 60.0
collector_volume_litres = 1.5
cylinders = 6
initial_rpm = 1400.0
initialize_phased_charge = true
layout = "inline"
maximum_load_torque_nm = 30.0
minimum_substeps = 1
phase_degrees = [0.0, 240.0, 480.0, 120.0, 600.0, 360.0]
pipe_damping = 0.0
plenum_volume_litres = 6.0
starter_torque_nm = 74.93359
throttle_diameter_mm = 65.0
topology = "staged"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 86.0
burn_duration_deg = 60.0
burn_start_deg = 5.0
compression = 10.0
energy_variation = 0.05
exhaust_close_deg = 390.0
exhaust_open_deg = 130.0
exhaust_valve_count = 1
exhaust_valve_diameter_mm = 27.52
exhaust_valve_lift_mm = 7.740001
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 590.0
intake_open_deg = 340.0
intake_valve_count = 1
intake_valve_diameter_mm = 32.68
intake_valve_lift_mm = 8.6
misfire_probability = 0.0
rod_mm = 154.8
stroke_mm = 86.0
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 11.98938
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.01198938
inertia_kg_m2 = 0.3596812
[ecu]
idle_base_opening = 0.05
idle_control = "feedback"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -5.0
idle_rpm = 1000.0
idle_spark_gain = 0.02
ignition_map = [[0.0, 10.0], [15000.0, 40.0]]
overrun_cut_rpm = 3500.0
overrun_fuel_cut_enabled = true
overrun_ignition_cut_enabled = false
overrun_resume_rpm = 2400.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = true
rev_limit_rpm = 7000.0
rev_resume_rpm = 6750.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 659.4156
clutch_engagement_rate = 8.0
drag_coefficient = 0.7
final_drive_ratio = 2.7
frontal_area = 0.65
gear_ratios = [4.1, 2.7, 2.0, 1.55, 1.25, 0.98]
gravity = 9.80665
mass_kg = 1400.0
primary_reduction = 1.8
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 1.2
wheel_radius_mm = 320.0
[fuel]
system = "injection"
[junctions]
h_pipe = true
junction_volume_cc = 50.0
[[pipes]]
id = "intake"
cells = 4
diameter_mm = 75.0
length_mm = 200.0
[[pipes]]
id = "runners"
count = 6
cells = 5
diameter_mm = 35.0
length_mm = 250.0
[[pipes]]
id = "primary_1"
cells = 11
diameter_mm = 38.0
length_mm = 520.0
[[pipes]]
id = "primary_2"
cells = 12
diameter_mm = 38.0
length_mm = 585.0
[[pipes]]
id = "primary_3"
cells = 13
diameter_mm = 38.0
length_mm = 650.0
[[pipes]]
id = "primary_4"
cells = 11
diameter_mm = 38.0
length_mm = 520.0
[[pipes]]
id = "primary_5"
cells = 12
diameter_mm = 38.0
length_mm = 585.0
[[pipes]]
id = "primary_6"
cells = 13
diameter_mm = 38.0
length_mm = 650.0
[[pipes]]
id = "tailpipe_13"
cells = 30
diameter_mm = 60.0
length_mm = 1500.0
[[pipes]]
id = "secondary_14"
cells = 9
diameter_mm = 48.0
length_mm = 420.0
[[pipes]]
id = "secondary_15"
cells = 11
diameter_mm = 48.0
length_mm = 540.0

147
engines/triple.toml Normal file
View File

@@ -0,0 +1,147 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "triple"
name = "1.0 litre / inline triple"
category = "Inline three"
description = "Unequal primaries / small junctions / staged collectors / EFI"
order = 21
cycle = "four_stroke"
[engine]
bank_angle_deg = 60.0
collector_volume_litres = 0.75
cylinders = 3
initial_rpm = 1400.0
initialize_phased_charge = true
layout = "inline"
maximum_load_torque_nm = 30.0
minimum_substeps = 1
phase_degrees = [0.0, 480.0, 240.0]
pipe_damping = 0.0
plenum_volume_litres = 3.0
starter_torque_nm = 24.99852
throttle_diameter_mm = 65.0
topology = "staged"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 79.0
burn_duration_deg = 60.0
burn_start_deg = 5.0
compression = 10.0
energy_variation = 0.05
exhaust_close_deg = 390.0
exhaust_open_deg = 130.0
exhaust_valve_count = 1
exhaust_valve_diameter_mm = 25.28
exhaust_valve_lift_mm = 7.110001
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 590.0
intake_open_deg = 340.0
intake_valve_count = 1
intake_valve_diameter_mm = 30.02
intake_valve_lift_mm = 7.900001
misfire_probability = 0.0
rod_mm = 122.4
stroke_mm = 68.0
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 3.999763
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.003999764
inertia_kg_m2 = 0.1199929
[ecu]
idle_base_opening = 0.05
idle_control = "feedback"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -5.0
idle_rpm = 1000.0
idle_spark_gain = 0.02
ignition_map = [[0.0, 10.0], [15000.0, 40.0]]
overrun_cut_rpm = 3500.0
overrun_fuel_cut_enabled = true
overrun_ignition_cut_enabled = false
overrun_resume_rpm = 2400.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = true
rev_limit_rpm = 7000.0
rev_resume_rpm = 6750.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 219.987
clutch_engagement_rate = 8.0
drag_coefficient = 0.7
final_drive_ratio = 2.7
frontal_area = 0.65
gear_ratios = [4.1, 2.7, 2.0, 1.55, 1.25, 0.98]
gravity = 9.80665
mass_kg = 1400.0
primary_reduction = 1.8
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 1.2
wheel_radius_mm = 320.0
[fuel]
system = "injection"
[junctions]
h_pipe = true
junction_volume_cc = 50.0
[[pipes]]
id = "intake"
cells = 4
diameter_mm = 75.0
length_mm = 200.0
[[pipes]]
id = "runners"
count = 3
cells = 5
diameter_mm = 35.0
length_mm = 250.0
[[pipes]]
id = "primary_1"
cells = 11
diameter_mm = 38.0
length_mm = 520.0
[[pipes]]
id = "primary_2"
cells = 12
diameter_mm = 38.0
length_mm = 585.0
[[pipes]]
id = "primary_3"
cells = 13
diameter_mm = 38.0
length_mm = 650.0
[[pipes]]
id = "tailpipe_7"
cells = 30
diameter_mm = 60.0
length_mm = 1500.0
[[pipes]]
id = "secondary_8"
cells = 9
diameter_mm = 48.0
length_mm = 420.0

126
engines/twin-270.toml Normal file
View File

@@ -0,0 +1,126 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "twin-270"
name = "700 cc / 270-degree parallel twin"
category = "Twins / parallel"
description = "Estimated concept calibration / EFI"
order = 19
cycle = "four_stroke"
[engine]
bank_angle_deg = 60.0
collector_volume_litres = 0.5
cylinders = 2
initial_rpm = 1400.0
initialize_phased_charge = true
layout = "inline"
maximum_load_torque_nm = 30.0
minimum_substeps = 1
phase_degrees = [0.0, 450.0]
pipe_damping = 1.0
plenum_volume_litres = 2.0
starter_torque_nm = 17.09026
throttle_diameter_mm = 65.0
topology = "collector"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 80.0
burn_duration_deg = 60.0
burn_start_deg = 5.0
compression = 10.0
energy_variation = 0.05
exhaust_close_deg = 390.0
exhaust_open_deg = 130.0
exhaust_valve_count = 1
exhaust_valve_diameter_mm = 25.6
exhaust_valve_lift_mm = 7.2
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 590.0
intake_open_deg = 340.0
intake_valve_count = 1
intake_valve_diameter_mm = 30.4
intake_valve_lift_mm = 8.0
misfire_probability = 0.0
rod_mm = 122.4
stroke_mm = 68.0
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 2.734442
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.002734442
inertia_kg_m2 = 0.08203326
[ecu]
idle_base_opening = 0.05
idle_control = "feedback"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -5.0
idle_rpm = 1000.0
idle_spark_gain = 0.02
ignition_map = [[0.0, 10.0], [15000.0, 40.0]]
overrun_cut_rpm = 3500.0
overrun_fuel_cut_enabled = true
overrun_ignition_cut_enabled = false
overrun_resume_rpm = 2400.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = true
rev_limit_rpm = 7000.0
rev_resume_rpm = 6750.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 150.3943
clutch_engagement_rate = 8.0
drag_coefficient = 0.7
final_drive_ratio = 2.7
frontal_area = 0.65
gear_ratios = [4.1, 2.7, 2.0, 1.55, 1.25, 0.98]
gravity = 9.80665
mass_kg = 240.0
primary_reduction = 1.8
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 1.2
wheel_radius_mm = 320.0
[fuel]
system = "injection"
[[pipes]]
id = "intake"
cells = 4
diameter_mm = 75.0
length_mm = 200.0
[[pipes]]
id = "runners"
count = 2
cells = 5
diameter_mm = 35.0
length_mm = 250.0
[[pipes]]
id = "primaries"
count = 2
cells = 13
diameter_mm = 38.0
length_mm = 650.0
[[pipes]]
id = "tailpipe"
cells = 30
diameter_mm = 60.0
length_mm = 1500.0

126
engines/twin.toml Normal file
View File

@@ -0,0 +1,126 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "twin"
name = "600 cc parallel twin"
category = "Twins / parallel"
description = "EFI motorcycle baseline"
order = 0
cycle = "four_stroke"
[engine]
bank_angle_deg = 56.0
collector_volume_litres = 0.15
cylinders = 2
initial_rpm = 2000.0
initialize_phased_charge = false
layout = "inline"
maximum_load_torque_nm = 30.0
minimum_substeps = 1
phase_degrees = [0.0, 180.0]
pipe_damping = 1.0
plenum_volume_litres = 1.0
starter_torque_nm = 18.0
throttle_diameter_mm = 26.0
topology = "collector"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 75.0
burn_duration_deg = 60.0
burn_start_deg = 5.0
compression = 11.5
energy_variation = 0.05
exhaust_close_deg = 390.0
exhaust_open_deg = 130.0
exhaust_valve_count = 1
exhaust_valve_diameter_mm = 26.0
exhaust_valve_lift_mm = 7.5
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 590.0
intake_open_deg = 340.0
intake_valve_count = 1
intake_valve_diameter_mm = 30.0
intake_valve_lift_mm = 8.0
misfire_probability = 0.0
rod_mm = 112.0
stroke_mm = 68.0
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 5.0
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.003
inertia_kg_m2 = 0.04
[ecu]
idle_base_opening = 0.08
idle_control = "feedback"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -5.0
idle_rpm = 1800.0
idle_spark_gain = 0.0
ignition_map = [[0.0, 10.0], [15000.0, 40.0]]
overrun_cut_rpm = 3500.0
overrun_fuel_cut_enabled = true
overrun_ignition_cut_enabled = false
overrun_resume_rpm = 2400.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = true
rev_limit_rpm = 10000.0
rev_resume_rpm = 9750.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 80.0
clutch_engagement_rate = 8.0
drag_coefficient = 0.7
final_drive_ratio = 2.7
frontal_area = 0.65
gear_ratios = [4.1, 2.7, 2.0, 1.55, 1.25, 0.98]
gravity = 9.80665
mass_kg = 160.0
primary_reduction = 1.8
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 1.2
wheel_radius_mm = 320.0
[fuel]
system = "injection"
[[pipes]]
id = "intake"
cells = 10
diameter_mm = 27.5
length_mm = 60.0
[[pipes]]
id = "runners"
count = 2
cells = 10
diameter_mm = 27.5
length_mm = 120.0
[[pipes]]
id = "primaries"
count = 2
cells = 6
diameter_mm = 28.0
length_mm = 200.0
[[pipes]]
id = "tailpipe"
cells = 10
diameter_mm = 28.0
length_mm = 400.0

201
engines/v10.toml Normal file
View File

@@ -0,0 +1,201 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "v10"
name = "5.0 litre / even-fire V10"
category = "V10"
description = "Separate five-cylinder banks / junction collectors / dual outlets"
order = 30
cycle = "four_stroke"
[engine]
bank_angle_deg = 72.0
collector_volume_litres = 2.5
cylinders = 10
initial_rpm = 1400.0
initialize_phased_charge = true
layout = "v"
maximum_load_torque_nm = 30.0
minimum_substeps = 1
phase_degrees = [0.0, 576.0, 432.0, 288.0, 144.0, 648.0001, 504.0, 360.0, 216.0, 72.00003]
pipe_damping = 0.0
plenum_volume_litres = 10.0
starter_torque_nm = 124.9751
throttle_diameter_mm = 65.0
topology = "staged"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 92.0
burn_duration_deg = 60.0
burn_start_deg = 5.0
compression = 10.0
energy_variation = 0.05
exhaust_close_deg = 390.0
exhaust_open_deg = 130.0
exhaust_valve_count = 1
exhaust_valve_diameter_mm = 29.44
exhaust_valve_lift_mm = 8.280001
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 590.0
intake_open_deg = 340.0
intake_valve_count = 1
intake_valve_diameter_mm = 34.96
intake_valve_lift_mm = 9.2
misfire_probability = 0.0
rod_mm = 135.36
stroke_mm = 75.2
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 19.99601
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.01999602
inertia_kg_m2 = 0.5998804
[ecu]
idle_base_opening = 0.05
idle_control = "feedback"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -5.0
idle_rpm = 1000.0
idle_spark_gain = 0.02
ignition_map = [[0.0, 10.0], [15000.0, 40.0]]
overrun_cut_rpm = 3500.0
overrun_fuel_cut_enabled = true
overrun_ignition_cut_enabled = false
overrun_resume_rpm = 2400.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = true
rev_limit_rpm = 7000.0
rev_resume_rpm = 6750.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 1099.781
clutch_engagement_rate = 8.0
drag_coefficient = 0.7
final_drive_ratio = 2.7
frontal_area = 0.65
gear_ratios = [4.1, 2.7, 2.0, 1.55, 1.25, 0.98]
gravity = 9.80665
mass_kg = 1400.0
primary_reduction = 1.8
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 1.2
wheel_radius_mm = 320.0
[fuel]
system = "injection"
[junctions]
h_pipe = true
junction_volume_cc = 50.0
[[pipes]]
id = "intake"
cells = 4
diameter_mm = 75.0
length_mm = 200.0
[[pipes]]
id = "runners"
count = 10
cells = 5
diameter_mm = 35.0
length_mm = 250.0
[[pipes]]
id = "primary_1"
cells = 11
diameter_mm = 38.0
length_mm = 520.0
[[pipes]]
id = "primary_2"
cells = 12
diameter_mm = 38.0
length_mm = 585.0
[[pipes]]
id = "primary_3"
cells = 13
diameter_mm = 38.0
length_mm = 650.0
[[pipes]]
id = "primary_4"
cells = 11
diameter_mm = 38.0
length_mm = 520.0
[[pipes]]
id = "primary_5"
cells = 12
diameter_mm = 38.0
length_mm = 585.0
[[pipes]]
id = "primary_6"
cells = 13
diameter_mm = 38.0
length_mm = 650.0
[[pipes]]
id = "primary_7"
cells = 11
diameter_mm = 38.0
length_mm = 520.0
[[pipes]]
id = "primary_8"
cells = 12
diameter_mm = 38.0
length_mm = 585.0
[[pipes]]
id = "primary_9"
cells = 13
diameter_mm = 38.0
length_mm = 650.0
[[pipes]]
id = "primary_10"
cells = 11
diameter_mm = 38.0
length_mm = 520.0
[[pipes]]
id = "tailpipe_21"
cells = 30
diameter_mm = 60.0
length_mm = 1500.0
[[pipes]]
id = "tailpipe_22"
cells = 28
diameter_mm = 60.0
length_mm = 1380.0
[[pipes]]
id = "secondary_23"
cells = 9
diameter_mm = 48.0
length_mm = 420.0
[[pipes]]
id = "secondary_24"
cells = 11
diameter_mm = 48.0
length_mm = 540.0

213
engines/v12.toml Normal file
View File

@@ -0,0 +1,213 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "v12"
name = "250-inspired / 3.0 V12"
category = "V12"
description = "Separate six-cylinder banks / junction collectors / dual outlets"
order = 31
cycle = "four_stroke"
[engine]
bank_angle_deg = 60.0
collector_volume_litres = 3.0
cylinders = 12
initial_rpm = 1400.0
initialize_phased_charge = true
layout = "v"
maximum_load_torque_nm = 30.0
minimum_substeps = 1
phase_degrees = [0.0, 600.0, 480.0, 360.0, 240.0, 120.0, 660.0, 540.0, 420.0, 300.0, 180.0, 59.99998]
pipe_damping = 0.0
plenum_volume_litres = 12.0
starter_torque_nm = 73.83022
throttle_diameter_mm = 65.0
topology = "staged"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 73.0
burn_duration_deg = 60.0
burn_start_deg = 5.0
compression = 10.0
energy_variation = 0.05
exhaust_close_deg = 390.0
exhaust_open_deg = 130.0
exhaust_valve_count = 1
exhaust_valve_diameter_mm = 23.36
exhaust_valve_lift_mm = 6.57
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 590.0
intake_open_deg = 340.0
intake_valve_count = 1
intake_valve_diameter_mm = 27.74
intake_valve_lift_mm = 7.3
misfire_probability = 0.0
rod_mm = 110.0
stroke_mm = 58.8
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 11.81283
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.01181284
inertia_kg_m2 = 0.354385
[ecu]
idle_base_opening = 0.05
idle_control = "feedback"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -5.0
idle_rpm = 1000.0
idle_spark_gain = 0.02
ignition_map = [[0.0, 10.0], [15000.0, 40.0]]
overrun_cut_rpm = 3500.0
overrun_fuel_cut_enabled = true
overrun_ignition_cut_enabled = false
overrun_resume_rpm = 2400.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = true
rev_limit_rpm = 7000.0
rev_resume_rpm = 6750.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 649.7059
clutch_engagement_rate = 8.0
drag_coefficient = 0.7
final_drive_ratio = 2.7
frontal_area = 0.65
gear_ratios = [4.1, 2.7, 2.0, 1.55, 1.25, 0.98]
gravity = 9.80665
mass_kg = 1400.0
primary_reduction = 1.8
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 1.2
wheel_radius_mm = 320.0
[fuel]
system = "injection"
[junctions]
h_pipe = true
junction_volume_cc = 50.0
[[pipes]]
id = "intake"
cells = 4
diameter_mm = 75.0
length_mm = 200.0
[[pipes]]
id = "runners"
count = 12
cells = 5
diameter_mm = 35.0
length_mm = 250.0
[[pipes]]
id = "primary_1"
cells = 11
diameter_mm = 38.0
length_mm = 520.0
[[pipes]]
id = "primary_2"
cells = 12
diameter_mm = 38.0
length_mm = 585.0
[[pipes]]
id = "primary_3"
cells = 13
diameter_mm = 38.0
length_mm = 650.0
[[pipes]]
id = "primary_4"
cells = 11
diameter_mm = 38.0
length_mm = 520.0
[[pipes]]
id = "primary_5"
cells = 12
diameter_mm = 38.0
length_mm = 585.0
[[pipes]]
id = "primary_6"
cells = 13
diameter_mm = 38.0
length_mm = 650.0
[[pipes]]
id = "primary_7"
cells = 11
diameter_mm = 38.0
length_mm = 520.0
[[pipes]]
id = "primary_8"
cells = 12
diameter_mm = 38.0
length_mm = 585.0
[[pipes]]
id = "primary_9"
cells = 13
diameter_mm = 38.0
length_mm = 650.0
[[pipes]]
id = "primary_10"
cells = 11
diameter_mm = 38.0
length_mm = 520.0
[[pipes]]
id = "primary_11"
cells = 12
diameter_mm = 38.0
length_mm = 585.0
[[pipes]]
id = "primary_12"
cells = 13
diameter_mm = 38.0
length_mm = 650.0
[[pipes]]
id = "tailpipe_25"
cells = 30
diameter_mm = 60.0
length_mm = 1500.0
[[pipes]]
id = "tailpipe_26"
cells = 28
diameter_mm = 60.0
length_mm = 1380.0
[[pipes]]
id = "secondary_27"
cells = 9
diameter_mm = 48.0
length_mm = 420.0
[[pipes]]
id = "secondary_28"
cells = 11
diameter_mm = 48.0
length_mm = 540.0

151
engines/v8-dual.toml Normal file
View File

@@ -0,0 +1,151 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "v8-dual"
name = "American 5.0 / independent banks"
category = "V8 / American 5.0"
description = "Same 4-2-1 headers with H crossover disconnected"
order = 8
cycle = "four_stroke"
[engine]
bank_angle_deg = 56.0
collector_volume_litres = 1.5
cylinders = 8
initial_rpm = 1000.0
initialize_phased_charge = true
layout = "v"
maximum_load_torque_nm = 300.0
minimum_substeps = 1
phase_degrees = [0.0, 450.0, 270.0, 540.0, 630.0, 360.0, 180.0, 89.99998]
pipe_damping = 1.0
plenum_volume_litres = 4.0
starter_torque_nm = 180.0
throttle_diameter_mm = 65.0
topology = "american_v8"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 101.6
burn_duration_deg = 65.0
burn_start_deg = 5.0
compression = 7.0
energy_variation = 0.05
exhaust_close_deg = 370.0
exhaust_open_deg = 140.0
exhaust_valve_count = 1
exhaust_valve_diameter_mm = 37.0
exhaust_valve_lift_mm = 10.0
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 570.0
intake_open_deg = 350.0
intake_valve_count = 1
intake_valve_diameter_mm = 45.0
intake_valve_lift_mm = 10.0
misfire_probability = 0.0
rod_mm = 129.0
stroke_mm = 76.2
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 22.0
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.025
inertia_kg_m2 = 1.1
[ecu]
idle_base_opening = 0.08
idle_control = "feedback"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -5.0
idle_rpm = 750.0
idle_spark_gain = 0.02
ignition_map = [[0.0, 10.0], [750.0, 12.0], [2500.0, 28.0], [4000.0, 32.0], [6000.0, 30.0]]
overrun_cut_rpm = 2000.0
overrun_fuel_cut_enabled = true
overrun_ignition_cut_enabled = false
overrun_resume_rpm = 1100.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = true
rev_limit_rpm = 7000.0
rev_resume_rpm = 6750.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 1100.0
clutch_engagement_rate = 8.0
drag_coefficient = 0.4
final_drive_ratio = 3.623529
frontal_area = 2.0
gear_ratios = [3.35, 1.93, 1.29, 1.0, 0.68]
gravity = 9.80665
mass_kg = 3000.0
primary_reduction = 1.0
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 1.2
wheel_radius_mm = 320.0
[fuel]
system = "injection"
[junctions]
h_pipe = false
junction_volume_cc = 50.0
[[pipes]]
id = "intake"
cells = 3
diameter_mm = 70.0
length_mm = 180.0
[[pipes]]
id = "runners"
count = 8
cells = 2
diameter_mm = 38.0
length_mm = 120.0
[[pipes]]
id = "primaries"
count = 8
cells = 15
diameter_mm = 38.0
length_mm = 750.0
[[pipes]]
id = "secondaries"
count = 4
cells = 8
diameter_mm = 45.0
length_mm = 400.0
[[pipes]]
id = "bank_collectors"
count = 2
cells = 11
diameter_mm = 50.0
length_mm = 550.0
[[pipes]]
id = "tailpipes"
count = 2
cells = 25
diameter_mm = 50.0
length_mm = 1250.0
[[pipes]]
id = "crossover"
cells = 16
diameter_mm = 38.0
length_mm = 780.0

151
engines/v8-h.toml Normal file
View File

@@ -0,0 +1,151 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "v8-h"
name = "American 5.0 / 4-2-1 + H"
category = "V8 / American 5.0"
description = "750 mm primaries, 50 cc junctions, H crossover"
order = 7
cycle = "four_stroke"
[engine]
bank_angle_deg = 56.0
collector_volume_litres = 1.5
cylinders = 8
initial_rpm = 1000.0
initialize_phased_charge = true
layout = "v"
maximum_load_torque_nm = 300.0
minimum_substeps = 1
phase_degrees = [0.0, 450.0, 270.0, 540.0, 630.0, 360.0, 180.0, 89.99998]
pipe_damping = 1.0
plenum_volume_litres = 4.0
starter_torque_nm = 180.0
throttle_diameter_mm = 65.0
topology = "american_v8"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 101.6
burn_duration_deg = 65.0
burn_start_deg = 5.0
compression = 7.0
energy_variation = 0.05
exhaust_close_deg = 370.0
exhaust_open_deg = 140.0
exhaust_valve_count = 1
exhaust_valve_diameter_mm = 37.0
exhaust_valve_lift_mm = 10.0
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 570.0
intake_open_deg = 350.0
intake_valve_count = 1
intake_valve_diameter_mm = 45.0
intake_valve_lift_mm = 10.0
misfire_probability = 0.0
rod_mm = 129.0
stroke_mm = 76.2
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 22.0
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.025
inertia_kg_m2 = 1.1
[ecu]
idle_base_opening = 0.08
idle_control = "feedback"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -5.0
idle_rpm = 750.0
idle_spark_gain = 0.02
ignition_map = [[0.0, 10.0], [750.0, 12.0], [2500.0, 28.0], [4000.0, 32.0], [6000.0, 30.0]]
overrun_cut_rpm = 2000.0
overrun_fuel_cut_enabled = true
overrun_ignition_cut_enabled = false
overrun_resume_rpm = 1100.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = true
rev_limit_rpm = 7000.0
rev_resume_rpm = 6750.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 1100.0
clutch_engagement_rate = 8.0
drag_coefficient = 0.4
final_drive_ratio = 3.623529
frontal_area = 2.0
gear_ratios = [3.35, 1.93, 1.29, 1.0, 0.68]
gravity = 9.80665
mass_kg = 3000.0
primary_reduction = 1.0
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 1.2
wheel_radius_mm = 320.0
[fuel]
system = "injection"
[junctions]
h_pipe = true
junction_volume_cc = 50.0
[[pipes]]
id = "intake"
cells = 3
diameter_mm = 70.0
length_mm = 180.0
[[pipes]]
id = "runners"
count = 8
cells = 2
diameter_mm = 38.0
length_mm = 120.0
[[pipes]]
id = "primaries"
count = 8
cells = 15
diameter_mm = 38.0
length_mm = 750.0
[[pipes]]
id = "secondaries"
count = 4
cells = 8
diameter_mm = 45.0
length_mm = 400.0
[[pipes]]
id = "bank_collectors"
count = 2
cells = 11
diameter_mm = 50.0
length_mm = 550.0
[[pipes]]
id = "tailpipes"
count = 2
cells = 25
diameter_mm = 50.0
length_mm = 1250.0
[[pipes]]
id = "crossover"
cells = 16
diameter_mm = 38.0
length_mm = 780.0

151
engines/v8-large.toml Normal file
View File

@@ -0,0 +1,151 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "v8-large"
name = "American 5.0 / larger primaries"
category = "V8 / American 5.0"
description = "42 mm x 750 mm primaries with H crossover"
order = 11
cycle = "four_stroke"
[engine]
bank_angle_deg = 56.0
collector_volume_litres = 1.5
cylinders = 8
initial_rpm = 1000.0
initialize_phased_charge = true
layout = "v"
maximum_load_torque_nm = 300.0
minimum_substeps = 1
phase_degrees = [0.0, 450.0, 270.0, 540.0, 630.0, 360.0, 180.0, 89.99998]
pipe_damping = 1.0
plenum_volume_litres = 4.0
starter_torque_nm = 180.0
throttle_diameter_mm = 65.0
topology = "american_v8"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 101.6
burn_duration_deg = 65.0
burn_start_deg = 5.0
compression = 7.0
energy_variation = 0.05
exhaust_close_deg = 370.0
exhaust_open_deg = 140.0
exhaust_valve_count = 1
exhaust_valve_diameter_mm = 37.0
exhaust_valve_lift_mm = 10.0
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 570.0
intake_open_deg = 350.0
intake_valve_count = 1
intake_valve_diameter_mm = 45.0
intake_valve_lift_mm = 10.0
misfire_probability = 0.0
rod_mm = 129.0
stroke_mm = 76.2
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 22.0
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.025
inertia_kg_m2 = 1.1
[ecu]
idle_base_opening = 0.08
idle_control = "feedback"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -5.0
idle_rpm = 750.0
idle_spark_gain = 0.02
ignition_map = [[0.0, 10.0], [750.0, 12.0], [2500.0, 28.0], [4000.0, 32.0], [6000.0, 30.0]]
overrun_cut_rpm = 2000.0
overrun_fuel_cut_enabled = true
overrun_ignition_cut_enabled = false
overrun_resume_rpm = 1100.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = true
rev_limit_rpm = 7000.0
rev_resume_rpm = 6750.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 1100.0
clutch_engagement_rate = 8.0
drag_coefficient = 0.4
final_drive_ratio = 3.623529
frontal_area = 2.0
gear_ratios = [3.35, 1.93, 1.29, 1.0, 0.68]
gravity = 9.80665
mass_kg = 3000.0
primary_reduction = 1.0
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 1.2
wheel_radius_mm = 320.0
[fuel]
system = "injection"
[junctions]
h_pipe = true
junction_volume_cc = 50.0
[[pipes]]
id = "intake"
cells = 3
diameter_mm = 70.0
length_mm = 180.0
[[pipes]]
id = "runners"
count = 8
cells = 2
diameter_mm = 38.0
length_mm = 120.0
[[pipes]]
id = "primaries"
count = 8
cells = 15
diameter_mm = 42.0
length_mm = 750.0
[[pipes]]
id = "secondaries"
count = 4
cells = 8
diameter_mm = 45.0
length_mm = 400.0
[[pipes]]
id = "bank_collectors"
count = 2
cells = 11
diameter_mm = 50.0
length_mm = 550.0
[[pipes]]
id = "tailpipes"
count = 2
cells = 25
diameter_mm = 50.0
length_mm = 1250.0
[[pipes]]
id = "crossover"
cells = 16
diameter_mm = 38.0
length_mm = 780.0

151
engines/v8-long.toml Normal file
View File

@@ -0,0 +1,151 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "v8-long"
name = "American 5.0 / long headers"
category = "V8 / American 5.0"
description = "900 mm primaries with H crossover"
order = 10
cycle = "four_stroke"
[engine]
bank_angle_deg = 56.0
collector_volume_litres = 1.5
cylinders = 8
initial_rpm = 1000.0
initialize_phased_charge = true
layout = "v"
maximum_load_torque_nm = 300.0
minimum_substeps = 1
phase_degrees = [0.0, 450.0, 270.0, 540.0, 630.0, 360.0, 180.0, 89.99998]
pipe_damping = 1.0
plenum_volume_litres = 4.0
starter_torque_nm = 180.0
throttle_diameter_mm = 65.0
topology = "american_v8"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 101.6
burn_duration_deg = 65.0
burn_start_deg = 5.0
compression = 7.0
energy_variation = 0.05
exhaust_close_deg = 370.0
exhaust_open_deg = 140.0
exhaust_valve_count = 1
exhaust_valve_diameter_mm = 37.0
exhaust_valve_lift_mm = 10.0
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 570.0
intake_open_deg = 350.0
intake_valve_count = 1
intake_valve_diameter_mm = 45.0
intake_valve_lift_mm = 10.0
misfire_probability = 0.0
rod_mm = 129.0
stroke_mm = 76.2
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 22.0
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.025
inertia_kg_m2 = 1.1
[ecu]
idle_base_opening = 0.08
idle_control = "feedback"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -5.0
idle_rpm = 750.0
idle_spark_gain = 0.02
ignition_map = [[0.0, 10.0], [750.0, 12.0], [2500.0, 28.0], [4000.0, 32.0], [6000.0, 30.0]]
overrun_cut_rpm = 2000.0
overrun_fuel_cut_enabled = true
overrun_ignition_cut_enabled = false
overrun_resume_rpm = 1100.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = true
rev_limit_rpm = 7000.0
rev_resume_rpm = 6750.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 1100.0
clutch_engagement_rate = 8.0
drag_coefficient = 0.4
final_drive_ratio = 3.623529
frontal_area = 2.0
gear_ratios = [3.35, 1.93, 1.29, 1.0, 0.68]
gravity = 9.80665
mass_kg = 3000.0
primary_reduction = 1.0
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 1.2
wheel_radius_mm = 320.0
[fuel]
system = "injection"
[junctions]
h_pipe = true
junction_volume_cc = 50.0
[[pipes]]
id = "intake"
cells = 3
diameter_mm = 70.0
length_mm = 180.0
[[pipes]]
id = "runners"
count = 8
cells = 2
diameter_mm = 38.0
length_mm = 120.0
[[pipes]]
id = "primaries"
count = 8
cells = 18
diameter_mm = 38.0
length_mm = 900.0
[[pipes]]
id = "secondaries"
count = 4
cells = 8
diameter_mm = 45.0
length_mm = 400.0
[[pipes]]
id = "bank_collectors"
count = 2
cells = 11
diameter_mm = 50.0
length_mm = 550.0
[[pipes]]
id = "tailpipes"
count = 2
cells = 25
diameter_mm = 50.0
length_mm = 1250.0
[[pipes]]
id = "crossover"
cells = 16
diameter_mm = 38.0
length_mm = 780.0

151
engines/v8-short.toml Normal file
View File

@@ -0,0 +1,151 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "v8-short"
name = "American 5.0 / short headers"
category = "V8 / American 5.0"
description = "600 mm primaries with H crossover"
order = 9
cycle = "four_stroke"
[engine]
bank_angle_deg = 56.0
collector_volume_litres = 1.5
cylinders = 8
initial_rpm = 1000.0
initialize_phased_charge = true
layout = "v"
maximum_load_torque_nm = 300.0
minimum_substeps = 1
phase_degrees = [0.0, 450.0, 270.0, 540.0, 630.0, 360.0, 180.0, 89.99998]
pipe_damping = 1.0
plenum_volume_litres = 4.0
starter_torque_nm = 180.0
throttle_diameter_mm = 65.0
topology = "american_v8"
[cylinder]
afr = 14.7
ambient_temperature_k = 300.0
bore_mm = 101.6
burn_duration_deg = 65.0
burn_start_deg = 5.0
compression = 7.0
energy_variation = 0.05
exhaust_close_deg = 370.0
exhaust_open_deg = 140.0
exhaust_valve_count = 1
exhaust_valve_diameter_mm = 37.0
exhaust_valve_lift_mm = 10.0
fuel_lhv_j_per_kg = 4.4e+07
heat_transfer = 100.0
intake_close_deg = 570.0
intake_open_deg = 350.0
intake_valve_count = 1
intake_valve_diameter_mm = 45.0
intake_valve_lift_mm = 10.0
misfire_probability = 0.0
rod_mm = 129.0
stroke_mm = 76.2
wall_area = 0.02
wiebe_a = 5.0
wiebe_m = 2.0
[crankshaft]
friction_constant_nm = 22.0
friction_quadratic_nm_s2 = 0.0
friction_viscous_nm_s = 0.025
inertia_kg_m2 = 1.1
[ecu]
idle_base_opening = 0.08
idle_control = "feedback"
idle_ki = 4e-05
idle_kp = 0.0001
idle_max_opening = 0.25
idle_minimum_advance_deg = -5.0
idle_rpm = 750.0
idle_spark_gain = 0.02
ignition_map = [[0.0, 10.0], [750.0, 12.0], [2500.0, 28.0], [4000.0, 32.0], [6000.0, 30.0]]
overrun_cut_rpm = 2000.0
overrun_fuel_cut_enabled = true
overrun_ignition_cut_enabled = false
overrun_resume_rpm = 1100.0
power_afr = 12.8
rev_limit_enabled = true
rev_limit_fuel_cut_enabled = true
rev_limit_rpm = 7000.0
rev_resume_rpm = 6750.0
stoichiometric_afr = 14.7
throttle_closing_rate = 0.0
throttle_closing_time_constant_s = 0.0
throttle_rate = 1000.0
throttle_time_constant_s = 0.001
[vehicle]
air_density = 1.225
clutch_capacity = 1100.0
clutch_engagement_rate = 8.0
drag_coefficient = 0.4
final_drive_ratio = 3.623529
frontal_area = 2.0
gear_ratios = [3.35, 1.93, 1.29, 1.0, 0.68]
gravity = 9.80665
mass_kg = 3000.0
primary_reduction = 1.0
rolling_resistance = 0.015
rotational_inertia_kg_m2 = 1.2
wheel_radius_mm = 320.0
[fuel]
system = "injection"
[junctions]
h_pipe = true
junction_volume_cc = 50.0
[[pipes]]
id = "intake"
cells = 3
diameter_mm = 70.0
length_mm = 180.0
[[pipes]]
id = "runners"
count = 8
cells = 2
diameter_mm = 38.0
length_mm = 120.0
[[pipes]]
id = "primaries"
count = 8
cells = 12
diameter_mm = 38.0
length_mm = 600.0
[[pipes]]
id = "secondaries"
count = 4
cells = 8
diameter_mm = 45.0
length_mm = 400.0
[[pipes]]
id = "bank_collectors"
count = 2
cells = 11
diameter_mm = 50.0
length_mm = 550.0
[[pipes]]
id = "tailpipes"
count = 2
cells = 25
diameter_mm = 50.0
length_mm = 1250.0
[[pipes]]
id = "crossover"
cells = 16
diameter_mm = 38.0
length_mm = 780.0

68
engines/yz-fat.toml Normal file
View File

@@ -0,0 +1,68 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "yz-fat"
name = "125 / wide belly chamber"
category = "Two-stroke / 125 cc"
description = "Estimated chamber geometry / same 125 cc engine"
order = 14
cycle = "two_stroke"
[two_stroke]
initial_rpm = 3000.0
starter_torque_nm = 8.0
maximum_load_torque_nm = 10.0
crankshaft_inertia_kg_m2 = 0.01
bore_mm = 54.0
carburetor_diameter_mm = 38.1
crankcase_clearance_cc = 150.0
exhaust = "racing"
exhaust_cell_length_mm = 10.0
exhaust_duration_deg = 190.0
exhaust_width_mm = 36.0
friction_bar = 0.4
friction_bar_per_mps = 0.09
friction_bar_per_mps2 = 0.0009
idle_minimum_advance_deg = -10.0
idle_slide_opening = 0.03
idle_spark_gain = 0.05
jet_scale = 1.0
main_jet_scale = 1.6
minimum_burn_s = 0.0008
needle_jet_scale = 1.0
needle_low_scale = 2.9
needle_mid_scale = 0.8
needle_off_idle_scale = 24.0
needle_quarter_scale = 1.4
needle_tip_scale = 6.5
needle_transition_scale = 9.0
needle_upper_scale = 0.76
pilot_jet_scale = 0.5475
pilot_tip_scale = 1.0
pilot_transition_scale = 1.55
rod_mm = 110.0
stroke_mm = 54.5
transfer_duration_deg = 130.0
transfer_width_mm = 50.0
trapped_compression = 8.2
chamber_sections_mm = [[160.0, 42.48, 42.48], [180.0, 42.48, 70.8], [190.0, 70.8, 123.9], [130.0, 123.9, 123.9], [210.0, 123.9, 25.96], [240.0, 25.96, 25.96]]
[ecu]
idle_control = "fixed"
idle_rpm = 3000.0
idle_max_opening = 0.3
throttle_time_constant_s = 0.0002
throttle_rate = 5000.0
rev_limit_enabled = false
rev_limit_fuel_cut_enabled = false
overrun_cut_rpm = 5000.0
overrun_resume_rpm = 3500.0
overrun_fuel_cut_enabled = false
overrun_ignition_cut_enabled = true
ignition_map = [[0, 10], [3000, 18], [6000, 24], [9000, 18], [11000, 12], [13000, 6], [15000, 2], [18000, 0]]
[vehicle]
primary_reduction = 3.368421
final_drive_ratio = 3.769231
mass_kg = 170
wheel_radius_mm = 331.3
gear_ratios = [2.384615, 1.933333, 1.588235, 1.352941, 1.176471, 1.055556]

68
engines/yz-racing.toml Normal file
View File

@@ -0,0 +1,68 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "yz-racing"
name = "YZ125 / expansion chamber"
category = "Two-stroke / 125 cc"
description = "Racing pipe / calibrated carb / 3% idle stop"
order = 1
cycle = "two_stroke"
[two_stroke]
initial_rpm = 3000.0
starter_torque_nm = 8.0
maximum_load_torque_nm = 10.0
crankshaft_inertia_kg_m2 = 0.01
bore_mm = 54.0
carburetor_diameter_mm = 38.1
crankcase_clearance_cc = 150.0
exhaust = "racing"
exhaust_cell_length_mm = 10.0
exhaust_duration_deg = 190.0
exhaust_width_mm = 36.0
friction_bar = 0.4
friction_bar_per_mps = 0.09
friction_bar_per_mps2 = 0.0009
idle_minimum_advance_deg = -10.0
idle_slide_opening = 0.03
idle_spark_gain = 0.05
jet_scale = 1.0
main_jet_scale = 1.6
minimum_burn_s = 0.0008
needle_jet_scale = 1.0
needle_low_scale = 2.9
needle_mid_scale = 0.8
needle_off_idle_scale = 24.0
needle_quarter_scale = 1.4
needle_tip_scale = 6.5
needle_transition_scale = 9.0
needle_upper_scale = 0.76
pilot_jet_scale = 0.5475
pilot_tip_scale = 1.0
pilot_transition_scale = 1.55
rod_mm = 110.0
stroke_mm = 54.5
transfer_duration_deg = 130.0
transfer_width_mm = 50.0
trapped_compression = 8.2
chamber_sections_mm = [[160.0, 36.0, 36.0], [180.0, 36.0, 60.0], [190.0, 60.0, 105.0], [130.0, 105.0, 105.0], [210.0, 105.0, 22.0], [240.0, 22.0, 22.0]]
[ecu]
idle_control = "fixed"
idle_rpm = 3000.0
idle_max_opening = 0.3
throttle_time_constant_s = 0.0002
throttle_rate = 5000.0
rev_limit_enabled = false
rev_limit_fuel_cut_enabled = false
overrun_cut_rpm = 5000.0
overrun_resume_rpm = 3500.0
overrun_fuel_cut_enabled = false
overrun_ignition_cut_enabled = true
ignition_map = [[0, 10], [3000, 18], [6000, 24], [9000, 18], [11000, 12], [13000, 6], [15000, 2], [18000, 0]]
[vehicle]
primary_reduction = 3.368421
final_drive_ratio = 3.769231
mass_kg = 170
wheel_radius_mm = 331.3
gear_ratios = [2.384615, 1.933333, 1.588235, 1.352941, 1.176471, 1.055556]

68
engines/yz-short.toml Normal file
View File

@@ -0,0 +1,68 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "yz-short"
name = "125 / short high-RPM chamber"
category = "Two-stroke / 125 cc"
description = "Estimated chamber geometry / same 125 cc engine"
order = 13
cycle = "two_stroke"
[two_stroke]
initial_rpm = 3000.0
starter_torque_nm = 8.0
maximum_load_torque_nm = 10.0
crankshaft_inertia_kg_m2 = 0.01
bore_mm = 54.0
carburetor_diameter_mm = 38.1
crankcase_clearance_cc = 150.0
exhaust = "racing"
exhaust_cell_length_mm = 10.0
exhaust_duration_deg = 190.0
exhaust_width_mm = 36.0
friction_bar = 0.4
friction_bar_per_mps = 0.09
friction_bar_per_mps2 = 0.0009
idle_minimum_advance_deg = -10.0
idle_slide_opening = 0.03
idle_spark_gain = 0.05
jet_scale = 1.0
main_jet_scale = 1.6
minimum_burn_s = 0.0008
needle_jet_scale = 1.0
needle_low_scale = 2.9
needle_mid_scale = 0.8
needle_off_idle_scale = 24.0
needle_quarter_scale = 1.4
needle_tip_scale = 6.5
needle_transition_scale = 9.0
needle_upper_scale = 0.76
pilot_jet_scale = 0.5475
pilot_tip_scale = 1.0
pilot_transition_scale = 1.55
rod_mm = 110.0
stroke_mm = 54.5
transfer_duration_deg = 130.0
transfer_width_mm = 50.0
trapped_compression = 8.2
chamber_sections_mm = [[131.2, 36.0, 36.0], [147.6, 36.0, 60.0], [155.8, 60.0, 105.0], [106.6, 105.0, 105.0], [172.2, 105.0, 22.0], [196.8, 22.0, 22.0]]
[ecu]
idle_control = "fixed"
idle_rpm = 3000.0
idle_max_opening = 0.3
throttle_time_constant_s = 0.0002
throttle_rate = 5000.0
rev_limit_enabled = false
rev_limit_fuel_cut_enabled = false
overrun_cut_rpm = 5000.0
overrun_resume_rpm = 3500.0
overrun_fuel_cut_enabled = false
overrun_ignition_cut_enabled = true
ignition_map = [[0, 10], [3000, 18], [6000, 24], [9000, 18], [11000, 12], [13000, 6], [15000, 2], [18000, 0]]
[vehicle]
primary_reduction = 3.368421
final_drive_ratio = 3.769231
mass_kg = 170
wheel_radius_mm = 331.3
gear_ratios = [2.384615, 1.933333, 1.588235, 1.352941, 1.176471, 1.055556]

68
engines/yz-straight.toml Normal file
View File

@@ -0,0 +1,68 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "yz-straight"
name = "YZ125 / straight exhaust"
category = "Two-stroke / 125 cc"
description = "Straight exhaust / calibrated pilot and needle / 3% idle stop"
order = 2
cycle = "two_stroke"
[two_stroke]
initial_rpm = 3000.0
starter_torque_nm = 8.0
maximum_load_torque_nm = 10.0
crankshaft_inertia_kg_m2 = 0.01
bore_mm = 54.0
carburetor_diameter_mm = 38.1
crankcase_clearance_cc = 150.0
exhaust = "straight"
exhaust_cell_length_mm = 10.0
exhaust_duration_deg = 190.0
exhaust_width_mm = 36.0
friction_bar = 0.4
friction_bar_per_mps = 0.09
friction_bar_per_mps2 = 0.0009
idle_minimum_advance_deg = -10.0
idle_slide_opening = 0.03
idle_spark_gain = 0.05
jet_scale = 1.0
main_jet_scale = 1.6
minimum_burn_s = 0.0008
needle_jet_scale = 1.0
needle_low_scale = 2.9
needle_mid_scale = 0.8
needle_off_idle_scale = 50.0
needle_quarter_scale = 1.42
needle_tip_scale = 6.0
needle_transition_scale = 14.0
needle_upper_scale = 0.76
pilot_jet_scale = 0.534
pilot_tip_scale = 1.0
pilot_transition_scale = 1.1
rod_mm = 110.0
stroke_mm = 54.5
transfer_duration_deg = 130.0
transfer_width_mm = 50.0
trapped_compression = 8.2
chamber_sections_mm = [[160.0, 36.0, 36.0], [180.0, 36.0, 60.0], [190.0, 60.0, 105.0], [130.0, 105.0, 105.0], [210.0, 105.0, 22.0], [240.0, 22.0, 22.0]]
[ecu]
idle_control = "fixed"
idle_rpm = 3000.0
idle_max_opening = 0.3
throttle_time_constant_s = 0.0002
throttle_rate = 5000.0
rev_limit_enabled = false
rev_limit_fuel_cut_enabled = false
overrun_cut_rpm = 5000.0
overrun_resume_rpm = 3500.0
overrun_fuel_cut_enabled = false
overrun_ignition_cut_enabled = true
ignition_map = [[0, 10], [3000, 18], [6000, 24], [9000, 18], [11000, 12], [13000, 6], [15000, 2], [18000, 0]]
[vehicle]
primary_reduction = 3.368421
final_drive_ratio = 3.769231
mass_kg = 170
wheel_radius_mm = 331.3
gear_ratios = [2.384615, 1.933333, 1.588235, 1.352941, 1.176471, 1.055556]

68
engines/yz-torque.toml Normal file
View File

@@ -0,0 +1,68 @@
# Engine definition. Dimensional keys state their units.
schema_version = 1
id = "yz-torque"
name = "125 / long torque chamber"
category = "Two-stroke / 125 cc"
description = "Estimated chamber geometry / same 125 cc engine"
order = 12
cycle = "two_stroke"
[two_stroke]
initial_rpm = 3000.0
starter_torque_nm = 8.0
maximum_load_torque_nm = 10.0
crankshaft_inertia_kg_m2 = 0.01
bore_mm = 54.0
carburetor_diameter_mm = 38.1
crankcase_clearance_cc = 150.0
exhaust = "racing"
exhaust_cell_length_mm = 10.0
exhaust_duration_deg = 190.0
exhaust_width_mm = 36.0
friction_bar = 0.4
friction_bar_per_mps = 0.09
friction_bar_per_mps2 = 0.0009
idle_minimum_advance_deg = -10.0
idle_slide_opening = 0.03
idle_spark_gain = 0.05
jet_scale = 1.0
main_jet_scale = 1.6
minimum_burn_s = 0.0008
needle_jet_scale = 1.0
needle_low_scale = 2.9
needle_mid_scale = 0.8
needle_off_idle_scale = 24.0
needle_quarter_scale = 1.4
needle_tip_scale = 6.5
needle_transition_scale = 9.0
needle_upper_scale = 0.76
pilot_jet_scale = 0.5475
pilot_tip_scale = 1.0
pilot_transition_scale = 1.55
rod_mm = 110.0
stroke_mm = 54.5
transfer_duration_deg = 130.0
transfer_width_mm = 50.0
trapped_compression = 8.2
chamber_sections_mm = [[188.8, 36.0, 36.0], [212.4, 36.0, 60.0], [224.2, 60.0, 105.0], [153.4, 105.0, 105.0], [247.8, 105.0, 22.0], [283.2, 22.0, 22.0]]
[ecu]
idle_control = "fixed"
idle_rpm = 3000.0
idle_max_opening = 0.3
throttle_time_constant_s = 0.0002
throttle_rate = 5000.0
rev_limit_enabled = false
rev_limit_fuel_cut_enabled = false
overrun_cut_rpm = 5000.0
overrun_resume_rpm = 3500.0
overrun_fuel_cut_enabled = false
overrun_ignition_cut_enabled = true
ignition_map = [[0, 10], [3000, 18], [6000, 24], [9000, 18], [11000, 12], [13000, 6], [15000, 2], [18000, 0]]
[vehicle]
primary_reduction = 3.368421
final_drive_ratio = 3.769231
mass_kg = 170
wheel_radius_mm = 331.3
gear_ratios = [2.384615, 1.933333, 1.588235, 1.352941, 1.176471, 1.055556]

View File

@@ -4,6 +4,7 @@ namespace fluidsim::port {
struct AudioFrame { float left{}, right{}; }; struct AudioFrame { float left{}, right{}; };
struct Vec3 { float x{}, y{}, z{}; }; struct Vec3 { float x{}, y{}, z{}; };
struct AudioListener { Vec3 position{}, right{1,0,0}; };
// Simple flow-derivative source used by the Helmholtz and pulse scenarios. // Simple flow-derivative source used by the Helmholtz and pulse scenarios.
class SoundProcessor { class SoundProcessor {

View File

@@ -5,21 +5,30 @@
namespace fluidsim::port { namespace fluidsim::port {
// Lumped slide-carburetor model for the crankcase-scavenged two-stroke. The // Lumped carburetor shared by two-stroke and four-stroke scenarios. The
// jets use liquid-orifice flow from a float bowl into the measured venturi // jets use liquid-orifice flow from a float bowl into the measured venturi
// depression. Delivered fuel is injected into the downstream fluid volume and // depression. Delivered fuel is injected into the downstream fluid volume and
// is then transported as a conserved species; this class never doses a cylinder. // is then transported as a conserved species; this class never doses a cylinder.
class Carburetor { struct CarburetorConfig {
public:
float fuel_density{740.0f}; float fuel_density{740.0f};
float pilot_jet_diameter{0.00040f}, main_jet_diameter{0.00150f}; float pilot_jet_diameter{0.00040f}, main_jet_diameter{0.00150f};
float needle_jet_diameter{0.00190f}; float needle_jet_diameter{0.00190f};
// Needle annulus area corrections at configured slide positions; jets meter
// liquid fuel from measured pressure, without AFR feedback.
float needle_low_scale{1.f}, needle_mid_scale{1.f};
float needle_off_idle_scale{1.f}, needle_transition_scale{1.f}, needle_tip_scale{1.f};
float needle_quarter_scale{1.f}, needle_upper_scale{1.f};
float pilot_transition_scale{1.f}, pilot_tip_scale{1.f};
float pilot_discharge_coefficient{0.62f}, main_discharge_coefficient{0.72f}; float pilot_discharge_coefficient{0.62f}, main_discharge_coefficient{0.72f};
float bowl_head_pressure{}; float bowl_head_pressure{};
float film_fraction{0.28f}, film_time_constant{0.11f}; float film_fraction{0.28f}, film_time_constant{0.11f};
float bowl_capacity{0.008f}, bowl_refill_rate{0.0025f}; // kg, kg/s float bowl_capacity{0.008f}, bowl_refill_rate{0.0025f}; // kg, kg/s
float overrun_rpm{4200.0f}, overrun_exit_rpm{3300.0f}; float overrun_rpm{4200.0f}, overrun_exit_rpm{3300.0f};
};
class Carburetor : public CarburetorConfig {
public:
explicit Carburetor(float throat_diameter = 0.0381f); explicit Carburetor(float throat_diameter = 0.0381f);
// air_mass_flow is positive from atmosphere toward the engine. // air_mass_flow is positive from atmosphere toward the engine.

View File

@@ -1,5 +1,5 @@
#pragma once #pragma once
#include "FluidSim/Scenario.hpp" #include "FluidSim/ScenarioSnapshot.hpp"
#include "FluidSim/DynoCurve.hpp" #include "FluidSim/DynoCurve.hpp"
#include "FluidSim/Desktop/PedalControl.hpp" #include "FluidSim/Desktop/PedalControl.hpp"
#include "FluidSim/Desktop/EngineRenderer3D.hpp" #include "FluidSim/Desktop/EngineRenderer3D.hpp"
@@ -22,6 +22,7 @@ public:
void nudge_pedal(float amount) {pedal_.set(pedal_.value()+amount);} 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);} 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); bool handle_event(const sf::Event&,sf::Vector2u);
AudioListener listener(const ScenarioSnapshot&,sf::Vector2u);
void draw(sf::RenderWindow&,const sf::Font*,const ScenarioSnapshot&,const PlaybackInfo&); void draw(sf::RenderWindow&,const sf::Font*,const ScenarioSnapshot&,const PlaybackInfo&);
private: private:
DynoCurve dyno_; DynoCurve dyno_;

View File

@@ -1,5 +1,5 @@
#pragma once #pragma once
#include "FluidSim/Scenario.hpp" #include "FluidSim/ScenarioSnapshot.hpp"
#include "FluidSim/Desktop/OrbitCamera.hpp" #include "FluidSim/Desktop/OrbitCamera.hpp"
#include <SFML/Graphics.hpp> #include <SFML/Graphics.hpp>
#include <memory> #include <memory>

View File

@@ -7,6 +7,13 @@ namespace fluidsim::port::desktop {
struct OrbitCamera { struct OrbitCamera {
Vec3 target{.12f,.25f,0}; Vec3 target{.12f,.25f,0};
float yaw{-28},pitch{24},distance{2.2f}; float yaw{-28},pitch{24},distance{2.2f};
AudioListener listener(float distance_scale=1, float target_offset_x=0) const {
const float y=yaw*.017453293f,p=pitch*.017453293f,d=distance*distance_scale;
// Inverse of the renderer's pitch/yaw world-to-view rotation.
return {{target.x+target_offset_x-std::sin(y)*std::cos(p)*d,
target.y+std::sin(p)*d,target.z+std::cos(y)*std::cos(p)*d},
{std::cos(y),0,std::sin(y)}};
}
void reset(float aspect) {target={.12f,.25f,0};yaw=-28;pitch=24;distance=std::max(.65f,1.6f/aspect)/.414214f;} 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 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) { void pan(sf::Vector2i delta,float height) {

View File

@@ -1,24 +1,28 @@
#pragma once #pragma once
#include "FluidSim/Scenario.hpp" #include "FluidSim/Scenario.hpp"
#include "FluidSim/ScenarioCatalog.hpp"
#include "FluidSim/TwoStrokeScenario.hpp" #include "FluidSim/TwoStrokeScenario.hpp"
#include <memory> #include <memory>
#include <mutex> #include <mutex>
namespace fluidsim::port { namespace fluidsim::port {
// Shared desktop controls/snapshot contract. Two-stroke access is serialized; // Shared desktop controls and snapshots. Each simulation owns its state lock.
// the twin already protects its own simulation state.
class DesktopScenario { class DesktopScenario {
public: public:
explicit DesktopScenario(ScenarioKind kind, TwoStrokeConfig config = {}, ExhaustConfig exhaust = {}, V8ExhaustConfig v8 = {}, EngineConfig engine = {}, std::string name = {}) : name_(std::move(name)) { explicit DesktopScenario(const ScenarioExample& e)
: DesktopScenario(e.kind,e.two,e.exhaust,e.junctions,e.engine,e.title) {}
explicit DesktopScenario(ScenarioKind kind, TwoStrokeConfig config = {}, ExhaustConfig exhaust = {}, JunctionConfig junctions = {}, EngineConfig engine = {}, std::string name = {}) : name_(std::move(name)) {
if (kind == ScenarioKind::TwoStroke) if (kind == ScenarioKind::TwoStroke)
two_ = std::make_unique<TwoStrokeScenario>(44100, config); two_ = std::make_unique<TwoStrokeScenario>(44100, config);
else else {
twin_ = std::make_unique<FourStrokeScenario>(44100, kind == ScenarioKind::SaabB205 ? if(!engine.cylinders)engine=preset_engine_definition(kind==ScenarioKind::SaabB205?
FourStrokePreset::SaabB205 : kind == ScenarioKind::AmericanV8 ? FourStrokePreset::AmericanV8 : FourStrokePreset::Twin, exhaust, v8,engine); FourStrokePreset::SaabB205:kind==ScenarioKind::AmericanV8?FourStrokePreset::AmericanV8:FourStrokePreset::Twin).engine;
four_ = std::make_unique<FourStrokeScenario>(44100,std::move(engine),exhaust,junctions);
}
} }
AudioFrame process() { AudioFrame process() {
if (twin_) if (four_)
return twin_->process(); return four_->process();
const std::lock_guard lock(mutex_); const std::lock_guard lock(mutex_);
return two_->process(); return two_->process();
} }
@@ -33,8 +37,11 @@ public:
void set_audio_scene(OutdoorScene scene) { void set_audio_scene(OutdoorScene scene) {
visit([&](auto& scenario) { scenario.set_audio_scene(scene); }); visit([&](auto& scenario) { scenario.set_audio_scene(scene); });
} }
void set_listener(AudioListener listener) {
visit([&](auto& scenario) { scenario.set_listener(listener); });
}
ScenarioSnapshot snapshot() const { ScenarioSnapshot snapshot() const {
if (twin_) { auto s=twin_->snapshot();s.example_name=name_;return s; } if (four_) { auto s=four_->snapshot();s.example_name=name_;return s; }
const std::lock_guard lock(mutex_); const std::lock_guard lock(mutex_);
const auto source = two_->snapshot(); const auto source = two_->snapshot();
ScenarioSnapshot s; ScenarioSnapshot s;
@@ -65,6 +72,7 @@ public:
s.valve1_exhaust_area = source.exhaust_area; s.valve1_exhaust_area = source.exhaust_area;
s.reed_open = source.reed_open; s.reed_open = source.reed_open;
s.fuel_cut = source.fuel_cut; s.fuel_cut = source.fuel_cut;
s.ignition_cut = source.ignition_cut;
s.idle_control = two_->ecu.idle_active(); s.idle_control = two_->ecu.idle_active();
s.rev_limit1 = two_->ecu.rev_limited(); s.rev_limit1 = two_->ecu.rev_limited();
s.audio_scene = source.audio_scene; s.audio_scene = source.audio_scene;
@@ -77,11 +85,11 @@ public:
private: private:
template<class Function> void visit(Function&& function) { template<class Function> void visit(Function&& function) {
const std::lock_guard lock(mutex_); const std::lock_guard lock(mutex_);
if (twin_) function(*twin_); if (four_) function(*four_);
else function(*two_); else function(*two_);
} }
std::string name_; std::string name_;
std::unique_ptr<FourStrokeTwinScenario> twin_; std::unique_ptr<FourStrokeScenario> four_;
std::unique_ptr<TwoStrokeScenario> two_; std::unique_ptr<TwoStrokeScenario> two_;
mutable std::mutex mutex_; mutable std::mutex mutex_;
}; };

View File

@@ -4,8 +4,7 @@
namespace fluidsim::port { namespace fluidsim::port {
// Ideal sensors and injectors; calibration parameters are not engine measurements. // Ideal sensors and injectors; calibration parameters are not engine measurements.
class Ecu { struct EcuConfig {
public:
enum class IdleControl { Feedback, FixedOpening }; enum class IdleControl { Feedback, FixedOpening };
IdleControl idle_control{IdleControl::Feedback}; IdleControl idle_control{IdleControl::Feedback};
struct IgnitionPoint { float rpm, degrees_btdc; }; struct IgnitionPoint { float rpm, degrees_btdc; };
@@ -18,18 +17,25 @@ public:
float throttle_closing_time_constant{}, throttle_closing_rate{}; float throttle_closing_time_constant{}, throttle_closing_rate{};
float rev_limit_rpm{10000.0f}, rev_resume_rpm{9750.0f}; float rev_limit_rpm{10000.0f}, rev_resume_rpm{9750.0f};
bool rev_limit_enabled{true}; bool rev_limit_enabled{true};
bool rev_limit_fuel_cut_enabled{true};
float overrun_cut_rpm{3500.0f}, overrun_resume_rpm{2400.0f}; float overrun_cut_rpm{3500.0f}, overrun_resume_rpm{2400.0f};
bool overrun_fuel_cut_enabled{true}; bool overrun_fuel_cut_enabled{true};
bool overrun_ignition_cut_enabled{false};
float idle_spark_gain{0.0f}, idle_minimum_advance{-5.0f}; // degrees/RPM, degrees BTDC float idle_spark_gain{0.0f}, idle_minimum_advance{-5.0f}; // degrees/RPM, degrees BTDC
float stoichiometric_afr{14.7f}, power_afr{12.8f}; float stoichiometric_afr{14.7f}, power_afr{12.8f};
};
class Ecu : public EcuConfig {
public:
void step(float dt, float pedal, float rpm); void step(float dt, float pedal, float rpm);
float fuel_for_air(float trapped_air_mass) const; float fuel_for_air(float trapped_air_mass) const;
float throttle_opening() const { return opening_; } float throttle_opening() const { return opening_; }
float throttle_target() const { return target_; } float throttle_target() const { return target_; }
float target_afr() const { return afr_; } float target_afr() const { return afr_; }
float spark_advance() const { return spark_; } float spark_advance() const { return spark_; }
bool fuel_cut() const { return limiter_ || (overrun_ && overrun_fuel_cut_enabled) || fault_; } bool fuel_cut() const { return (limiter_ && rev_limit_fuel_cut_enabled) || (overrun_ && overrun_fuel_cut_enabled) || fault_; }
bool ignition_cut() const { return limiter_ || (overrun_ && overrun_ignition_cut_enabled) || fault_; }
bool rev_limited() const { return limiter_; } bool rev_limited() const { return limiter_; }
bool idle_active() const { return idle_; } bool idle_active() const { return idle_; }
bool sensor_fault() const { return fault_; } bool sensor_fault() const { return fault_; }

View File

@@ -10,12 +10,16 @@ namespace fluidsim::port {
class Ecu; class Ecu;
class Crankshaft { struct CrankshaftConfig {
public: float inertia{0.2f};
float omega{400.0f * 2.0f * pi / 60.0f};
float angle{}, previous_angle{}, inertia{0.2f};
float friction_constant{}, friction_viscous{}; float friction_constant{}, friction_viscous{};
float friction_quadratic{}; // Nm / (rad/s)^2, dissipative windage term float friction_quadratic{}; // Nm / (rad/s)^2, dissipative windage term
};
class Crankshaft : public CrankshaftConfig {
public:
float omega{400.0f * 2.0f * pi / 60.0f};
float angle{}, previous_angle{};
float load_torque{}, drivetrain_torque{}; float load_torque{}, drivetrain_torque{};
float average_power{}, average_torque{}; float average_power{}, average_torque{};
@@ -28,18 +32,22 @@ private:
float power_sum_{}, torque_sum_{}; float power_sum_{}, torque_sum_{};
}; };
class EngineCylinder : public Component { struct CylinderConfig {
public: float intake_open{340}, intake_close{590}, exhaust_open{130}, exhaust_close{390};
Ecu* ecu{}; // Optional ideal electronic fuel/ignition controller.
bool transported_fuel{}; // Upstream carb meters the fuel species.
float bore{0.075f}, stroke{0.068f}, rod{0.112f}, compression{11.5f}; float bore{0.075f}, stroke{0.068f}, rod{0.112f}, compression{11.5f};
float intake_valve_diameter{0.030f}, exhaust_valve_diameter{0.028f}; float intake_valve_diameter{0.030f}, exhaust_valve_diameter{0.028f};
int intake_valve_count{1}, exhaust_valve_count{1}; int intake_valve_count{1}, exhaust_valve_count{1};
float intake_valve_lift{0.005f}, exhaust_valve_lift{0.005f}; float intake_valve_lift{0.005f}, exhaust_valve_lift{0.005f};
float phase{};
float spark_advance{20.0f}, wiebe_a{5.0f}, wiebe_m{2.0f}, burn_duration{60.0f}, burn_start{5.0f}; float spark_advance{20.0f}, wiebe_a{5.0f}, wiebe_m{2.0f}, burn_duration{60.0f}, burn_start{5.0f};
float afr{14.7f}, fuel_lhv{44.0e6f}, energy_variation{0.05f}, misfire_probability{}; float afr{14.7f}, fuel_lhv{44.0e6f}, energy_variation{0.05f}, misfire_probability{};
float wall_area{0.02f}, heat_transfer{100.0f}, ambient_temperature{300.0f}; float wall_area{0.02f}, heat_transfer{100.0f}, ambient_temperature{300.0f};
};
class EngineCylinder : public Component, public CylinderConfig {
public:
Ecu* ecu{};
bool transported_fuel{};
float phase{};
bool rev_limit_enabled{}; bool rev_limit_enabled{};
float rev_limit_rpm{7500.0f}, rev_limit_cut_time{0.05f}, rev_limit_hysteresis{200.0f}; float rev_limit_rpm{7500.0f}, rev_limit_cut_time{0.05f}, rev_limit_hysteresis{200.0f};
float air_mass{}, exhaust_mass{}, energy{}, volume{}, pressure{101325.0f}; float air_mass{}, exhaust_mass{}, energy{}, volume{}, pressure{101325.0f};
@@ -78,7 +86,6 @@ protected:
class FourStrokeCylinder final : public EngineCylinder { class FourStrokeCylinder final : public EngineCylinder {
public: public:
float intake_open{340.0f}, intake_close{590.0f}, exhaust_open{130.0f}, exhaust_close{390.0f};
float valve_area(float cycle_degrees, bool inlet) const override; float valve_area(float cycle_degrees, bool inlet) const override;
void pre_step(Crankshaft& crank, float dt, float throttle) override; void pre_step(Crankshaft& crank, float dt, float throttle) override;
}; };

View File

@@ -0,0 +1,53 @@
#pragma once
#include "FluidSim/Engine.hpp"
#include "FluidSim/Ecu.hpp"
#include "FluidSim/Vehicle.hpp"
#include "FluidSim/Carburetor.hpp"
#include <string>
#include <vector>
namespace fluidsim::port {
enum class ScenarioKind { Twin, TwoStroke, SaabB205, AmericanV8 };
enum class FourStrokePreset { Twin, SaabB205, AmericanV8 };
enum class EngineLayout { Inline, V, Boxer };
enum class ExhaustTopology { Collector, Staged, AmericanV8 };
struct PipeConfig {
std::string id;
float diameter{}, length{}; // metres
int cells{}, count{1};
bool silenced{};
};
// Parsed calibration only. Runtime state belongs to the scenario components.
// Dimensional fields use SI units after loading.
struct EngineConfig {
int cylinders{};
EngineLayout layout{EngineLayout::Inline};
ExhaustTopology topology{ExhaustTopology::Collector};
float bank_angle{60};
CylinderConfig cylinder;
CrankshaftConfig crankshaft;
EcuConfig ecu;
VehicleConfig vehicle;
CarburetorConfig fuel;
bool carbureted{}, initialize_phased_charge{true};
float initial_rpm{}, starter_torque{}, maximum_load_torque{};
float plenum_volume{}, collector_volume{}, throttle_diameter{}, pipe_damping{};
int minimum_substeps{1};
std::vector<float> phase_degrees;
std::vector<PipeConfig> pipes;
bool has_silenced_exhaust() const { for(const auto& p:pipes)if(p.silenced)return true;return false; }
bool american_headers() const { return topology==ExhaustTopology::AmericanV8; }
bool staged_headers() const { return topology==ExhaustTopology::Staged; }
bool dual_outlets() const { return american_headers() || (staged_headers() && layout==EngineLayout::V); }
int header_groups() const { return cylinders==3?1:2; }
};
struct JunctionConfig {
bool h_pipe{true};
float junction_volume{50e-6f};
};
void validate_engine_config(const EngineConfig& config,const std::string& context="engine config");
}

View File

@@ -25,7 +25,7 @@ struct OutdoorAudioConfig {
float monitor_cutoff_hz{6000.0f}; // Second-order acquisition/rendering filter. float monitor_cutoff_hz{6000.0f}; // Second-order acquisition/rendering filter.
}; };
// Static geometric direct/first-order reflection paths plus an explicitly // Moving geometric direct/first-order reflection paths plus an explicitly
// approximate diffuse tail. No feedback into the fluid/combustion simulation. // approximate diffuse tail. No feedback into the fluid/combustion simulation.
class OutdoorAudio { class OutdoorAudio {
public: public:
@@ -35,6 +35,9 @@ public:
AudioFrame process(float intake_mass_flow, float exhaust_mass_flow, float second_exhaust_mass_flow = 0); AudioFrame process(float intake_mass_flow, float exhaust_mass_flow, float second_exhaust_mass_flow = 0);
void set_scene(OutdoorScene scene) { requested_scene_.store(scene, std::memory_order_relaxed); } void set_scene(OutdoorScene scene) { requested_scene_.store(scene, std::memory_order_relaxed); }
OutdoorScene scene() const { return requested_scene_.load(std::memory_order_relaxed); } OutdoorScene scene() const { return requested_scene_.load(std::memory_order_relaxed); }
// Call on the simulation thread or under the scenario's simulation lock.
void set_listener(AudioListener listener);
void set_source_positions(const std::array<Vec3,3>& positions);
void set_monitor_filter(bool enabled) { monitor_filter_ = enabled; } // Set before playback/capture. void set_monitor_filter(bool enabled) { monitor_filter_ = enabled; } // Set before playback/capture.
AudioFrame dry_frame() const { return last_dry_; } AudioFrame dry_frame() const { return last_dry_; }
AudioFrame unfiltered_frame() const { return last_raw_; } AudioFrame unfiltered_frame() const { return last_raw_; }
@@ -45,8 +48,8 @@ private:
float process(float x); float process(float x);
}; };
struct Path { struct Path {
std::size_t delay_left{}, delay_right{}; float delay_left{}, delay_right{}, gain_left{}, gain_right{}, alpha{1};
float fraction_left{}, fraction_right{}, gain_left{}, gain_right{}, alpha{1}; float target_delay_left{}, target_delay_right{}, target_gain_left{}, target_gain_right{};
float state_left{}, state_right{}; float state_left{}, state_right{};
int group{}; // direct, ground, wall, trees, tunnel int group{}; // direct, ground, wall, trees, tunnel
}; };
@@ -55,6 +58,7 @@ private:
std::vector<Path> paths; std::vector<Path> paths;
std::size_t position{}; std::size_t position{};
float previous_flow{}; float previous_flow{};
float center_delay{}, target_center_delay{}, center_gain{}, target_center_gain{};
LowPass filter; LowPass filter;
}; };
int rate_{}; int rate_{};
@@ -66,8 +70,10 @@ private:
OutdoorExhaustReverb late_reverb_; OutdoorExhaustReverb late_reverb_;
std::array<float, 5> weights_{1, 0, 0, 0, 0}; std::array<float, 5> weights_{1, 0, 0, 0, 0};
float scene_alpha_{}, tail_mix_{}; float scene_alpha_{}, tail_mix_{};
float geometry_alpha_{}, reverb_input_{};
AudioFrame last_dry_{}, last_raw_{}; AudioFrame last_dry_{}, last_raw_{};
void configure_emitter(Emitter &emitter, Vec3 position); void configure_emitter(Emitter &emitter, Vec3 position);
void update_geometry();
AudioFrame render(Emitter &emitter, float flow, AudioFrame &dry, AudioFrame &raw); AudioFrame render(Emitter &emitter, float flow, AudioFrame &dry, AudioFrame &raw);
}; };
} // namespace fluidsim::port } // namespace fluidsim::port

View File

@@ -1,5 +1,7 @@
#pragma once #pragma once
#include "FluidSim/Silencer.hpp" #include "FluidSim/Silencer.hpp"
#include "FluidSim/EngineConfig.hpp"
#include "FluidSim/ScenarioSnapshot.hpp"
#include "FluidSim/Carburetor.hpp" #include "FluidSim/Carburetor.hpp"
#include <string> #include <string>
#include "FluidSim/Junction.hpp" #include "FluidSim/Junction.hpp"
@@ -19,85 +21,24 @@
#include <vector> #include <vector>
namespace fluidsim::port { namespace fluidsim::port {
struct ScenarioExample;
enum class ScenarioKind { Twin, TwoStroke, SaabB205, AmericanV8 }; // Calibrated four-stroke assembly with independent runners and shared plenums.
enum class FourStrokePreset { Twin, SaabB205, AmericanV8 };
enum class EngineLayout { Inline, V, Boxer };
struct EngineConfig {
int cylinders{}; // zero selects the existing preset
EngineLayout layout{EngineLayout::Inline};
float bank_angle{60}, bore{.086f}, stroke{.086f}, rod{.145f}, compression{10};
float idle_rpm{1000}, rev_limit{7000}, idle_opening{.045f};
bool carbureted{};
float carb_pilot_scale{1.f};
float carb_jet_scale{1.f}; // multiplier on displacement-scaled effective jet diameters
std::array<float,12> phase_degrees{}; // used when custom_phases is true
bool custom_phases{};
};
struct V8ExhaustConfig {
float primary_length{.75f}, primary_diameter{.038f};
bool h_pipe{true};
float junction_volume{50e-6f}; // m3, 50 cc per fitting
};
struct ScenarioSnapshot {
std::string example_name;
EngineLayout layout{EngineLayout::Inline};
float bank_angle{};
bool v8_headers{};
PipeSolver pipe_solver{PipeSolver::Rusanov};
int cylinder_count{2};
float rev_limit_rpm{10000};
std::array<float, 12> cylinder_angles{};
std::array<float, 12> cylinder_pressures{}, piston_fractions{}, intake_areas{}, exhaust_areas{};
std::uint64_t solver_substeps{}, boundary_iterations{}, pipe_positivity_repairs{};
float rpm{}, speed_kmh{}, kinetic_energy{}, average_power{}, average_torque{};
float cylinder_pressure1{}, cylinder_pressure2{};
float cylinder_temperature1{}, cylinder_temperature2{};
float cylinder_piston_fraction1{}, cylinder_piston_fraction2{};
float valve1_intake_area{}, valve1_exhaust_area{}, valve2_intake_area{}, valve2_exhaust_area{};
float intake_pressure{}, exhaust_pressure{}, second_bank_pressure{};
std::array<float,2> crossover_pressures{};
float throttle{}, pedal{}, target_afr{}, clutch{}, intake_flow{}, exhaust_flow{};
bool idle_control{}, fuel_cut{}, two_stroke{}, reed_open{};
bool racing_exhaust{};
bool silencers{}, h_pipe{};
ExhaustConfig exhaust_config{};
float crankcase_pressure{};
OutdoorScene audio_scene{OutdoorScene::Wall};
int gear{};
bool rev_limit1{}, rev_limit2{};
std::array<std::vector<GasState>, 32> pipe_states;
std::array<std::vector<float>, 32> pipe_areas, pipe_lengths;
void capture_pipes(const std::vector<Pipe1D>& pipes) {
for (std::size_t k = 0; k < pipes.size(); ++k) {
pipe_positivity_repairs += pipes[k].positivity_repairs;
pipe_areas[k] = pipes[k].cell_areas;
pipe_lengths[k] = pipes[k].cell_lengths;
pipe_states[k].reserve(pipes[k].cells.size());
for (std::size_t i = 0; i < pipes[k].cells.size(); ++i)
pipe_states[k].push_back(pipes[k].state(static_cast<int>(i)));
}
}
};
// Shared four-stroke topology: independent runners/headers and shared plenums.
// Presets include a 600 cc twin, B205-sized inline-four and American 5.0 V8.
class FourStrokeScenario : public MotorcycleTest { class FourStrokeScenario : public MotorcycleTest {
public: public:
explicit FourStrokeScenario(int sample_rate = 44100, FourStrokePreset preset = FourStrokePreset::Twin, ExhaustConfig exhaust = {}, V8ExhaustConfig v8_exhaust = {}, EngineConfig engine = {}); explicit FourStrokeScenario(int sample_rate = 44100, FourStrokePreset preset = FourStrokePreset::Twin);
const FourStrokePreset preset; FourStrokeScenario(int sample_rate, const ScenarioExample& definition);
FourStrokeScenario(int sample_rate, EngineConfig engine, ExhaustConfig exhaust = {}, JunctionConfig junctions = {});
const ExhaustConfig exhaust_config; const ExhaustConfig exhaust_config;
const V8ExhaustConfig v8_exhaust_config; const JunctionConfig junction_config;
const EngineConfig engine_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; } int cylinder_count() const { return engine_config.cylinders; }
std::array<FourStrokeCylinder*, 12> cylinders() { return {&cylinder1, &cylinder2, &cylinder3, &cylinder4, std::array<FourStrokeCylinder*, 12> cylinders() { return {&cylinder1, &cylinder2, &cylinder3, &cylinder4,
&cylinder5, &cylinder6, &cylinder7, &cylinder8,&cylinder9,&cylinder10,&cylinder11,&cylinder12}; } &cylinder5, &cylinder6, &cylinder7, &cylinder8,&cylinder9,&cylinder10,&cylinder11,&cylinder12}; }
AudioFrame process(); AudioFrame process();
void set_controls(float throttle_value, float load_value, float clutch_value); void set_controls(float throttle_value, float load_value, float clutch_value);
void set_audio_scene(OutdoorScene scene); void set_audio_scene(OutdoorScene scene);
void set_listener(AudioListener listener);
void set_pipe_solver(PipeSolver solver); void set_pipe_solver(PipeSolver solver);
void set_monitor_filter(bool enabled); void set_monitor_filter(bool enabled);
void set_starter(bool engaged); void set_starter(bool engaged);
@@ -115,8 +56,8 @@ public:
FourStrokeCylinder cylinder1{}, cylinder2{}, cylinder3{}, cylinder4{}; FourStrokeCylinder cylinder1{}, cylinder2{}, cylinder3{}, cylinder4{};
FourStrokeCylinder cylinder5{}, cylinder6{}, cylinder7{}, cylinder8{}; FourStrokeCylinder cylinder5{}, cylinder6{}, cylinder7{}, cylinder8{};
FourStrokeCylinder cylinder9{}, cylinder10{}, cylinder11{}, cylinder12{}; FourStrokeCylinder cylinder9{}, cylinder10{}, cylinder11{}, cylinder12{};
Volume0D intake_plenum{1.0e-3f, 101325.0f, 300.0f}; Volume0D intake_plenum;
Junction exhaust_collector{0.15e-3f, 101325.0f, 700.0f}; Junction exhaust_collector;
std::vector<Pipe1D> pipes; std::vector<Pipe1D> pipes;
float intake_flow{}, exhaust_flow{}; float intake_flow{}, exhaust_flow{};
std::uint64_t sample_count{}; std::uint64_t sample_count{};
@@ -134,7 +75,7 @@ private:
Port* throttle_port_{}; Port* throttle_port_{};
static float area_for_diameter(float diameter); static float area_for_diameter(float diameter);
static std::vector<Pipe1D> create_pipes(FourStrokePreset preset, const ExhaustConfig& exhaust, const V8ExhaustConfig& v8_exhaust, const EngineConfig& engine); static std::vector<Pipe1D> create_pipes(const EngineConfig& engine, const ExhaustConfig& exhaust);
}; };
using FourStrokeTwinScenario = FourStrokeScenario; using FourStrokeTwinScenario = FourStrokeScenario;

View File

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

View File

@@ -0,0 +1,16 @@
#pragma once
#include "FluidSim/AudioProcessing.hpp"
#include <array>
namespace fluidsim::port {
struct ScenarioSnapshot;
// Fractions refer to simulated pipe length; positions are world metres.
// Both the pipe mesh and its acoustic mouths use these same routes.
struct PipeRoute {
std::array<Vec3, 5> points{};
std::array<float, 5> fractions{};
unsigned count{};
};
std::array<PipeRoute, 32> scenario_pipe_routes(const ScenarioSnapshot& state);
std::array<Vec3, 3> scenario_sound_sources(const ScenarioSnapshot& state);
}

View File

@@ -0,0 +1,54 @@
#pragma once
#include "FluidSim/EngineConfig.hpp"
#include "FluidSim/Fluid.hpp"
#include "FluidSim/OutdoorAudio.hpp"
#include "FluidSim/Silencer.hpp"
#include <array>
#include <cstdint>
#include <string>
#include <vector>
namespace fluidsim::port {
struct ScenarioSnapshot {
std::string example_name;
EngineLayout layout{EngineLayout::Inline};
float bank_angle{};
bool v8_headers{};
bool staged_headers{}, dual_exhaust{};
PipeSolver pipe_solver{PipeSolver::Rusanov};
int cylinder_count{2};
float rev_limit_rpm{10000};
std::array<float, 12> cylinder_angles{};
std::array<float, 12> cylinder_pressures{}, piston_fractions{}, intake_areas{}, exhaust_areas{};
std::uint64_t solver_substeps{}, boundary_iterations{}, pipe_positivity_repairs{};
float rpm{}, speed_kmh{}, kinetic_energy{}, average_power{}, average_torque{};
float cylinder_pressure1{}, cylinder_pressure2{};
float cylinder_temperature1{}, cylinder_temperature2{};
float cylinder_piston_fraction1{}, cylinder_piston_fraction2{};
float valve1_intake_area{}, valve1_exhaust_area{}, valve2_intake_area{}, valve2_exhaust_area{};
float intake_pressure{}, exhaust_pressure{}, second_bank_pressure{};
std::array<float,2> crossover_pressures{};
float throttle{}, pedal{}, target_afr{}, clutch{}, intake_flow{}, exhaust_flow{};
bool idle_control{}, fuel_cut{}, ignition_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{};
std::array<std::vector<GasState>, 32> pipe_states;
std::array<std::vector<float>, 32> pipe_areas, pipe_lengths;
void capture_pipes(const std::vector<Pipe1D>& pipes) {
for (std::size_t k = 0; k < pipes.size(); ++k) {
pipe_positivity_repairs += pipes[k].positivity_repairs;
pipe_areas[k] = pipes[k].cell_areas;
pipe_lengths[k] = pipes[k].cell_lengths;
pipe_states[k].reserve(pipes[k].cells.size());
for (std::size_t i = 0; i < pipes[k].cells.size(); ++i)
pipe_states[k].push_back(pipes[k].state(static_cast<int>(i)));
}
}
};
}

View File

@@ -1,5 +1,7 @@
#pragma once #pragma once
#include "FluidSim/PipeProfile.hpp" #include "FluidSim/PipeProfile.hpp"
#include "FluidSim/Ecu.hpp"
#include "FluidSim/Vehicle.hpp"
#include <array> #include <array>
namespace fluidsim::port { namespace fluidsim::port {
@@ -8,14 +10,40 @@ enum class TwoStrokeExhaust { Racing, Straight };
// YZ125-sized baseline. Port/chamber/combustion dimensions are explicit design // YZ125-sized baseline. Port/chamber/combustion dimensions are explicit design
// estimates, not a measured Yamaha cylinder or factory exhaust/ignition map. // estimates, not a measured Yamaha cylinder or factory exhaust/ignition map.
struct TwoStrokeConfig { struct TwoStrokeConfig {
EcuConfig ecu=[] {
EcuConfig e;
e.idle_control=EcuConfig::IdleControl::FixedOpening;
e.throttle_time_constant=.0002f;e.throttle_rate=5000;
e.ignition_map={{0,10},{3000,18},{6000,24},{9000,18},{11000,12},{13000,6},{15000,2},{18000,0}};
e.idle_rpm=3000;e.idle_max_opening=.3f;e.rev_limit_enabled=false;
e.overrun_cut_rpm=5000;e.overrun_resume_rpm=3500;
e.rev_limit_fuel_cut_enabled=false;e.overrun_fuel_cut_enabled=false;e.overrun_ignition_cut_enabled=true;
return e;
}();
VehicleConfig vehicle=[] {
VehicleConfig v;
v.primary_reduction=64.f/19;v.final_drive_ratio=49.f/13;
v.gear_ratios={31.f/13,29.f/15,27.f/17,23.f/17,20.f/17,19.f/18};
v.mass=170;v.wheel_radius=.3313f;
return v;
}();
float initial_rpm{3000}, starter_torque{8}, maximum_load_torque{10};
float crankshaft_inertia{.01f};
TwoStrokeExhaust exhaust{TwoStrokeExhaust::Racing}; TwoStrokeExhaust exhaust{TwoStrokeExhaust::Racing};
float bore{.054f}, stroke{.0545f}, rod{.110f}; float bore{.054f}, stroke{.0545f}, rod{.110f};
float trapped_compression{8.2f}; // exhaust-closure volume / clearance volume float trapped_compression{8.2f}; // exhaust-closure volume / clearance volume
float transfer_duration{130}, exhaust_duration{190}; float transfer_duration{130}, exhaust_duration{190};
float transfer_width{.050f}, exhaust_width{.036f}; // summed effective window widths float transfer_width{.050f}, exhaust_width{.036f}; // summed effective window widths
float pilot_jet_scale{.75f}, main_jet_scale{1.6f}, needle_jet_scale{1.f};
float needle_low_scale{2.72f}, needle_mid_scale{.8f};
float needle_off_idle_scale{12.f}, needle_transition_scale{9.f}, needle_tip_scale{5.f};
float needle_quarter_scale{1.4f}, needle_upper_scale{.76f};
float pilot_transition_scale{1.f}, pilot_tip_scale{1.f};
float jet_scale{1.f}; // multiplier on effective jet diameters float jet_scale{1.f}; // multiplier on effective jet diameters
float crankcase_clearance{150e-6f}; float crankcase_clearance{150e-6f};
float carburetor_diameter{.0381f}; float carburetor_diameter{.0381f};
float idle_spark_gain{.05f}; // spark degrees per RPM error at the fixed carb stop
float idle_minimum_advance{-10.f};
float idle_slide_opening{.045f}; // fixed mechanical stop, normalized 0..1 (4.5%) float idle_slide_opening{.045f}; // fixed mechanical stop, normalized 0..1 (4.5%)
float minimum_burn_seconds{.0008f}; float minimum_burn_seconds{.0008f};
// Empirical mechanical FMEP = A + B*mean_piston_speed + C*speed^2. // Empirical mechanical FMEP = A + B*mean_piston_speed + C*speed^2.

View File

@@ -19,7 +19,7 @@ namespace fluidsim::port {
struct TwoStrokeSnapshot { struct TwoStrokeSnapshot {
float rpm{}, cylinder_pressure{}, crankcase_pressure{}, intake_pressure{}; float rpm{}, cylinder_pressure{}, crankcase_pressure{}, intake_pressure{};
float throttle{}, transfer_area{}, exhaust_area{}; float throttle{}, transfer_area{}, exhaust_area{};
bool reed_open{}, fuel_cut{}; bool reed_open{}, fuel_cut{}, ignition_cut{};
float pedal{}; float pedal{};
OutdoorScene audio_scene{OutdoorScene::Wall}; OutdoorScene audio_scene{OutdoorScene::Wall};
}; };
@@ -28,6 +28,7 @@ struct TwoStrokeSnapshot {
// the plenum / reed / crankcase / transfer / cylinder / exhaust topology. // the plenum / reed / crankcase / transfer / cylinder / exhaust topology.
class TwoStrokeScenario : public MotorcycleTest { class TwoStrokeScenario : public MotorcycleTest {
public: public:
void set_listener(AudioListener listener) { audio_.set_listener(listener); }
explicit TwoStrokeScenario(int sample_rate = 44100, TwoStrokeConfig config = {}); explicit TwoStrokeScenario(int sample_rate = 44100, TwoStrokeConfig config = {});
AudioFrame process(); AudioFrame process();
void set_throttle(float value); void set_throttle(float value);

View File

@@ -5,16 +5,19 @@
namespace fluidsim::port { namespace fluidsim::port {
// Longitudinal vehicle model and the friction clutch coupling it to the crank. // Longitudinal vehicle model and the friction clutch coupling it to the crank.
class Vehicle { struct VehicleConfig {
public:
int current_gear{};
std::vector<float> gear_ratios{4.1f, 2.7f, 2.0f, 1.55f, 1.25f, 0.98f}; std::vector<float> gear_ratios{4.1f, 2.7f, 2.0f, 1.55f, 1.25f, 0.98f};
float final_drive_ratio{2.7f}, primary_reduction{1.8f}; float final_drive_ratio{2.7f}, primary_reduction{1.8f};
float clutch_input{1.0f}, clutch_capacity{80.0f}, clutch_engagement_rate{8.0f}; float clutch_capacity{80.0f}, clutch_engagement_rate{8.0f};
float mass{160.0f}, wheel_radius{0.32f}, rotational_inertia{1.2f}; float mass{160.0f}, wheel_radius{0.32f}, rotational_inertia{1.2f};
float rolling_resistance{0.015f}, drag_coefficient{0.7f}, frontal_area{0.65f}; float rolling_resistance{0.015f}, drag_coefficient{0.7f}, frontal_area{0.65f};
float air_density{1.225f}, gravity{9.80665f}; float air_density{1.225f}, gravity{9.80665f};
float speed{}, engagement{}; };
class Vehicle : public VehicleConfig {
public:
int current_gear{};
float clutch_input{1.0f}, speed{}, engagement{};
float total_gear_ratio() const; float total_gear_ratio() const;
float kinetic_energy() const; float kinetic_energy() const;

View File

@@ -61,14 +61,31 @@ void Carburetor::step(float dt, float pedal, float slide, float rpm, float air_m
const float pilot_area = circle_area(pilot_jet_diameter); const float pilot_area = circle_area(pilot_jet_diameter);
const float main_area = circle_area(main_jet_diameter); const float main_area = circle_area(main_jet_diameter);
const float needle_jet_area = circle_area(needle_jet_diameter); const float needle_jet_area = circle_area(needle_jet_diameter);
const float needle_lift = smoothstep((slide - 0.04f) / 0.86f); // Piecewise needle annulus area, interpolated continuously between configured
// slide positions. A monotone area profile avoids artificial lean/rich holes
// from interpolating a multiplier over a curved base taper.
const std::array<float,10> positions{.04f,.045f,.05f,.07f,.1f,.15f,.25f,.5f,.75f,1.f};
const std::array<float,10> scales{1.f,std::lerp(1.f,needle_low_scale,.005f/.11f),
needle_off_idle_scale,needle_transition_scale,needle_tip_scale,needle_low_scale,needle_quarter_scale,
needle_mid_scale,needle_upper_scale,1.f};
float needle_lift=0;
for(std::size_t i=1;i<positions.size();++i) {
const float a=smoothstep((positions[i-1]-.04f)/.86f)*scales[i-1];
const float b=smoothstep((positions[i]-.04f)/.86f)*scales[i];
needle_lift=std::lerp(a,b,std::clamp((slide-positions[i-1])/(positions[i]-positions[i-1]),0.f,1.f));
if(slide<=positions[i]) break;
}
const float needle_area = needle_jet_area * needle_lift; const float needle_area = needle_jet_area * needle_lift;
const float main_path_area = needle_area > 1e-12f ? const float main_path_area = needle_area > 1e-12f ?
1.0f / std::sqrt(1.0f / (main_area * main_area) + 1.0f / (needle_area * needle_area)) : 0.0f; 1.0f / std::sqrt(1.0f / (main_area * main_area) + 1.0f / (needle_area * needle_area)) : 0.0f;
const float pilot_depression = bowl_head_pressure + (0.62f * static_drop + const float pilot_depression = bowl_head_pressure + (0.62f * static_drop +
0.25f * dynamic_pressure) * (1.0f + 0.25f * lift_enrichment_); 0.25f * dynamic_pressure) * (1.0f + 0.25f * lift_enrichment_);
const float main_depression = bowl_head_pressure + venturi_depression_; const float main_depression = bowl_head_pressure + venturi_depression_;
const float pilot_flow = pilot_discharge_coefficient * pilot_area * const float pilot_progression = slide<.05f ? std::lerp(1.f,pilot_transition_scale,
std::clamp((slide-.03f)/.02f,0.f,1.f)) : slide<.1f ?
std::lerp(pilot_transition_scale,pilot_tip_scale,(slide-.05f)/.05f) :
std::lerp(pilot_tip_scale,1.f,std::clamp((slide-.1f)/.1f,0.f,1.f));
const float pilot_flow = pilot_progression * pilot_discharge_coefficient * pilot_area *
std::sqrt(2.0f * fuel_density * std::max(pilot_depression, 0.0f)); std::sqrt(2.0f * fuel_density * std::max(pilot_depression, 0.0f));
const float main_flow = main_discharge_coefficient * main_path_area * const float main_flow = main_discharge_coefficient * main_path_area *
std::sqrt(2.0f * fuel_density * std::max(main_depression, 0.0f)); std::sqrt(2.0f * fuel_density * std::max(main_depression, 0.0f));
@@ -96,7 +113,7 @@ void Carburetor::step(float dt, float pedal, float slide, float rpm, float air_m
const float fuel_alpha = -std::expm1(-dt / 0.025f); const float fuel_alpha = -std::expm1(-dt / 0.025f);
filtered_fuel_flow_ += (delivered_fuel_flow_ - filtered_fuel_flow_) * fuel_alpha; filtered_fuel_flow_ += (delivered_fuel_flow_ - filtered_fuel_flow_) * fuel_alpha;
if (filtered_air_flow_ > 1e-7f && filtered_fuel_flow_ > 1e-9f) if (filtered_air_flow_ > 1e-7f && filtered_fuel_flow_ > 1e-9f)
indicated_afr_ = std::clamp(filtered_air_flow_ / filtered_fuel_flow_, 4.0f, 40.0f); indicated_afr_ = filtered_air_flow_ / filtered_fuel_flow_;
} }
} // namespace fluidsim::port } // namespace fluidsim::port

View File

@@ -28,14 +28,11 @@ constexpr float throttle_lerp_rate = 10.0f;
} // namespace } // namespace
int main(int argc, char** argv) { int main(int argc, char** argv) try {
using namespace fluidsim::port; using namespace fluidsim::port;
ScenarioKind selected_kind=ScenarioKind::Twin;
bool scenario_from_args=false; bool scenario_from_args=false;
TwoStrokeConfig two_stroke_config; const auto& examples=scenario_examples();
ExhaustConfig exhaust_config; V8ExhaustConfig v8_config; EngineConfig engine_config; auto selected=examples.front();
const auto examples=scenario_examples();
std::string example_title;
PipeSolver selected_solver=PipeSolver::Rusanov; PipeSolver selected_solver=PipeSolver::Rusanov;
OutdoorScene selected_scene=OutdoorScene::Wall; OutdoorScene selected_scene=OutdoorScene::Wall;
for(int i=1;i<argc;++i){ for(int i=1;i<argc;++i){
@@ -45,18 +42,17 @@ int main(int argc, char** argv) {
if(option=="--example") { if(option=="--example") {
const auto found=std::find_if(examples.begin(),examples.end(),[&](const auto& e){return e.id==value;}); const auto found=std::find_if(examples.begin(),examples.end(),[&](const auto& e){return e.id==value;});
if(found==examples.end()){std::cerr<<"Unknown example: "<<value<<"\n";return 2;} if(found==examples.end()){std::cerr<<"Unknown example: "<<value<<"\n";return 2;}
selected_kind=found->kind;two_stroke_config=found->two;exhaust_config=found->exhaust;v8_config=found->v8;engine_config=found->engine;example_title=found->title;scenario_from_args=true; selected=*found;scenario_from_args=true;
} else if(option=="--scenario"){ } else if(option=="--scenario"){
if(value!="twin"&&value!="two-stroke"&&value!="b205"&&value!="v8"){std::cerr<<"Choose twin, two-stroke, b205 or v8\n";return 2;} if(value!="twin"&&value!="two-stroke"&&value!="b205"&&value!="v8"){std::cerr<<"Choose twin, two-stroke, b205 or v8\n";return 2;}
selected_kind=value=="v8" ? ScenarioKind::AmericanV8 : value=="b205" ? ScenarioKind::SaabB205 : selected=scenario_example(value=="v8"?"v8-h":value=="b205"?"saab-straight":value=="two-stroke"?"yz-racing":"twin");
(value=="two-stroke" ? ScenarioKind::TwoStroke : ScenarioKind::Twin);
scenario_from_args=true; scenario_from_args=true;
}else if(option=="--solver"){ }else if(option=="--solver"){
try { selected_solver=parse_pipe_solver(value); } try { selected_solver=parse_pipe_solver(value); }
catch(const std::exception& e) { std::cerr<<e.what()<<"\n"; return 2; } catch(const std::exception& e) { std::cerr<<e.what()<<"\n"; return 2; }
}else if(option=="--exhaust"){ }else if(option=="--exhaust"){
if(value=="racing") two_stroke_config.exhaust=TwoStrokeExhaust::Racing; if(value=="racing") selected.two.exhaust=TwoStrokeExhaust::Racing;
else if(value=="straight") two_stroke_config.exhaust=TwoStrokeExhaust::Straight; else if(value=="straight") selected.two.exhaust=TwoStrokeExhaust::Straight;
else { std::cerr<<"Choose racing or straight exhaust\n";return 2; } else { std::cerr<<"Choose racing or straight exhaust\n";return 2; }
}else{ }else{
if(value=="dry")selected_scene=OutdoorScene::Dry; if(value=="dry")selected_scene=OutdoorScene::Dry;
@@ -89,22 +85,26 @@ int main(int argc, char** argv) {
if(!has_font)std::cerr<<"No UI font available; configure FLUIDSIM_FONT_FILE.\n"; if(!has_font)std::cerr<<"No UI font available; configure FLUIDSIM_FONT_FILE.\n";
const sf::Font* font_ptr=has_font?&font:nullptr; const sf::Font* font_ptr=has_font?&font:nullptr;
while (window.isOpen()) {
if (!scenario_from_args) { if (!scenario_from_args) {
window.setTitle("FluidSim - Examples");
window.setMinimumSize(sf::Vector2u{720,606});
window.setSize({720,606});
const auto choice = desktop::choose_scenario(window, font_ptr,examples); const auto choice = desktop::choose_scenario(window, font_ptr,examples);
if (!choice) return 0; if (!choice) return 0;
const auto& example=examples[*choice]; selected=examples[*choice];
selected_kind=example.kind;two_stroke_config=example.two;exhaust_config=example.exhaust;v8_config=example.v8;engine_config=example.engine;example_title=example.title;
window.setSize({window_width, window_height}); window.setSize({window_width, window_height});
window.setView(sf::View(sf::FloatRect({0, 0}, {float(window_width), float(window_height)}))); window.setView(sf::View(sf::FloatRect({0, 0}, {float(window_width), float(window_height)})));
} }
window.setTitle(selected_kind == ScenarioKind::AmericanV8 ? "FluidSim - 1980s American 5.0 V8" : selected_kind == ScenarioKind::SaabB205 ? "FluidSim - Saab B205 / 9-5 wagon" : scenario_from_args=false;
(selected_kind == ScenarioKind::TwoStroke ? "FluidSim - YZ125 two-stroke" : "FluidSim - parallel twin")); window.setTitle("FluidSim - "+selected.title);
window.setMinimumSize(sf::Vector2u{1040,800}); window.setMinimumSize(sf::Vector2u{1040,800});
if(!example_title.empty())window.setTitle("FluidSim - "+example_title); DesktopScenario scenario(selected);
DesktopScenario scenario(selected_kind, two_stroke_config,exhaust_config,v8_config,engine_config,example_title);
scenario.set_pipe_solver(selected_solver); scenario.set_pipe_solver(selected_solver);
scenario.set_audio_scene(selected_scene); scenario.set_audio_scene(selected_scene);
std::cout << "Desktop started: scenario=" << static_cast<int>(selected_kind) desktop::DesktopUi ui;
scenario.set_listener(ui.listener(scenario.snapshot(),window.getSize()));
std::cout << "Desktop started: scenario=" << selected.id
<< " solver=" << pipe_solver_name(selected_solver) << " scene=" << scene_name(selected_scene) << std::endl; << " solver=" << pipe_solver_name(selected_solver) << " scene=" << scene_name(selected_scene) << std::endl;
SimulationRingBuffer ring(ring_capacity); SimulationRingBuffer ring(ring_capacity);
for (std::size_t i = 0; i < preroll_samples; ++i) { for (std::size_t i = 0; i < preroll_samples; ++i) {
@@ -134,11 +134,16 @@ int main(int argc, char** argv) {
float brake_target = 0.0f, brake_current = 0.0f; float brake_target = 0.0f, brake_current = 0.0f;
float clutch_current = 0.0f; float clutch_current = 0.0f;
bool throttle_held = false, throttle_down = false, clutch_held = false; bool throttle_held = false, throttle_down = false, clutch_held = false;
desktop::DesktopUi ui;
auto previous_frame = std::chrono::steady_clock::now(); auto previous_frame = std::chrono::steady_clock::now();
while (window.isOpen()) { bool return_to_menu=false;
while (window.isOpen() && !return_to_menu) {
while (const std::optional event = window.pollEvent()) { while (const std::optional event = window.pollEvent()) {
if (const auto* key = event->getIf<sf::Event::KeyPressed>();
key && key->code == sf::Keyboard::Key::Escape) {
return_to_menu=true;
break;
}
if(ui.handle_event(*event,window.getSize())) continue; if(ui.handle_event(*event,window.getSize())) continue;
if(event->is<sf::Event::FocusLost>()) { throttle_held=false; throttle_down=false; clutch_held=false; scenario.set_starter(false); } if(event->is<sf::Event::FocusLost>()) { throttle_held=false; throttle_down=false; clutch_held=false; scenario.set_starter(false); }
if (event->is<sf::Event::Closed>()) window.close(); if (event->is<sf::Event::Closed>()) window.close();
@@ -185,6 +190,8 @@ int main(int argc, char** argv) {
} }
if (!window.isOpen() || return_to_menu) break;
const auto now = std::chrono::steady_clock::now(); const auto now = std::chrono::steady_clock::now();
const float frame_dt = std::chrono::duration<float>(now - previous_frame).count(); const float frame_dt = std::chrono::duration<float>(now - previous_frame).count();
previous_frame = now; previous_frame = now;
@@ -202,11 +209,18 @@ int main(int argc, char** argv) {
const desktop::PlaybackInfo playback{throttle_current,brake_current,current_speed, const desktop::PlaybackInfo playback{throttle_current,brake_current,current_speed,
ring.available_samples(),ring_capacity,audio.underrun_blocks(),audio.auto_gain(),time_warp}; ring.available_samples(),ring_capacity,audio.underrun_blocks(),audio.auto_gain(),time_warp};
ui.draw(window,font_ptr,scenario.snapshot(),playback); const auto state=scenario.snapshot();
scenario.set_listener(ui.listener(state,window.getSize()));
ui.draw(window,font_ptr,state,playback);
} }
simulation.request_stop(); simulation.request_stop();
audio.stop(); audio.stop();
simulation.join();
}
return 0; return 0;
} catch(const std::exception& error) {
std::cerr<<"Startup failed: "<<error.what()<<'\n';
return 2;
} }

View File

@@ -60,6 +60,15 @@ bool DesktopUi::handle_event(const sf::Event& event,sf::Vector2u size) {
} }
return false; return false;
} }
AudioListener DesktopUi::listener(const ScenarioSnapshot& s,sf::Vector2u size) {
const auto viewport=WorkspaceLayout::calculate(size).viewport;
if(!camera_fitted_) {
camera_.reset(viewport.size.x/(viewport.size.y-56));
camera_fitted_=true;
}
return camera_.listener(s.silencers?1.8f:s.cylinder_count==8?1.4f:1.f,
s.silencers?1.1f:s.cylinder_count==8?.7f:0.f);
}
void DesktopUi::draw(sf::RenderWindow& w,const sf::Font* font,const ScenarioSnapshot& s,const PlaybackInfo& p) { 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); 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); w.setView(sf::View(sf::FloatRect({0,0},{width,height})));w.clear(background);
@@ -85,7 +94,7 @@ void DesktopUi::draw(sf::RenderWindow& w,const sf::Font* font,const ScenarioSnap
y+=12;label(w,font,"AUDIO & SIMULATION",x,y,11,accent);y+=28; 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("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)); 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); label(w,font,s.fuel_cut?"FUEL CUT":s.ignition_cut?"IGNITION CUT":s.idle_control?"IDLE CONTROL":"RUNNING",x,y+8,12,(s.fuel_cut||s.ignition_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); 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_?"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); 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);
@@ -112,7 +121,7 @@ void DesktopUi::draw(sf::RenderWindow& w,const sf::Font* font,const ScenarioSnap
const bool exhaust=s.two_stroke?s.valve1_exhaust_area>0:s.exhaust_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,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,"Esc: menu 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); label(w,font,"Pressure: blue vacuum / green atmosphere / red 1 MPa",l.viewport.position.x+2,height-29,11,muted);
w.display(); w.display();
} }

View File

@@ -51,7 +51,7 @@ void Ecu::step(float dt, float pedal, float rpm) {
if (rpm <= b.rpm) break; if (rpm <= b.rpm) break;
} }
} }
if(idle_ && !fixed_idle && idle_spark_gain>0) spark_=std::clamp(spark_+idle_spark_gain*error,idle_minimum_advance,35.0f); if(idle_ && idle_spark_gain>0) spark_=std::clamp(spark_+idle_spark_gain*error,idle_minimum_advance,35.0f);
} }
float Ecu::fuel_for_air(float air) const { float Ecu::fuel_for_air(float air) const {

View File

@@ -150,7 +150,7 @@ void FourStrokeCylinder::pre_step(Crankshaft& crank, float dt, float /*throttle*
const float spark_angle = wrap_degrees(720.0f - spark_advance); const float spark_angle = wrap_degrees(720.0f - spark_advance);
const bool crossed_spark = crossed_angle(previous_degrees,current_degrees,spark_angle); const bool crossed_spark = crossed_angle(previous_degrees,current_degrees,spark_angle);
if (crossed_spark && fuel_injected && !combustion_active && !(ecu ? ecu->fuel_cut() : rev_limit_active) && random_unit() >= misfire_probability) { if (crossed_spark && fuel_injected && !combustion_active && !(ecu ? ecu->ignition_cut() : rev_limit_active) && random_unit() >= misfire_probability) {
combustion_active = true; combustion_active = true;
burn_fraction = 0.0f; burn_fraction = 0.0f;
active_fuel_mass_ = transported_fuel ? std::min(fuel_mass, air_mass / 14.7f) : fuel_mass; active_fuel_mass_ = transported_fuel ? std::min(fuel_mass, air_mass / 14.7f) : fuel_mass;

526
src/engine_config.cpp Normal file
View File

@@ -0,0 +1,526 @@
#include "FluidSim/ScenarioCatalog.hpp"
#include <toml.hpp>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <limits>
#include <set>
#include <sstream>
#include <stdexcept>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#endif
namespace fluidsim::port {
namespace {
class Reader {
const toml::table& table_;
std::set<std::string> used_;
std::string path_;
public:
Reader(const toml::table& table,std::string path):table_(table),path_(std::move(path)) {}
[[noreturn]] void fail(const std::string& message) const { throw std::runtime_error(path_+": "+message); }
void required(std::initializer_list<const char*> keys) const {
for(const auto* key:keys)if(!table_.contains(key))fail(std::string("missing required field '")+key+"'");
}
const toml::node* node(const std::string& key) { used_.insert(key);return table_.get(key); }
void field(const char* key,float& value,double scale=1) {
if(const auto* n=node(key)) {
auto number=n->value<double>();
if(!number || !std::isfinite(*number) || std::abs(*number*scale)>std::numeric_limits<float>::max())
fail(std::string(key)+" must be a finite number");
value=static_cast<float>(*number*scale);
}
}
void field(const char* key,int& value) {
if(const auto* n=node(key)) {
auto number=n->value<int64_t>();
if(!n->is_integer() || !number || *number<std::numeric_limits<int>::min() || *number>std::numeric_limits<int>::max())
fail(std::string(key)+" must be an integer");
value=static_cast<int>(*number);
}
}
void field(const char* key,bool& value) {
if(const auto* n=node(key)) {
auto b=n->value<bool>();if(!b)fail(std::string(key)+" must be true or false");value=*b;
}
}
void field(const char* key,std::string& value) {
if(const auto* n=node(key)) {
auto s=n->value<std::string>();if(!s)fail(std::string(key)+" must be a string");value=*s;
}
}
std::string text(const char* key,std::string fallback={}) {field(key,fallback);return fallback;}
Reader section(const char* key,bool required=false) {
static const toml::table empty;
const auto* n=node(key);
if(n && !n->is_table())fail(std::string(key)+" must be a table");
if(!n && required)fail(std::string("missing [")+key+"]");
return Reader(n?*n->as_table():empty,path_+"."+key);
}
const toml::array* array(const char* key) {
const auto* n=node(key);
if(n && !n->is_array())fail(std::string(key)+" must be an array");
if(n && n->as_array()->size()>4096)fail(std::string(key)+" has too many entries");
return n?n->as_array():nullptr;
}
float number(const toml::node& n) const {
auto value=n.value<double>();
if(!value || !std::isfinite(*value) || std::abs(*value)>std::numeric_limits<float>::max())fail("array entries must be finite numbers");
return static_cast<float>(*value);
}
void field(const char* key,std::vector<float>& values) {
if(const auto* a=array(key)) {values.clear();for(const auto& n:*a)values.push_back(number(n));}
}
void finish() const {
for(const auto& [key,value]:table_)if(!used_.contains(std::string(key.str())))fail("unknown field '"+std::string(key.str())+"'");
}
};
template<class T> void require(bool condition,const T& where,const char* message) {
if(!condition)throw std::runtime_error(std::string(where)+": "+message);
}
// Scalar readers below keep the file units explicit at the conversion boundary.
void read_cylinder(Reader r,CylinderConfig& c) {
r.required({"bore_mm","stroke_mm","rod_mm","compression"});
r.field("afr",c.afr);
r.field("ambient_temperature_k",c.ambient_temperature);
r.field("bore_mm",c.bore,0.001);
r.field("burn_duration_deg",c.burn_duration);
r.field("burn_start_deg",c.burn_start);
r.field("compression",c.compression);
r.field("energy_variation",c.energy_variation);
r.field("exhaust_close_deg",c.exhaust_close);
r.field("exhaust_open_deg",c.exhaust_open);
r.field("exhaust_valve_count",c.exhaust_valve_count);
r.field("exhaust_valve_diameter_mm",c.exhaust_valve_diameter,0.001);
r.field("exhaust_valve_lift_mm",c.exhaust_valve_lift,0.001);
r.field("fuel_lhv_j_per_kg",c.fuel_lhv);
r.field("heat_transfer",c.heat_transfer);
r.field("intake_close_deg",c.intake_close);
r.field("intake_open_deg",c.intake_open);
r.field("intake_valve_count",c.intake_valve_count);
r.field("intake_valve_diameter_mm",c.intake_valve_diameter,0.001);
r.field("intake_valve_lift_mm",c.intake_valve_lift,0.001);
r.field("misfire_probability",c.misfire_probability);
r.field("rod_mm",c.rod,0.001);
r.field("stroke_mm",c.stroke,0.001);
r.field("wall_area",c.wall_area);
r.field("wiebe_a",c.wiebe_a);
r.field("wiebe_m",c.wiebe_m);
r.finish();
}
void read_crankshaft(Reader r,CrankshaftConfig& c) {
r.field("friction_constant_nm",c.friction_constant);
r.field("friction_quadratic_nm_s2",c.friction_quadratic);
r.field("friction_viscous_nm_s",c.friction_viscous);
r.field("inertia_kg_m2",c.inertia);
r.finish();
}
void read_ecu(Reader r,EcuConfig& c) {
r.field("idle_base_opening",c.idle_base_opening);
r.field("idle_ki",c.idle_ki);
r.field("idle_kp",c.idle_kp);
r.field("idle_max_opening",c.idle_max_opening);
r.field("idle_minimum_advance_deg",c.idle_minimum_advance);
r.field("idle_rpm",c.idle_rpm);
r.field("idle_spark_gain",c.idle_spark_gain);
r.field("overrun_cut_rpm",c.overrun_cut_rpm);
r.field("overrun_fuel_cut_enabled",c.overrun_fuel_cut_enabled);
r.field("overrun_ignition_cut_enabled",c.overrun_ignition_cut_enabled);
r.field("overrun_resume_rpm",c.overrun_resume_rpm);
r.field("power_afr",c.power_afr);
r.field("rev_limit_enabled",c.rev_limit_enabled);
r.field("rev_limit_fuel_cut_enabled",c.rev_limit_fuel_cut_enabled);
r.field("rev_limit_rpm",c.rev_limit_rpm);
r.field("rev_resume_rpm",c.rev_resume_rpm);
r.field("stoichiometric_afr",c.stoichiometric_afr);
r.field("throttle_closing_rate",c.throttle_closing_rate);
r.field("throttle_closing_time_constant_s",c.throttle_closing_time_constant);
r.field("throttle_rate",c.throttle_rate);
r.field("throttle_time_constant_s",c.throttle_time_constant);
const auto mode=r.text("idle_control","feedback");
if(mode!="fixed" && mode!="feedback")r.fail("idle_control must be fixed or feedback");
c.idle_control=mode=="fixed"?EcuConfig::IdleControl::FixedOpening:EcuConfig::IdleControl::Feedback;
if(const auto* curve=r.array("ignition_map")) {
c.ignition_map.clear();float previous=-1;
for(const auto& n:*curve) {
const auto* point=n.as_array();
if(!point || point->size()!=2)r.fail("ignition_map entries must be [RPM, degrees BTDC]");
const float rpm=r.number(*point->get(0)),advance=r.number(*point->get(1));
if(rpm<0 || rpm<=previous || advance < -90 || advance>90)r.fail("ignition_map needs increasing nonnegative RPM and advances within +/-90 degrees");
c.ignition_map.push_back({rpm,advance});previous=rpm;
}
if(c.ignition_map.empty())r.fail("ignition_map must not be empty");
}
r.finish();
}
void read_vehicle(Reader r,VehicleConfig& c) {
r.field("air_density",c.air_density);
r.field("clutch_capacity",c.clutch_capacity);
r.field("clutch_engagement_rate",c.clutch_engagement_rate);
r.field("drag_coefficient",c.drag_coefficient);
r.field("final_drive_ratio",c.final_drive_ratio);
r.field("frontal_area",c.frontal_area);
r.field("gear_ratios",c.gear_ratios);
r.field("gravity",c.gravity);
r.field("mass_kg",c.mass);
r.field("primary_reduction",c.primary_reduction);
r.field("rolling_resistance",c.rolling_resistance);
r.field("rotational_inertia_kg_m2",c.rotational_inertia);
r.field("wheel_radius_mm",c.wheel_radius,0.001);
r.finish();
}
void read_silencer(Reader r,SilencerConfig& c) {
r.field("chambers",c.chambers);
r.field("core_diameter_mm",c.core_diameter,0.001);
r.field("length_mm",c.length,0.001);
r.field("loss_coefficient",c.loss_coefficient);
r.field("passage_diameter_mm",c.passage_diameter,0.001);
r.field("resistance",c.resistance);
r.field("volume_litres",c.volume,0.001);
const auto type=r.text("type","straight_through");
if(type=="straight_through")c.type=SilencerType::StraightThrough;
else if(type=="chambered")c.type=SilencerType::Chambered;
else if(type=="expansion")c.type=SilencerType::ExpansionResonator;
else r.fail("unknown silencer type '"+type+"'");
r.finish();
}
void read_silencers(Reader r,ExhaustConfig& c) {
r.field("cell_length_mm",c.cell_length,0.001);
r.field("connecting_length_mm",c.connecting_length,0.001);
r.field("diameter_mm",c.diameter,0.001);
r.field("enabled",c.enabled);
r.field("inlet_length_mm",c.inlet_length,0.001);
r.field("outlet_length_mm",c.outlet_length,0.001);
read_silencer(r.section("resonator"),c.resonator);
read_silencer(r.section("muffler"),c.muffler);
r.finish();
}
void read_two_stroke(Reader r,TwoStrokeConfig& c) {
r.required({"bore_mm","stroke_mm","rod_mm","trapped_compression","carburetor_diameter_mm"});
r.field("initial_rpm",c.initial_rpm);
r.field("starter_torque_nm",c.starter_torque);
r.field("maximum_load_torque_nm",c.maximum_load_torque);
r.field("crankshaft_inertia_kg_m2",c.crankshaft_inertia);
r.field("bore_mm",c.bore,0.001);
r.field("carburetor_diameter_mm",c.carburetor_diameter,0.001);
r.field("crankcase_clearance_cc",c.crankcase_clearance,1e-06);
r.field("exhaust_cell_length_mm",c.exhaust_cell_length,0.001);
r.field("exhaust_duration_deg",c.exhaust_duration);
r.field("exhaust_width_mm",c.exhaust_width,0.001);
r.field("friction_bar",c.friction_bar);
r.field("friction_bar_per_mps",c.friction_bar_per_mps);
r.field("friction_bar_per_mps2",c.friction_bar_per_mps2);
r.field("idle_minimum_advance_deg",c.idle_minimum_advance);
r.field("idle_slide_opening",c.idle_slide_opening);
r.field("idle_spark_gain",c.idle_spark_gain);
r.field("jet_scale",c.jet_scale);
r.field("main_jet_scale",c.main_jet_scale);
r.field("minimum_burn_s",c.minimum_burn_seconds);
r.field("needle_jet_scale",c.needle_jet_scale);
r.field("needle_low_scale",c.needle_low_scale);
r.field("needle_mid_scale",c.needle_mid_scale);
r.field("needle_off_idle_scale",c.needle_off_idle_scale);
r.field("needle_quarter_scale",c.needle_quarter_scale);
r.field("needle_tip_scale",c.needle_tip_scale);
r.field("needle_transition_scale",c.needle_transition_scale);
r.field("needle_upper_scale",c.needle_upper_scale);
r.field("pilot_jet_scale",c.pilot_jet_scale);
r.field("pilot_tip_scale",c.pilot_tip_scale);
r.field("pilot_transition_scale",c.pilot_transition_scale);
r.field("rod_mm",c.rod,0.001);
r.field("stroke_mm",c.stroke,0.001);
r.field("transfer_duration_deg",c.transfer_duration);
r.field("transfer_width_mm",c.transfer_width,0.001);
r.field("trapped_compression",c.trapped_compression);
const auto exhaust=r.text("exhaust","racing");
if(exhaust!="racing" && exhaust!="straight")r.fail("exhaust must be racing or straight");
c.exhaust=exhaust=="racing"?TwoStrokeExhaust::Racing:TwoStrokeExhaust::Straight;
if(const auto* sections=r.array("chamber_sections_mm")) {
if(sections->size()!=c.racing_pipe.size())r.fail("chamber_sections_mm must have six sections");
for(std::size_t i=0;i<sections->size();++i) {
const auto* section=sections->get(i)->as_array();
if(!section || section->size()!=3)r.fail("chamber sections must be [length, inlet diameter, outlet diameter] in mm");
auto& p=c.racing_pipe[i];
p.length=r.number(*section->get(0))*.001f;
p.inlet_diameter=r.number(*section->get(1))*.001f;
p.outlet_diameter=r.number(*section->get(2))*.001f;
}
}
r.finish();
}
void read_engine(Reader r,EngineConfig& c) {
r.required({"cylinders","layout","topology","phase_degrees"});
r.field("bank_angle_deg",c.bank_angle);
r.field("collector_volume_litres",c.collector_volume,0.001);
r.field("cylinders",c.cylinders);
r.field("initial_rpm",c.initial_rpm);
r.field("initialize_phased_charge",c.initialize_phased_charge);
r.field("maximum_load_torque_nm",c.maximum_load_torque);
r.field("minimum_substeps",c.minimum_substeps);
r.field("phase_degrees",c.phase_degrees);
r.field("pipe_damping",c.pipe_damping);
r.field("plenum_volume_litres",c.plenum_volume,0.001);
r.field("starter_torque_nm",c.starter_torque);
r.field("throttle_diameter_mm",c.throttle_diameter,0.001);
const auto layout=r.text("layout","inline"),topology=r.text("topology","collector");
if(layout=="inline")c.layout=EngineLayout::Inline;
else if(layout=="v")c.layout=EngineLayout::V;
else if(layout=="boxer")c.layout=EngineLayout::Boxer;
else r.fail("layout must be inline, v or boxer");
if(topology=="collector")c.topology=ExhaustTopology::Collector;
else if(topology=="staged")c.topology=ExhaustTopology::Staged;
else if(topology=="american_v8")c.topology=ExhaustTopology::AmericanV8;
else r.fail("unknown exhaust topology '"+topology+"'");
r.finish();
}
void read_fuel(Reader r,EngineConfig& c) {
const auto system=r.text("system","injection");
if(system!="injection" && system!="carburetor")r.fail("system must be injection or carburetor");
c.carbureted=system=="carburetor";
r.field("pilot_jet_diameter_mm",c.fuel.pilot_jet_diameter,0.001);
r.field("main_jet_diameter_mm",c.fuel.main_jet_diameter,0.001);
r.field("needle_jet_diameter_mm",c.fuel.needle_jet_diameter,0.001);
r.field("needle_low_scale",c.fuel.needle_low_scale);
r.field("needle_mid_scale",c.fuel.needle_mid_scale);
r.field("needle_off_idle_scale",c.fuel.needle_off_idle_scale);
r.field("needle_transition_scale",c.fuel.needle_transition_scale);
r.field("needle_tip_scale",c.fuel.needle_tip_scale);
r.field("needle_quarter_scale",c.fuel.needle_quarter_scale);
r.field("needle_upper_scale",c.fuel.needle_upper_scale);
r.field("pilot_transition_scale",c.fuel.pilot_transition_scale);
r.field("pilot_tip_scale",c.fuel.pilot_tip_scale);
r.field("fuel_density",c.fuel.fuel_density);
r.field("pilot_discharge_coefficient",c.fuel.pilot_discharge_coefficient);
r.field("main_discharge_coefficient",c.fuel.main_discharge_coefficient);
r.field("bowl_head_pressure",c.fuel.bowl_head_pressure);
r.field("film_fraction",c.fuel.film_fraction);
r.field("film_time_constant",c.fuel.film_time_constant);
r.field("overrun_rpm",c.fuel.overrun_rpm);
r.field("overrun_exit_rpm",c.fuel.overrun_exit_rpm);
r.field("bowl_capacity_kg",c.fuel.bowl_capacity);
r.field("bowl_refill_kg_per_s",c.fuel.bowl_refill_rate);
r.finish();
}
void validate_ecu(const EcuConfig& c,const std::string& path) {
require(c.idle_rpm>0 && c.idle_base_opening>=0 && c.idle_base_opening<=1 &&
c.idle_max_opening>=c.idle_base_opening && c.idle_max_opening<=1,path,"ecu: invalid idle calibration");
require(c.rev_limit_rpm>c.rev_resume_rpm && c.rev_resume_rpm>0 &&
c.overrun_cut_rpm>c.overrun_resume_rpm && c.overrun_resume_rpm>0,path,"ecu: invalid cut/resume RPM");
require(c.throttle_time_constant>0 && c.throttle_rate>0 && c.idle_spark_gain>=0 &&
c.throttle_closing_time_constant>=0 && c.throttle_closing_rate>=0,path,"ecu: invalid actuator calibration");
require(c.stoichiometric_afr>0 && c.power_afr>0,path,"ecu: AFR targets must be positive");
}
void validate_vehicle(const VehicleConfig& c,const std::string& path) {
require(c.mass>0 && c.wheel_radius>0 && c.rotational_inertia>=0 &&
c.primary_reduction>0 && c.final_drive_ratio>0 && c.clutch_capacity>0 &&
c.clutch_engagement_rate>0 && c.rolling_resistance>=0 &&
c.drag_coefficient>=0 && c.frontal_area>0 && c.air_density>0 && c.gravity>0,
path,"vehicle: invalid dimensions, ratios or resistance");
require(!c.gear_ratios.empty() && c.gear_ratios.size()<=16,path,"vehicle: expected 1..16 gear ratios");
for(float ratio:c.gear_ratios)require(ratio>0,path,"vehicle: gear ratios must be positive");
}
void validate_two(const TwoStrokeConfig& c,const std::string& path) {
require(c.bore>0 && c.bore<=1 && c.stroke>0 && c.stroke<=1 && c.rod>c.stroke*.5f &&
c.trapped_compression>1 && c.trapped_compression<=50,path,"two_stroke: invalid cylinder geometry");
require(c.transfer_duration>0 && c.transfer_duration<c.exhaust_duration && c.exhaust_duration<360 &&
c.transfer_width>0 && c.exhaust_width>0 && c.crankcase_clearance>0,
path,"two_stroke: invalid port timing or crankcase geometry");
require(c.idle_slide_opening>=0 && c.idle_slide_opening<=1 && c.initial_rpm>0 && c.initial_rpm<=30000 &&
c.crankshaft_inertia>0 && c.starter_torque>=0 && c.maximum_load_torque>=0 && c.idle_spark_gain>=0,
path,"two_stroke: invalid idle or crankshaft calibration");
require(c.carburetor_diameter>0 && c.minimum_burn_seconds>0 &&
c.friction_bar>=0 && c.friction_bar_per_mps>=0 && c.friction_bar_per_mps2>=0,
path,"two_stroke: invalid carburetor, burn time or friction");
for(float value:{c.jet_scale,c.pilot_jet_scale,c.main_jet_scale,c.needle_jet_scale,
c.needle_low_scale,c.needle_mid_scale,c.needle_off_idle_scale,c.needle_transition_scale,
c.needle_tip_scale,c.needle_quarter_scale,c.needle_upper_scale,c.pilot_transition_scale,c.pilot_tip_scale})
require(value>0,path,"two_stroke: jet and needle scales must be positive");
require(c.exhaust_cell_length>=.002f && c.exhaust_cell_length<=1,path,"two_stroke: exhaust cell length outside 2..1000 mm");
float total=0;
for(auto p:c.racing_pipe) {
require(p.length>0 && p.inlet_diameter>0 && p.outlet_diameter>0,path,"two_stroke: invalid chamber section");
total+=p.length;
}
require(total/c.exhaust_cell_length<=4096,path,"two_stroke: chamber exceeds 4096 cells");
validate_ecu(c.ecu,path);validate_vehicle(c.vehicle,path);
}
} // namespace
void validate_engine_config(const EngineConfig& c,const std::string& path) {
require(c.cylinders>=1 && c.cylinders<=12 && (c.layout==EngineLayout::Inline || c.cylinders%2==0),
path,"engine: expected 1..12 cylinders and even banked cylinder counts");
const auto& cylinder=c.cylinder;
require(cylinder.bore>0 && cylinder.bore<=1 && cylinder.stroke>0 && cylinder.stroke<=1 &&
cylinder.rod>cylinder.stroke*.5f && cylinder.compression>1 && cylinder.compression<=50,
path,"cylinder: invalid bore, stroke, rod or compression");
require(cylinder.intake_valve_count>=1 && cylinder.intake_valve_count<=8 &&
cylinder.exhaust_valve_count>=1 && cylinder.exhaust_valve_count<=8 &&
cylinder.intake_valve_diameter>0 && cylinder.exhaust_valve_diameter>0 &&
cylinder.intake_valve_lift>0 && cylinder.exhaust_valve_lift>0,path,"cylinder: invalid valve geometry");
for(float angle:{cylinder.intake_open,cylinder.intake_close,cylinder.exhaust_open,cylinder.exhaust_close})
require(angle>=0 && angle<720,path,"cylinder: valve events must be in [0,720) degrees");
require(cylinder.intake_open!=cylinder.intake_close && cylinder.exhaust_open!=cylinder.exhaust_close &&
cylinder.burn_duration>0 && cylinder.wiebe_a>0 && cylinder.wiebe_m>=0 &&
cylinder.energy_variation>=0 && cylinder.energy_variation<=1 &&
cylinder.misfire_probability>=0 && cylinder.misfire_probability<=1 &&
cylinder.fuel_lhv>0 && cylinder.wall_area>0 && cylinder.heat_transfer>=0 && cylinder.ambient_temperature>0,
path,"cylinder: invalid timing or combustion calibration");
require(c.crankshaft.inertia>0 && c.crankshaft.friction_constant>=0 && c.crankshaft.friction_viscous>=0 &&
c.crankshaft.friction_quadratic>=0,path,"crankshaft: invalid inertia or friction");
require(c.initial_rpm>0 && c.initial_rpm<=30000 && c.starter_torque>=0 && c.maximum_load_torque>=0 &&
c.plenum_volume>0 && c.collector_volume>0 && c.throttle_diameter>0 &&
c.pipe_damping>=0 && c.minimum_substeps>=1 && c.minimum_substeps<=64 &&
c.bank_angle>0 && c.bank_angle<=180,path,"engine: invalid startup, intake or solver calibration");
require(c.phase_degrees.size()==static_cast<std::size_t>(c.cylinders),path,"engine: phase_degrees needs one value per cylinder");
for(float phase:c.phase_degrees)require(std::isfinite(phase) && phase>=0 && phase<720,path,"engine: invalid cylinder phase");
require(!c.american_headers() || (c.cylinders==8 && c.layout==EngineLayout::V),path,"engine: american_v8 topology requires a V8");
require(!c.staged_headers() || c.cylinders>=3,path,"engine: staged topology requires at least three cylinders");
const int expected=c.american_headers()?26:1+2*c.cylinders+(c.dual_outlets()?2:1)+(c.staged_headers()?c.header_groups():0);
int count=0,cells=0,silenced=0;std::set<std::string> ids;
for(const auto& p:c.pipes) {
require(!p.id.empty() && ids.insert(p.id).second,path,"pipes: missing or duplicate pipe group id");
require(p.count>=1 && p.count<=12,path,"pipes: count must be in 1..12");
if(p.silenced) {
require(c.has_silenced_exhaust() && c.topology==ExhaustTopology::Collector &&
count==1+2*c.cylinders && p.count==1,path,"pipes: silenced profile must be the collector's final outlet");
++silenced;
} else {
require(p.diameter>0 && p.diameter<=2 && p.length>0 && p.length<=100 && p.cells>=1 && p.cells<=4096,
path,"pipes: invalid diameter, length or cell count");
cells+=p.cells*p.count;
}
count+=p.count;
}
require(count==expected && count<=32 && cells<=32768,path,"pipes: groups do not match the selected topology or exceed mesh limits");
require(silenced<=1,path,"pipes: only one silenced outlet is supported");
if(c.carbureted) {
const auto& f=c.fuel;
for(float value:{f.pilot_jet_diameter,f.main_jet_diameter,f.needle_jet_diameter,f.needle_low_scale,
f.needle_mid_scale,f.needle_off_idle_scale,f.needle_transition_scale,f.needle_tip_scale,
f.needle_quarter_scale,f.needle_upper_scale,f.pilot_transition_scale,f.pilot_tip_scale,
f.fuel_density,f.pilot_discharge_coefficient,f.main_discharge_coefficient,f.bowl_capacity,f.bowl_refill_rate,f.film_time_constant})
require(value>0,path,"fuel: jets, scales, density and time constants must be positive");
require(f.film_fraction>=0 && f.film_fraction<=.8f && f.bowl_head_pressure>=0 &&
f.overrun_rpm>f.overrun_exit_rpm && f.overrun_exit_rpm>0,path,"fuel: invalid film or overrun calibration");
}
validate_ecu(c.ecu,path);validate_vehicle(c.vehicle,path);
}
ScenarioExample load_engine_file(const std::filesystem::path& path) {
toml::table table;
try { table=toml::parse_file(path.string()); }
catch(const toml::parse_error& error) {std::ostringstream message;message<<error;throw std::runtime_error(message.str());}
Reader root(table,path.string());
int version=0;root.field("schema_version",version);
if(version!=1)root.fail("schema_version must be 1");
ScenarioExample e;
root.field("id",e.id);root.field("name",e.title);root.field("category",e.category);
root.field("description",e.description);root.field("order",e.order);
if(e.id.empty() || e.title.empty() || e.category.empty())root.fail("id, name and category are required");
if(e.id.find_first_not_of("abcdefghijklmnopqrstuvwxyz0123456789-_")!=std::string::npos)root.fail("id must use lowercase letters, digits, - or _");
const auto cycle=root.text("cycle");
if(cycle=="two_stroke") {
e.kind=ScenarioKind::TwoStroke;
read_two_stroke(root.section("two_stroke",true),e.two);
read_ecu(root.section("ecu"),e.two.ecu);
read_vehicle(root.section("vehicle"),e.two.vehicle);
validate_two(e.two,path.string());
} else if(cycle=="four_stroke") {
read_engine(root.section("engine",true),e.engine);
read_cylinder(root.section("cylinder",true),e.engine.cylinder);
read_crankshaft(root.section("crankshaft",true),e.engine.crankshaft);
read_ecu(root.section("ecu",true),e.engine.ecu);
read_vehicle(root.section("vehicle",true),e.engine.vehicle);
read_fuel(root.section("fuel",true),e.engine);
read_silencers(root.section("silencers"),e.exhaust);
auto junctions=root.section("junctions");
junctions.field("h_pipe",e.junctions.h_pipe);junctions.field("junction_volume_cc",e.junctions.junction_volume,1e-6);
junctions.finish();
if(const auto* pipes=root.array("pipes"))for(const auto& value:*pipes) {
if(!value.is_table())root.fail("pipes entries must be tables");
Reader r(*value.as_table(),path.string()+".pipes");PipeConfig p;
r.field("id",p.id);r.field("count",p.count);
const auto profile=r.text("profile");
if(!profile.empty() && profile!="silenced")r.fail("unknown pipe profile '"+profile+"'");
p.silenced=profile=="silenced";
r.field("diameter_mm",p.diameter,.001);r.field("length_mm",p.length,.001);r.field("cells",p.cells);
r.finish();e.engine.pipes.push_back(std::move(p));
}
validate_engine_config(e.engine,path.string());
require(e.junctions.junction_volume>0 && e.junctions.junction_volume<=1,path.string(),"junction_volume_cc outside (0,1000000]");
if(e.engine.has_silenced_exhaust()) {
require(e.exhaust.cell_length>=.002f && e.exhaust.cell_length<=1,path.string(),"silencers: cell length outside 2..1000 mm");
require(e.exhaust.diameter>0 && e.exhaust.diameter<=2 && e.exhaust.inlet_length>0 &&
e.exhaust.outlet_length>0 && e.exhaust.connecting_length>0,
path.string(),"silencers: invalid connecting pipe geometry");
float length=e.exhaust.inlet_length+e.exhaust.outlet_length+e.exhaust.connecting_length;
for(const auto& s:{e.exhaust.resonator,e.exhaust.muffler}) {
require(s.volume>0 && s.volume<=1 && s.length>0 && s.core_diameter>0 && s.core_diameter<=2 &&
s.passage_diameter>0 && s.passage_diameter<=2 && s.chambers>=1 && s.chambers<=16 &&
s.resistance>=0 && s.loss_coefficient>=0,path.string(),"silencers: invalid chamber geometry or resistance");
length+=s.length;
}
require(length/e.exhaust.cell_length+6*(e.exhaust.resonator.chambers+e.exhaust.muffler.chambers)+6<=4096,
path.string(),"silencers: mesh exceeds 4096 cells");
try {const auto pipe=make_silenced_exhaust(e.exhaust);require(pipe.cells.size()<=4096,path.string(),"silencers: mesh exceeds 4096 cells");}
catch(const std::exception& error) {root.fail(std::string("silencers: ")+error.what());}
}
} else root.fail("cycle must be two_stroke or four_stroke");
root.finish();return e;
}
std::vector<ScenarioExample> load_engine_directory(const std::filesystem::path& directory) {
require(std::filesystem::is_directory(directory),directory.string(),"engine directory does not exist");
std::vector<std::filesystem::path> files;
for(const auto& entry:std::filesystem::directory_iterator(directory))
if(entry.is_regular_file() && entry.path().extension()==".toml")files.push_back(entry.path());
std::sort(files.begin(),files.end());
std::vector<ScenarioExample> engines;std::set<std::string> ids;
for(const auto& file:files) {
auto engine=load_engine_file(file);
require(ids.insert(engine.id).second,file.string(),"duplicate engine id");
engines.push_back(std::move(engine));
}
require(!engines.empty(),directory.string(),"no .toml engine definitions found");
std::sort(engines.begin(),engines.end(),[](const auto& a,const auto& b) {
return a.order==b.order?a.id<b.id:a.order<b.order;
});
return engines;
}
std::filesystem::path engine_directory() {
if(const char* override_path=std::getenv("FLUIDSIM_ENGINE_DIR");override_path && *override_path)return override_path;
std::vector<std::filesystem::path> candidates{std::filesystem::current_path()/"engines"};
#ifdef _WIN32
std::wstring executable(32768,L'\0');
const auto size=GetModuleFileNameW(nullptr,executable.data(),static_cast<DWORD>(executable.size()));
if(size>0 && size<executable.size()) {executable.resize(size);candidates.push_back(std::filesystem::path(executable).parent_path()/"engines");}
#elif defined(__linux__)
std::error_code error;
const auto executable=std::filesystem::read_symlink("/proc/self/exe",error);
if(!error)candidates.push_back(executable.parent_path()/"engines");
#endif
candidates.emplace_back(FLUIDSIM_ENGINE_SOURCE_DIR);
for(const auto& candidate:candidates)if(std::filesystem::is_directory(candidate))return candidate;
throw std::runtime_error("Cannot find engines/. Set FLUIDSIM_ENGINE_DIR to the engine definitions directory.");
}
const std::vector<ScenarioExample>& scenario_examples() {
static const auto engines=load_engine_directory(engine_directory());return engines;
}
const ScenarioExample& scenario_example(const std::string& id) {
const auto& engines=scenario_examples();
const auto found=std::find_if(engines.begin(),engines.end(),[&](const auto& e) {return e.id==id;});
if(found==engines.end())throw std::runtime_error("Unknown engine id '"+id+"' in "+engine_directory().string());
return *found;
}
const ScenarioExample& preset_engine_definition(FourStrokePreset preset) {
return scenario_example(preset==FourStrokePreset::AmericanV8?"v8-h":preset==FourStrokePreset::SaabB205?"saab-straight":"twin");
}
} // namespace fluidsim::port

View File

@@ -1,4 +1,5 @@
#include "FluidSim/Desktop/EngineRenderer3D.hpp" #include "FluidSim/Desktop/EngineRenderer3D.hpp"
#include "FluidSim/ScenarioGeometry.hpp"
#include "FluidSim/Desktop/PressurePalette.hpp" #include "FluidSim/Desktop/PressurePalette.hpp"
#include "FluidSim/Desktop/PipePath.hpp" #include "FluidSim/Desktop/PipePath.hpp"
#include <SFML/OpenGL.hpp> #include <SFML/OpenGL.hpp>
@@ -61,12 +62,7 @@ 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);} 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);} Vec3 pressure_color(float pressure) {return pressure_tint(pressure);}
void color(Vec3 c) {glColor3f(c.x,c.y,c.z);} void color(Vec3 c) {glColor3f(c.x,c.y,c.z);}
struct Route {std::array<Vec3,5> points{};std::array<float,5> fractions{};unsigned count{};}; using Route = PipeRoute;
// Scene points specify a fraction of the pipe's physical length, converted
// to metre-based PipeAnchor distances when constructing the cached surface.
Route route(std::initializer_list<PipeAnchor> p) {
Route r;for(auto v:p) {r.points[r.count]=v.position;r.fractions[r.count++]=v.distance;}return r;
}
Vec3 cross(Vec3 a,Vec3 b) {return {a.y*b.z-a.z*b.y,a.z*b.x-a.x*b.z,a.x*b.y-a.y*b.x};} 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;} 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};} 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};}
@@ -162,55 +158,58 @@ struct EngineRenderer3D::Impl {
glPopMatrix(); glPopMatrix();
} }
void engine(const ScenarioSnapshot& s) { void engine(const ScenarioSnapshot& s) {
const auto routes=scenario_pipe_routes(s);
if(s.two_stroke) { if(s.two_stroke) {
cylinder_part({0,0,0},s.cylinder_piston_fraction1,s.cylinder_pressure1,s.cylinder_angles[0]); 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({0,.07f,0},{.21f,.14f,.19f},pressure_color(s.crankcase_pressure));
box({-.55f,.35f,0},{.18f,.12f,.15f},pressure_color(s.intake_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,0,routes[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,1,routes[1]);
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,2,routes[2]);
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}}})); pipe(s,3,routes[3]);
} else { } else {
const int n=s.cylinder_count;const bool v8=s.v8_headers; 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 bool banked=s.layout!=EngineLayout::Inline;const int per_bank=banked?n/2:n;
const float spacing=.21f,first=-(per_bank-1)*spacing*.5f; const float spacing=.21f,first=-(per_bank-1)*spacing*.5f;
box({0,.64f,0},{per_bank*.21f,.12f,.15f},pressure_color(s.intake_pressure)); 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}}})); pipe(s,0,routes[0]);
for(int i=0;i<n;++i) { for(int i=0;i<n;++i) {
const float x=first+(i%per_bank)*spacing,z=0; const float x=first+(i%per_bank)*spacing,z=0;
const float bank_tilt=s.layout==EngineLayout::Boxer?90.f:s.bank_angle*.5f; const float bank_tilt=s.layout==EngineLayout::Boxer?90.f:s.bank_angle*.5f;
const float tilt=banked?(i<per_bank?-bank_tilt:bank_tilt):0; const float tilt=banked?(i<per_bank?-bank_tilt:bank_tilt):0;
const auto port=[&](float y,float dx) {float a=tilt*pi3/180;return Vec3{x+dx,.1f+(y-.1f)*std::cos(a),z+(y-.1f)*std::sin(a)};};
cylinder_part({x,0,z},s.piston_fractions[i],s.cylinder_pressures[i],s.cylinder_angles[i],tilt); cylinder_part({x,0,z},s.piston_fractions[i],s.cylinder_pressures[i],s.cylinder_angles[i],tilt);
const float cz=v8?(i<4?-.39f:.39f):.26f; pipe(s,1+i,routes[1+i]);
const auto intake=port(.45f,-.055f),exhaust=port(.43f,.055f); if(!v8) pipe(s,1+n+i,routes[1+n+i]);
pipe(s,1+i,route({{0,{x,.58f,0}},{.3f,{x,.55f,intake.z*.5f}},{.7f,{intake.x,.52f,intake.z}},{1,intake}}));
if(!v8) pipe(s,1+n+i,route({{0,exhaust},{.3f,{x+.12f,exhaust.y,cz}},{.6f,{x+.12f,.17f,cz}},{1,{.68f,.17f,cz}}}));
} }
if(v8) { if(v8) {
const std::array<int,8> pair{0,0,1,1,2,3,2,3};
for(int i=0;i<8;++i) { for(int i=0;i<8;++i) {
const int bank=i/4, group=pair[i]%2; pipe(s,9+i,routes[9+i]);
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) { for(int i=0;i<4;++i) {
const float z=i<2?-.39f:.39f; pipe(s,17+i,routes[17+i]);
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}}})); if(s.h_pipe) pipe(s,25,routes[25]);
}
if(s.staged_headers) {
const int exit=1+2*n, groups=n==3?1:2, secondary=exit+(s.dual_exhaust?2:1);
for(int group=0;group<groups;++group) {
const float z=group==0?-.26f:.26f;
pipe(s,secondary+group,routes[secondary+group]);
box({.68f,.17f,z},{.04f,.04f,.04f},pressure_color(s.pipe_states[secondary+group].front().pressure));
}
for(int bank=0;bank<(s.dual_exhaust?2:1);++bank) {
pipe(s,exit+bank,routes[exit+bank]);
}
rod({first-.07f,.1f,0},{first+(per_bank-1)*spacing+.07f,.1f,0},.022f,{.56f,.56f,.56f});
return;
} }
for(int bank=0;bank<(v8?2:1);++bank) { for(int bank=0;bank<(v8?2:1);++bank) {
float z=v8?(bank==0?-.39f:.39f):.26f; 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(!v8) box({.68f,.17f,z},{.12f,.10f,.12f},pressure_color(bank==0?s.exhaust_pressure:s.second_bank_pressure));
if(s.silencers) { if(s.silencers) {
const auto& c=s.exhaust_config; 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; const float start=.74f;
pipe(s,1+2*n,route({{0,{start,.17f,z}},{1,{start+total,.17f,z}}})); pipe(s,1+2*n,routes[1+2*n]);
float x=start+c.inlet_length; float x=start+c.inlet_length;
for(int index=0;index<2;++index) { for(int index=0;index<2;++index) {
if(index) x+=c.connecting_length; if(index) x+=c.connecting_length;
@@ -219,9 +218,9 @@ struct EngineRenderer3D::Impl {
x+=silencer.length; x+=silencer.length;
} }
} else if(v8) { } else if(v8) {
pipe(s,21+bank,route({{0,{1.14f,.17f,z}},{1,{1.69f,.17f,z}}})); pipe(s,21+bank,routes[21+bank]);
pipe(s,23+bank,route({{0,{1.69f,.17f,z}},{1,{2.94f,.17f,z}}})); pipe(s,23+bank,routes[23+bank]);
} else pipe(s,1+2*n,route({{0,{.74f,.17f,z}},{1,{1.45f,.17f,z}}})); } else pipe(s,1+2*n,routes[1+2*n]);
if(bank==0)rod({first-.07f,.1f,0},{first+(per_bank-1)*spacing+.07f,.1f,0},.022f,{.56f,.56f,.56f}); if(bank==0)rod({first-.07f,.1f,0},{first+(per_bank-1)*spacing+.07f,.1f,0},.022f,{.56f,.56f,.56f});
} }
} }
@@ -236,8 +235,8 @@ void EngineRenderer3D::draw(sf::RenderWindow& window,const ScenarioSnapshot& sta
glViewport(x,y,w,h);glEnable(GL_SCISSOR_TEST);glScissor(x,y,w,h);glClear(GL_DEPTH_BUFFER_BIT); 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_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); 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; glMatrixMode(GL_PROJECTION);glLoadIdentity();const double nearClip=.03,halfHeight=nearClip*.41421356;
glFrustum(-top*w/h,top*w/h,-top,top,near,50); glFrustum(-halfHeight*w/h,halfHeight*w/h,-halfHeight,halfHeight,nearClip,50);
glMatrixMode(GL_MODELVIEW);glLoadIdentity(); glMatrixMode(GL_MODELVIEW);glLoadIdentity();
const GLfloat light[]{-.4f,1,1,0},ambient[]{.35f,.35f,.35f,1},diffuse[]{1,1,1,1}; 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); glLightfv(GL_LIGHT0,GL_POSITION,light);glLightfv(GL_LIGHT0,GL_DIFFUSE,diffuse);glLightModelfv(GL_LIGHT_MODEL_AMBIENT,ambient);

View File

@@ -196,7 +196,31 @@ void draw_b205_scene(sf::RenderWindow& window, const sf::Font* font, const Scena
void draw_schematic(sf::RenderWindow& window, const sf::Font* font, const ScenarioSnapshot& state) { void draw_schematic(sf::RenderWindow& window, const sf::Font* font, const ScenarioSnapshot& state) {
if (state.cylinder_count == 8) { if(state.staged_headers) {
const int n=state.cylinder_count, groups=n==3?1:2, split=n==3?n:(n+1)/2;
const int exit=1+2*n, secondary=exit+(state.dual_exhaust?2:1);
draw_label(window,font,"PULSE HEADERS / SMALL JUNCTION COLLECTORS",{25,185},sf::Color::White,16);
for(int i=0;i<n;++i) {
const int group=groups==1?0:(i<split?0:1);
const float x=40+(i%split)*145.f, top=group==0?235.f:500.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,1+i,top-15,x-10,x+90);
draw_pipe(window,state,1+n+i,top+155,x-10,x+90);
draw_graph_line(window,{x+90,top+155},{950,top+155},{200,120,80});
}
for(int group=0;group<groups;++group) {
const float y=group==0?390.f:655.f;
draw_pipe(window,state,secondary+group,y,950,1080);
const float out_y=state.dual_exhaust?y:480.f;
draw_graph_line(window,{1080,y},{1110,out_y},{200,120,80});
}
for(int bank=0;bank<(state.dual_exhaust?2:1);++bank)
draw_pipe(window,state,exit+bank,state.dual_exhaust?(bank==0?390.f:655.f):480.f,1110,1260);
draw_label(window,font,state.dual_exhaust?"SEPARATE BANK OUTLETS":"STAGED MERGE / SINGLE OUTLET",{950,710},sf::Color::White,11);
}
else 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_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_pipe(window, state, 0, 205, 25, 115);

View File

@@ -1,4 +1,5 @@
#include "FluidSim/Scenario.hpp" #include "FluidSim/Scenario.hpp"
#include "FluidSim/ScenarioCatalog.hpp"
#include "FluidSim/HelmholtzScenario.hpp" #include "FluidSim/HelmholtzScenario.hpp"
#include "FluidSim/PulseTestScenario.hpp" #include "FluidSim/PulseTestScenario.hpp"
#include "FluidSim/TwoStrokeScenario.hpp" #include "FluidSim/TwoStrokeScenario.hpp"
@@ -10,12 +11,12 @@
#include <iostream> #include <iostream>
#include <string> #include <string>
int main(int argc, char** argv) { int main(int argc, char** argv) try {
using namespace fluidsim::port; using namespace fluidsim::port;
float duration_seconds = 1.0f; float duration_seconds = 1.0f;
float throttle = 0.35f; float throttle = 0.35f;
std::string scenario_name = "twin"; std::string scenario_name = "twin";
TwoStrokeConfig two_stroke_config; auto two_stroke_config=scenario_example("yz-racing").two;
PipeSolver pipe_solver=PipeSolver::Rusanov; PipeSolver pipe_solver=PipeSolver::Rusanov;
for (int i = 1; i + 1 < argc; ++i) { for (int i = 1; i + 1 < argc; ++i) {
const std::string option = argv[i]; const std::string option = argv[i];
@@ -107,4 +108,7 @@ int main(int argc, char** argv) {
return 2; return 2;
} }
return 0; return 0;
} catch(const std::exception& error) {
std::cerr<<error.what()<<'\n';
return 2;
} }

View File

@@ -10,6 +10,7 @@ namespace {
Vec3 sub(Vec3 a, Vec3 b) { return {a.x - b.x, a.y - b.y, a.z - b.z}; } 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); } float length(Vec3 a) { return std::sqrt(a.x * a.x + a.y * a.y + a.z * a.z); }
float distance(Vec3 a, Vec3 b) { return length(sub(a, b)); } float distance(Vec3 a, Vec3 b) { return length(sub(a, b)); }
bool finite(Vec3 v) { return std::isfinite(v.x) && std::isfinite(v.y) && std::isfinite(v.z); }
AudioFrame add(AudioFrame a, AudioFrame b) { return {a.left + b.left, a.right + b.right}; } AudioFrame add(AudioFrame a, AudioFrame b) { return {a.left + b.left, a.right + b.right}; }
} // namespace } // namespace
const char *scene_name(OutdoorScene scene) { const char *scene_name(OutdoorScene scene) {
@@ -40,7 +41,10 @@ float OutdoorAudio::LowPass::process(float x) {
OutdoorAudio::OutdoorAudio(int rate, OutdoorAudioConfig config) : rate_(rate), config_(config), late_reverb_(rate) { OutdoorAudio::OutdoorAudio(int rate, OutdoorAudioConfig config) : rate_(rate), config_(config), late_reverb_(rate) {
if (rate < 8000 || !std::isfinite(config.sound_speed) || config.sound_speed <= 0 || if (rate < 8000 || !std::isfinite(config.sound_speed) || config.sound_speed <= 0 ||
!std::isfinite(config.monitor_cutoff_hz) || config.monitor_cutoff_hz <= 0 || !std::isfinite(config.monitor_cutoff_hz) || config.monitor_cutoff_hz <= 0 ||
!std::isfinite(config.pressure_gain) || config.pressure_gain < 0) !std::isfinite(config.pressure_gain) || config.pressure_gain < 0 ||
!std::isfinite(config.ear_spacing) || config.ear_spacing < 0 ||
!finite(config.listener) || !finite(config.intake_position) || !finite(config.exhaust_position) ||
!finite(config.second_exhaust_position))
throw std::invalid_argument("invalid outdoor audio configuration"); throw std::invalid_argument("invalid outdoor audio configuration");
const float norm = length(config_.listener_right); const float norm = length(config_.listener_right);
if (!std::isfinite(norm) || norm < 1e-6f) if (!std::isfinite(norm) || norm < 1e-6f)
@@ -55,10 +59,40 @@ OutdoorAudio::OutdoorAudio(int rate, OutdoorAudioConfig config) : rate_(rate), c
late_reverb_.tail_mix = 1; late_reverb_.tail_mix = 1;
late_reverb_.set_decay_times(.75f, .45f, .22f); late_reverb_.set_decay_times(.75f, .45f, .22f);
scene_alpha_ = 1 - std::exp(-1.0f / (rate_ * .02f)); scene_alpha_ = 1 - std::exp(-1.0f / (rate_ * .02f));
geometry_alpha_ = 1 - std::exp(-1.0f / (rate_ * .02f));
}
void OutdoorAudio::set_listener(AudioListener listener) {
const float norm=length(listener.right);
if(!finite(listener.position) || !std::isfinite(norm) || norm<1e-6f) return;
listener.right={listener.right.x/norm,listener.right.y/norm,listener.right.z/norm};
if(distance(config_.listener,listener.position)<1e-6f && distance(config_.listener_right,listener.right)<1e-6f)return;
config_.listener=listener.position;
config_.listener_right=listener.right;
update_geometry();
}
void OutdoorAudio::set_source_positions(const std::array<Vec3,3>& positions) {
for(auto p:positions)if(!finite(p))return;
config_.intake_position=positions[0];
config_.exhaust_position=positions[1];
config_.second_exhaust_position=positions[2];
update_geometry();
}
void OutdoorAudio::update_geometry() {
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);
} }
void OutdoorAudio::configure_emitter(Emitter &emitter, Vec3 source) { void OutdoorAudio::configure_emitter(Emitter &emitter, Vec3 source) {
const bool initial=emitter.filtered.empty();
emitter.filtered.resize(std::bit_ceil(std::size_t(rate_))); emitter.filtered.resize(std::bit_ceil(std::size_t(rate_)));
emitter.raw.resize(emitter.filtered.size()); // One second, validated path range. emitter.raw.resize(emitter.filtered.size()); // Power-of-two propagation history, at least one second.
std::size_t path_index=0;
const float center_distance=std::max(distance(source,config_.listener),.1f);
const float max_delay=float(emitter.filtered.size()-2);
const float center_samples=center_distance*rate_/config_.sound_speed;
emitter.target_center_delay=std::min(center_samples,max_delay);
emitter.target_center_gain=center_samples<=max_delay?1/center_distance:0;
if(initial) {emitter.center_delay=emitter.target_center_delay;emitter.center_gain=emitter.target_center_gain;}
const auto ear = [&](float sign) { const auto ear = [&](float sign) {
return Vec3{config_.listener.x + sign * config_.listener_right.x * config_.ear_spacing * .5f, return Vec3{config_.listener.x + sign * config_.listener_right.x * config_.ear_spacing * .5f,
config_.listener.y + sign * config_.listener_right.y * config_.ear_spacing * .5f, config_.listener.y + sign * config_.listener_right.y * config_.ear_spacing * .5f,
@@ -71,26 +105,31 @@ void OutdoorAudio::configure_emitter(Emitter &emitter, Vec3 source) {
const float dl = std::max(distance(image, ear(-1)), .1f), dr = std::max(distance(image, ear(1)), .1f); const float dl = std::max(distance(image, ear(-1)), .1f), dr = std::max(distance(image, ear(1)), .1f);
const float sl = (dl + extra) * rate_ / config_.sound_speed, const float sl = (dl + extra) * rate_ / config_.sound_speed,
sr = (dr + extra) * rate_ / config_.sound_speed; sr = (dr + extra) * rate_ / config_.sound_speed;
if (!std::isfinite(sl) || !std::isfinite(sr) || sl >= rate_ - 2 || sr >= rate_ - 2) // Very distant camera pans fade out instead of wrapping the delay line.
throw std::invalid_argument("outdoor propagation path exceeds one second"); p.delay_left = p.target_delay_left = std::min(sl,max_delay);
p.delay_left = std::size_t(sl); p.delay_right = p.target_delay_right = std::min(sr,max_delay);
p.delay_right = std::size_t(sr);
p.fraction_left = sl - p.delay_left;
p.fraction_right = sr - p.delay_right;
// A scatterer adds path length before the listener leg. Use spreading // A scatterer adds path length before the listener leg. Use spreading
// over the complete reflected path rather than multiplying two // over the complete reflected path rather than multiplying two
// inverse-distance losses, which made the woodland paths nearly // inverse-distance losses, which made the woodland paths nearly
// inaudible. // inaudible.
const float path_left = source_distance > 1.0f ? dl + source_distance : dl; const float path_left = source_distance > 1.0f ? dl + source_distance : dl;
const float path_right = source_distance > 1.0f ? dr + source_distance : dr; const float path_right = source_distance > 1.0f ? dr + source_distance : dr;
p.gain_left = coefficient / path_left; p.gain_left = p.target_gain_left = sl<=max_delay?coefficient / path_left:0;
p.gain_right = coefficient / path_right; p.gain_right = p.target_gain_right = sr<=max_delay?coefficient / path_right:0;
p.alpha = cutoff > 0 ? 1 - std::exp(-2 * pi * cutoff / rate_) : 1; p.alpha = cutoff > 0 ? 1 - std::exp(-2 * pi * cutoff / rate_) : 1;
emitter.paths.push_back(p); if(initial)emitter.paths.push_back(p);
else {
auto& current=emitter.paths[path_index];
current.target_delay_left=p.delay_left;current.target_delay_right=p.delay_right;
current.target_gain_left=p.gain_left;current.target_gain_right=p.gain_right;
}
++path_index;
}; };
path(source, 1, 0, 0); path(source, 1, 0, 0);
path({source.x, 2 * config_.ground_y - source.y, source.z}, config_.ground_reflection, 1, 1800); const bool ground_visible=(source.y-config_.ground_y)*(config_.listener.y-config_.ground_y)>=0;
path({2 * config_.wall_x - source.x, source.y, source.z}, config_.wall_reflection, 2, 4500); const bool wall_visible=(source.x-config_.wall_x)*(config_.listener.x-config_.wall_x)>=0;
path({source.x, 2 * config_.ground_y - source.y, source.z}, ground_visible?config_.ground_reflection:0, 1, 1800);
path({2 * config_.wall_x - source.x, source.y, source.z}, wall_visible?config_.wall_reflection:0, 2, 4500);
// Single-bounce point scatterers: geometric paths, empirical scattering strengths. // Single-bounce point scatterers: geometric paths, empirical scattering strengths.
for (Vec3 tree : for (Vec3 tree :
std::array<Vec3, 6>{{{-4, 1, 3}, {3, 1, 5}, {5, 1, -2}, {-6, 2, -3}, {-2, 1, 7}, {6, 2, 6}}}) { std::array<Vec3, 6>{{{-4, 1, 3}, {3, 1, 5}, {5, 1, -2}, {-6, 2, -3}, {-2, 1, 7}, {6, 2, 6}}}) {
@@ -102,20 +141,22 @@ void OutdoorAudio::configure_emitter(Emitter &emitter, Vec3 source) {
// distinct before the diffuse FDN tail builds up. // distinct before the diffuse FDN tail builds up.
constexpr float tunnel_width = 6.0f; constexpr float tunnel_width = 6.0f;
constexpr float tunnel_height = 4.0f; constexpr float tunnel_height = 4.0f;
const float tunnel_visible=config_.listener.y>=config_.ground_y && config_.listener.y<=tunnel_height &&
std::abs(config_.listener.z)<=tunnel_width*.5f?1.f:0.f;
const Vec3 ceiling{source.x, 2 * tunnel_height - source.y, source.z}; 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 left_wall{source.x, source.y, -tunnel_width - source.z};
const Vec3 right_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(ceiling, .82f*tunnel_visible, 4, 7200);
path(left_wall, .82f, 4, 6200); path(left_wall, .82f*tunnel_visible, 4, 6200);
path(right_wall, .82f, 4, 6200); path(right_wall, .82f*tunnel_visible, 4, 6200);
path({ceiling.x, ceiling.y, left_wall.z}, .68f, 4, 5600); path({ceiling.x, ceiling.y, left_wall.z}, .68f*tunnel_visible, 4, 5600);
path({ceiling.x, ceiling.y, right_wall.z}, .68f, 4, 5600); path({ceiling.x, ceiling.y, right_wall.z}, .68f*tunnel_visible, 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*tunnel_visible, 4, 4800);
path({source.x, source.y, 2 * tunnel_width + source.z}, .56f, 4, 4800); path({source.x, source.y, 2 * tunnel_width + source.z}, .56f*tunnel_visible, 4, 4800);
const float cutoff = std::min(config_.monitor_cutoff_hz, rate_ * .45f); const float cutoff = std::min(config_.monitor_cutoff_hz, rate_ * .45f);
const float w = 2 * pi * cutoff / rate_, cs = std::cos(w), alpha = std::sin(w) / std::sqrt(2.0f), const float w = 2 * pi * cutoff / rate_, cs = std::cos(w), alpha = std::sin(w) / std::sqrt(2.0f),
a0 = 1 + alpha; a0 = 1 + alpha;
emitter.filter = {(1 - cs) * .5f / a0, (1 - cs) / a0, (1 - cs) * .5f / a0, -2 * cs / a0, if(initial)emitter.filter = {(1 - cs) * .5f / a0, (1 - cs) / a0, (1 - cs) * .5f / a0, -2 * cs / a0,
(1 - alpha) / a0}; (1 - alpha) / a0};
} }
AudioFrame OutdoorAudio::render(Emitter &e, float flow, AudioFrame &dry, AudioFrame &raw) { AudioFrame OutdoorAudio::render(Emitter &e, float flow, AudioFrame &dry, AudioFrame &raw) {
@@ -123,15 +164,31 @@ AudioFrame OutdoorAudio::render(Emitter &e, float flow, AudioFrame &dry, AudioFr
e.previous_flow = flow; e.previous_flow = flow;
e.raw[e.position] = pressure; e.raw[e.position] = pressure;
e.filtered[e.position] = monitor_filter_ ? e.filter.process(pressure) : pressure; e.filtered[e.position] = monitor_filter_ ? e.filter.process(pressure) : pressure;
const auto read = [&](const std::vector<float> &data, std::size_t delay, float fraction) { const auto read = [&](const std::vector<float> &data, float samples) {
const auto delay=std::size_t(samples);
const float fraction=samples-delay;
const auto i = (e.position + data.size() - delay) & (data.size() - 1); const auto i = (e.position + data.size() - delay) & (data.size() - 1);
const auto j = (i + data.size() - 1) & (data.size() - 1); const auto j = (i + data.size() - 1) & (data.size() - 1);
return data[i] + (data[j] - data[i]) * fraction; return data[i] + (data[j] - data[i]) * fraction;
}; };
AudioFrame result{}; AudioFrame result{};
const auto smooth=[&](float& current,float target) {
const float next=current+(target-current)*geometry_alpha_;
// Finish the movement when float precision would stall the smoother.
current=next==current?target:next;
};
smooth(e.center_delay,e.target_center_delay);
smooth(e.center_gain,e.target_center_gain);
// The diffuse field is excited by a centre microphone, avoiding comb
// filtering from summing the differently delayed left and right ears.
reverb_input_+=read(e.filtered,e.center_delay)*e.center_gain;
for (auto &p : e.paths) { for (auto &p : e.paths) {
const float l = read(e.filtered, p.delay_left, p.fraction_left) * p.gain_left; smooth(p.delay_left,p.target_delay_left);
const float r = read(e.filtered, p.delay_right, p.fraction_right) * p.gain_right; smooth(p.delay_right,p.target_delay_right);
smooth(p.gain_left,p.target_gain_left);
smooth(p.gain_right,p.target_gain_right);
const float l = read(e.filtered, p.delay_left) * p.gain_left;
const float r = read(e.filtered, p.delay_right) * p.gain_right;
p.state_left += p.alpha * (l - p.state_left); p.state_left += p.alpha * (l - p.state_left);
p.state_right += p.alpha * (r - p.state_right); p.state_right += p.alpha * (r - p.state_right);
if (std::abs(p.state_left) < 1e-25f) if (std::abs(p.state_left) < 1e-25f)
@@ -142,8 +199,8 @@ AudioFrame OutdoorAudio::render(Emitter &e, float flow, AudioFrame &dry, AudioFr
result.right += p.state_right * weights_[p.group]; result.right += p.state_right * weights_[p.group];
if (p.group == 0) { if (p.group == 0) {
dry = add(dry, {l, r}); dry = add(dry, {l, r});
raw = add(raw, {read(e.raw, p.delay_left, p.fraction_left) * p.gain_left, raw = add(raw, {read(e.raw, p.delay_left) * p.gain_left,
read(e.raw, p.delay_right, p.fraction_right) * p.gain_right}); read(e.raw, p.delay_right) * p.gain_right});
} }
} }
e.position = (e.position + 1) & (e.filtered.size() - 1); e.position = (e.position + 1) & (e.filtered.size() - 1);
@@ -189,12 +246,13 @@ AudioFrame OutdoorAudio::process(float intake, float exhaust, float second_exhau
tail_mix_ += (target_tail - tail_mix_) * scene_alpha_; tail_mix_ += (target_tail - tail_mix_) * scene_alpha_;
last_dry_ = {}; last_dry_ = {};
last_raw_ = {}; last_raw_ = {};
reverb_input_ = 0;
const auto a = render(emitters_[0], intake, last_dry_, last_raw_); const auto a = render(emitters_[0], intake, last_dry_, last_raw_);
const auto b = render(emitters_[1], exhaust, last_dry_, last_raw_); const auto b = render(emitters_[1], exhaust, last_dry_, last_raw_);
const auto c = config_.dual_exhaust const auto c = config_.dual_exhaust
? render(emitters_[2], second_exhaust, last_dry_, last_raw_) ? render(emitters_[2], second_exhaust, last_dry_, last_raw_)
: AudioFrame{}; : AudioFrame{};
const auto tail = late_reverb_.process_stereo((last_dry_.left + last_dry_.right) * .5f); const auto tail = late_reverb_.process_stereo(reverb_input_);
return {a.left + b.left + c.left + tail.left * tail_mix_, a.right + b.right + c.right + tail.right * tail_mix_}; return {a.left + b.left + c.left + tail.left * tail_mix_, a.right + b.right + c.right + tail.right * tail_mix_};
} }
} // namespace fluidsim::port } // namespace fluidsim::port

View File

@@ -1,141 +1,62 @@
#include "FluidSim/Scenario.hpp" #include "FluidSim/Scenario.hpp"
#include "FluidSim/Constants.hpp" #include "FluidSim/Constants.hpp"
#include "FluidSim/ScenarioGeometry.hpp"
#include "FluidSim/ScenarioCatalog.hpp"
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
namespace fluidsim::port { namespace fluidsim::port {
FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset preset)
: FourStrokeScenario(sample_rate,preset_engine_definition(preset)) {}
FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selected_preset, ExhaustConfig exhaust, V8ExhaustConfig v8_exhaust, EngineConfig engine) FourStrokeScenario::FourStrokeScenario(int sample_rate,const ScenarioExample& definition)
: preset(selected_preset), exhaust_config(exhaust), v8_exhaust_config(v8_exhaust), engine_config(engine), : FourStrokeScenario(sample_rate,definition.engine,definition.exhaust,definition.junctions) {}
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), FourStrokeScenario::FourStrokeScenario(int sample_rate, EngineConfig configuration, ExhaustConfig exhaust, JunctionConfig fittings)
pipes(create_pipes(selected_preset, exhaust, v8_exhaust, engine)), dt_(1.f / std::max(sample_rate, 1)), : exhaust_config(exhaust), junction_config(fittings), engine_config(std::move(configuration)),
boundaries_(pipes), solver_(pipes, boundaries_), audio_(sample_rate, [&] { intake_plenum(engine_config.plenum_volume,101325,300),
exhaust_collector(engine_config.collector_volume,101325,700),
pipes(create_pipes(engine_config,exhaust)), dt_(1.f/std::max(sample_rate,1)),
boundaries_(pipes), solver_(pipes,boundaries_), audio_(sample_rate,[&] {
OutdoorAudioConfig config; OutdoorAudioConfig config;
config.dual_exhaust = selected_preset == FourStrokePreset::AmericanV8; config.dual_exhaust=engine_config.dual_outlets();
return config; return config;
}()) { }()) {
const bool saab = preset == FourStrokePreset::SaabB205; const auto& engine=engine_config;
const bool v8 = preset == FourStrokePreset::AmericanV8; const auto& junctions=junction_config;
const int count = cylinder_count(); const bool v8=engine.american_headers(),staged=engine.staged_headers();
if(engine.cylinders<0 || engine.cylinders>12 || (engine.layout!=EngineLayout::Inline && engine.cylinders%2)) throw std::invalid_argument("invalid engine cylinder layout"); const int count=cylinder_count();
starter_torque = v8 ? 180.f : saab ? 65.f : 18.f; static_cast<CrankshaftConfig&>(crankshaft)=engine.crankshaft;
crankshaft.omega = (saab ? 1200 : 2000) * 2 * pi / 60; static_cast<EcuConfig&>(ecu)=engine.ecu;
crankshaft.inertia = saab ? .18f : .04f; static_cast<VehicleConfig&>(vehicle)=engine.vehicle;
crankshaft.friction_constant = saab ? 12.f : 5.f; crankshaft.omega=engine.initial_rpm*2*pi/60;
crankshaft.friction_viscous = saab ? .012f : .003f; starter_torque=engine.starter_torque;
if (saab) { solver_.minimum_substeps=engine.minimum_substeps;
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;
vehicle.final_drive_ratio = 4.05f;
vehicle.gear_ratios = {13.70f/4.05f, 7.12f/4.05f, 4.77f/4.05f, 3.62f/4.05f, 2.67f/4.05f};
vehicle.wheel_radius = .317f;
vehicle.rotational_inertia = 3.6f;
vehicle.drag_coefficient = .32f;
vehicle.frontal_area = 2.2f;
vehicle.clutch_capacity = 300;
ecu.idle_rpm = 850;
ecu.idle_spark_gain = .025f;
ecu.idle_minimum_advance = -10;
ecu.rev_limit_rpm = 6500;
ecu.rev_resume_rpm = 6250;
ecu.overrun_cut_rpm = 2000;
ecu.overrun_resume_rpm = 1300;
}
if (v8) {
// Estimated mild 1980s 302-class pushrod small block and manual coupe.
crankshaft.omega = 1000 * 2*pi/60;
crankshaft.inertia = 1.1f;
crankshaft.friction_constant = 22;
crankshaft.friction_viscous = .025f;
vehicle.mass = 3000;
vehicle.primary_reduction = 1;
vehicle.final_drive_ratio = 6.16f / 1.7f; // 1.7x taller gearing in every gear.
vehicle.gear_ratios = {3.35f, 1.93f, 1.29f, 1.f, .68f};
vehicle.wheel_radius = .32f;
vehicle.clutch_capacity = 1100; // Hold V8 torque with the heavy flywheel/car.
vehicle.drag_coefficient = .4f;
vehicle.frontal_area = 2.f;
ecu.idle_rpm = 750;
ecu.idle_base_opening = .08f;
ecu.idle_spark_gain = .02f;
ecu.ignition_map = {{0, 10}, {750, 12}, {2500, 28}, {4000, 32}, {6000, 30}};
ecu.rev_limit_rpm = 7000;
ecu.rev_resume_rpm = 6750;
ecu.overrun_cut_rpm = 2000;
ecu.overrun_resume_rpm = 1100;
// 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) { if(engine.carbureted) {
ecu.idle_control=Ecu::IdleControl::FixedOpening;ecu.idle_base_opening=engine.idle_opening; carb_=std::make_unique<Carburetor>(engine.throttle_diameter);
static_cast<CarburetorConfig&>(*carb_)=engine.fuel;
// Carburetors keep metering fuel during both limiter and overrun cuts.
ecu.rev_limit_fuel_cut_enabled=false;
ecu.overrun_fuel_cut_enabled=false; ecu.overrun_fuel_cut_enabled=false;
carb_=std::make_unique<Carburetor>(.085f); ecu.overrun_ignition_cut_enabled=true;
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); intake_plenum.add_fuel_mass(intake_plenum.mass()/14.7f);
} }
}
vehicle.set_neutral(); vehicle.set_neutral();
vehicle.clutch_input = clutch; vehicle.clutch_input = clutch;
const auto engines = cylinders(); const auto engines = cylinders();
const std::array<float, 8> phases{0, pi, 3*pi, 2*pi}; // I4 firing: 1-3-4-2 for (int i=0;i<count;++i) {
const std::array<int, 8> firing_order{1, 5, 4, 2, 6, 3, 7, 8}; auto& c=*engines[i];
for (int i = 0; i < count; ++i) { static_cast<CylinderConfig&>(c)=engine.cylinder;
auto& c = *engines[i]; c.phase=engine.phase_degrees.at(i)*pi/180;
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);
c.bore = .1016f;
c.stroke = .0762f;
c.rod = .129f;
c.compression = 7.0f;
c.intake_open = 350; c.intake_close = 570;
c.exhaust_open = 140; c.exhaust_close = 370;
c.burn_duration = 65;
}
if (saab) {
c.bore = .090f;
c.stroke = .078f;
c.rod = .153f; // estimated, not a verified B205 rod measurement
c.compression = 8.8f;
c.intake_valve_count = c.exhaust_valve_count = 2;
}
c.intake_valve_diameter = saab ? .032f : .030f;
c.intake_valve_lift = saab ? .0085f : .008f;
c.exhaust_valve_diameter = saab ? .028f : .026f;
c.exhaust_valve_lift = saab ? .008f : .0075f;
if (v8) {
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.transported_fuel=engine.carbureted;
} c.rev_limit_enabled = ecu.rev_limit_enabled;
c.rev_limit_enabled = true;
c.rev_limit_rpm = ecu.rev_limit_rpm; c.rev_limit_rpm = ecu.rev_limit_rpm;
c.rev_limit_hysteresis = 250; c.rev_limit_hysteresis = 250;
c.rev_limit_cut_time = .020f; c.rev_limit_cut_time = .020f;
c.ecu = &ecu; c.ecu = &ecu;
c.initialize(crankshaft); c.initialize(crankshaft);
if (saab || v8 || engine.cylinders) { if (engine.initialize_phased_charge) {
// Initialize each phased cylinder at its actual piston position. // Initialize each phased cylinder at its actual piston position.
c.volume = c.compute_volume(c.phase); c.volume = c.compute_volume(c.phase);
c.air_mass = ambient_density * c.volume; c.air_mass = ambient_density * c.volume;
@@ -148,13 +69,13 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selecte
header_area_ = pipes[1+count].area; header_area_ = pipes[1+count].area;
tail_area_ = pipes[1+2*count].area; tail_area_ = pipes[1+2*count].area;
for (auto& pipe : pipes) { for (auto& pipe : pipes) {
pipe.damping_multiplier = saab ? 0.f : 1.f; pipe.damping_multiplier = engine.pipe_damping;
pipe.energy_relaxation_rate = 0; pipe.energy_relaxation_rate = 0;
} }
auto& throttle_port = intake_plenum.create_port(); auto& throttle_port = intake_plenum.create_port();
throttle_port_=&throttle_port; throttle_port_=&throttle_port;
solver_.add_component(intake_plenum); solver_.add_component(intake_plenum);
if(!v8) solver_.add_component(exhaust_collector); if(!v8 && !staged) solver_.add_component(exhaust_collector);
boundaries_.add_orifice(throttle_port, 0, false, throttle_area_, .7f); boundaries_.add_orifice(throttle_port, 0, false, throttle_area_, .7f);
for (int i = 0; i < count; ++i) { for (int i = 0; i < count; ++i) {
auto& feed = intake_plenum.create_port(); auto& feed = intake_plenum.create_port();
@@ -164,11 +85,11 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selecte
boundaries_.add_orifice(feed, 1+i, true, runner_area_); boundaries_.add_orifice(feed, 1+i, true, runner_area_);
boundaries_.add_orifice(engines[i]->intake, 1+i, false, intake_valve_areas_[i]); boundaries_.add_orifice(engines[i]->intake, 1+i, false, intake_valve_areas_[i]);
boundaries_.add_orifice(engines[i]->exhaust, 1+count+i, true, exhaust_valve_areas_[i]); boundaries_.add_orifice(engines[i]->exhaust, 1+count+i, true, exhaust_valve_areas_[i]);
if (!v8) boundaries_.add_orifice(exhaust_collector.create_port(), 1+count+i, false, header_area_); if (!v8 && !staged) boundaries_.add_orifice(exhaust_collector.create_port(), 1+count+i, false, header_area_);
} }
if(v8) { if(v8) {
auto junction=[&](std::initializer_list<JunctionEnd> ends) { auto junction=[&](std::initializer_list<JunctionEnd> ends) {
auto fitting=std::make_unique<Junction>(v8_exhaust.junction_volume,101325,700); auto fitting=std::make_unique<Junction>(junctions.junction_volume,101325,700);
fitting->connect(boundaries_,pipes,std::span(ends.begin(),ends.size())); fitting->connect(boundaries_,pipes,std::span(ends.begin(),ends.size()));
solver_.add_component(*fitting);exhaust_junctions_.push_back(std::move(fitting)); solver_.add_component(*fitting);exhaust_junctions_.push_back(std::move(fitting));
}; };
@@ -179,7 +100,7 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selecte
junction({{14,false},{16,false},{20,true}}); junction({{14,false},{16,false},{20,true}});
junction({{17,false},{18,false},{21,true}}); junction({{17,false},{18,false},{21,true}});
junction({{19,false},{20,false},{22,true}}); junction({{19,false},{20,false},{22,true}});
if(v8_exhaust.h_pipe) { if(junctions.h_pipe) {
junction({{21,false},{23,true},{25,true}}); junction({{21,false},{23,true},{25,true}});
junction({{22,false},{24,true},{25,false}}); junction({{22,false},{24,true},{25,false}});
} else { } else {
@@ -189,6 +110,33 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selecte
} }
boundaries_.add_radiating_open_end(23,false,101325,pipes[23].area); boundaries_.add_radiating_open_end(23,false,101325,pipes[23].area);
boundaries_.add_radiating_open_end(24,false,101325,pipes[24].area); boundaries_.add_radiating_open_end(24,false,101325,pipes[24].area);
} else if(staged) {
const int exit=1+2*count;
const bool dual=engine.dual_outlets();
const int groups=engine.header_groups(), secondary=exit+(dual?2:1);
auto junction=[&](const std::vector<JunctionEnd>& ends) {
auto fitting=std::make_unique<Junction>(junctions.junction_volume,101325,700);
fitting->connect(boundaries_,pipes,ends);
solver_.add_component(*fitting);exhaust_junctions_.push_back(std::move(fitting));
};
for(int group=0;group<groups;++group) {
std::vector<JunctionEnd> ends;
for(int i=0;i<count;++i)
if(groups==1 || (i<(count+1)/2?0:1)==group) ends.push_back({1+count+i,false});
ends.push_back({secondary+group,true});junction(ends);
}
if(dual) {
// Preserve independent bank pulse trains all the way to their mouths.
for(int bank=0;bank<2;++bank) {
junction({{secondary+bank,false},{exit+bank,true}});
boundaries_.add_radiating_open_end(exit+bank,false,101325,pipes[exit+bank].area);
}
} else {
std::vector<JunctionEnd> ends{{exit,true}};
for(int group=0;group<groups;++group) ends.push_back({secondary+group,false});
junction(ends);
boundaries_.add_radiating_open_end(exit,false,101325,pipes[exit].area);
}
} else { } else {
auto& collector_out = exhaust_collector.create_port(); auto& collector_out = exhaust_collector.create_port();
const int exit=1+2*count; const int exit=1+2*count;
@@ -196,11 +144,12 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selecte
boundaries_.add_radiating_open_end(exit,false,101325,pipes[exit].area); boundaries_.add_radiating_open_end(exit,false,101325,pipes[exit].area);
} }
boundaries_.add_radiating_open_end(0, true, 101325, pipes[0].area); boundaries_.add_radiating_open_end(0, true, 101325, pipes[0].area);
audio_.set_source_positions(scenario_sound_sources(snapshot()));
} }
AudioFrame FourStrokeScenario::process() { AudioFrame FourStrokeScenario::process() {
const std::lock_guard guard(mutex_); const std::lock_guard guard(mutex_);
step_motorcycle(crankshaft, dt_, preset == FourStrokePreset::AmericanV8 ? 300.f : preset == FourStrokePreset::SaabB205 ? 200.f : 30.f); step_motorcycle(crankshaft, dt_, engine_config.maximum_load_torque);
ecu.step(dt_, throttle, crankshaft.omega * 60 / (2*pi)); ecu.step(dt_, throttle, crankshaft.omega * 60 / (2*pi));
const auto engines = cylinders(); const auto engines = cylinders();
for (int i = 0; i < cylinder_count(); ++i) { for (int i = 0; i < cylinder_count(); ++i) {
@@ -212,21 +161,21 @@ AudioFrame FourStrokeScenario::process() {
} }
const float opening = ecu.throttle_opening(); const float opening = ecu.throttle_opening();
const float angle = opening * pi * .5f; const float angle = opening * pi * .5f;
throttle_area_ = area_for_diameter(engine_config.cylinders ? (engine_config.carbureted?.085f:.065f) : preset == FourStrokePreset::AmericanV8 ? .065f : preset == FourStrokePreset::SaabB205 ? .052f : .026f) * (1-std::cos(angle)); throttle_area_ = area_for_diameter(engine_config.throttle_diameter) * (1-std::cos(angle));
if(carb_) { if(carb_) {
const auto inlet=pipes[0].state(int(pipes[0].cells.size())-1); 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()); 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_); intake_plenum.add_fuel_mass(carb_->fuel_flow()*dt_);
} }
solver_.step(dt_); solver_.step(dt_);
intake_flow = boundaries_.open_end_mass_flow(preset == FourStrokePreset::AmericanV8 ? 2 : 1); intake_flow = boundaries_.open_end_mass_flow(engine_config.dual_outlets() ? 2 : 1);
exhaust_flow = boundaries_.open_end_mass_flow(0); exhaust_flow = boundaries_.open_end_mass_flow(0);
const float other_exhaust = preset == FourStrokePreset::AmericanV8 ? boundaries_.open_end_mass_flow(1) : 0.f; const float other_exhaust = engine_config.dual_outlets() ? boundaries_.open_end_mass_flow(1) : 0.f;
// Compact mouth radiation responds to volume velocity. Use ambient-equivalent // Compact mouth radiation responds to volume velocity. Use ambient-equivalent
// mass flow so hot/cold donor-density changes do not enter the derivative. // 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 intake_source = ambient_density * boundaries_.open_end_volume_flow(engine_config.dual_outlets() ? 2 : 1);
const float exhaust_source = ambient_density * boundaries_.open_end_volume_flow(0); 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 float other_source = engine_config.dual_outlets() ? ambient_density * boundaries_.open_end_volume_flow(1) : 0.f;
const auto rendered = audio_.process(intake_source, exhaust_source, other_source); const auto rendered = audio_.process(intake_source, exhaust_source, other_source);
exhaust_flow += other_exhaust; exhaust_flow += other_exhaust;
++sample_count; ++sample_count;
@@ -241,6 +190,9 @@ void FourStrokeScenario::set_pipe_solver(PipeSolver solver) {
void FourStrokeScenario::set_audio_scene(OutdoorScene scene) { void FourStrokeScenario::set_audio_scene(OutdoorScene scene) {
const std::lock_guard<std::mutex> guard(mutex_); audio_.set_scene(scene); const std::lock_guard<std::mutex> guard(mutex_); audio_.set_scene(scene);
} }
void FourStrokeScenario::set_listener(AudioListener listener) {
const std::lock_guard guard(mutex_); audio_.set_listener(listener);
}
void FourStrokeScenario::set_monitor_filter(bool enabled) { void FourStrokeScenario::set_monitor_filter(bool enabled) {
const std::lock_guard<std::mutex> guard(mutex_); audio_.set_monitor_filter(enabled); const std::lock_guard<std::mutex> guard(mutex_); audio_.set_monitor_filter(enabled);
} }
@@ -283,107 +235,18 @@ void FourStrokeScenario::set_first_gear() {
vehicle.set_first_gear(); vehicle.set_first_gear();
} }
ScenarioSnapshot FourStrokeScenario::snapshot() const {
const std::lock_guard<std::mutex> guard(mutex_);
ScenarioSnapshot result;
result.layout=engine_config.cylinders?engine_config.layout:preset==FourStrokePreset::AmericanV8?EngineLayout::V:EngineLayout::Inline;
result.bank_angle=engine_config.cylinders?engine_config.bank_angle:56;
result.v8_headers=preset==FourStrokePreset::AmericanV8;
result.silencers = preset == FourStrokePreset::SaabB205 && exhaust_config.enabled;
result.exhaust_config = exhaust_config;
result.h_pipe = preset==FourStrokePreset::AmericanV8 && v8_exhaust_config.h_pipe;
result.crossover_pressures = preset==FourStrokePreset::AmericanV8 ? std::array<float,2>{pipes[21].state(0).pressure,pipes[22].state(0).pressure} : std::array<float,2>{};
result.pipe_solver = pipes.front().solver;
result.solver_substeps = solver_.substep_count();
result.boundary_iterations = boundaries_.nonlinear_iterations;
result.rpm = crankshaft.omega * 60.0f / (2.0f * pi);
result.speed_kmh = vehicle.speed * 3.6f;
result.kinetic_energy = vehicle.kinetic_energy();
result.average_power = crankshaft.average_power;
result.average_torque = crankshaft.average_torque;
result.cylinder_pressure1 = cylinder1.pressure;
result.cylinder_pressure2 = cylinder2.pressure;
result.cylinder_temperature1 = cylinder1.temperature();
result.cylinder_temperature2 = cylinder2.temperature();
result.cylinder_piston_fraction1 = (cylinder1.volume - cylinder1.clearance_volume()) / cylinder1.swept_volume();
result.cylinder_piston_fraction2 = (cylinder2.volume - cylinder2.clearance_volume()) / cylinder2.swept_volume();
result.valve1_intake_area = intake_valve_areas_[0];
result.valve1_exhaust_area = exhaust_valve_areas_[0];
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 = 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 = carb_?carb_->indicated_afr():ecu.target_afr();
result.idle_control = ecu.idle_active();
result.fuel_cut = ecu.fuel_cut();
result.audio_scene = audio_.scene();
result.clutch = clutch;
result.intake_flow = intake_flow;
result.exhaust_flow = exhaust_flow;
result.gear = vehicle.current_gear;
result.rev_limit1 = cylinder1.rev_limit_active;
result.rev_limit2 = cylinder2.rev_limit_active;
result.cylinder_count = cylinder_count();
result.rev_limit_rpm = ecu.rev_limit_rpm;
const std::array<const FourStrokeCylinder*, 12> engines{&cylinder1, &cylinder2, &cylinder3, &cylinder4,
&cylinder5, &cylinder6, &cylinder7, &cylinder8,&cylinder9,&cylinder10,&cylinder11,&cylinder12};
for (int i = 0; i < cylinder_count(); ++i) {
result.cylinder_angles[i] = crankshaft.angle + engines[i]->phase;
result.cylinder_pressures[i] = engines[i]->pressure;
result.piston_fractions[i] = (engines[i]->volume-engines[i]->clearance_volume())/engines[i]->swept_volume();
result.intake_areas[i] = intake_valve_areas_[i];
result.exhaust_areas[i] = exhaust_valve_areas_[i];
}
result.capture_pipes(pipes);
return result;
}
float FourStrokeScenario::area_for_diameter(float diameter) { float FourStrokeScenario::area_for_diameter(float diameter) {
return pi * diameter * diameter * 0.25f; return pi * diameter * diameter * 0.25f;
} }
std::vector<Pipe1D> FourStrokeScenario::create_pipes(FourStrokePreset preset, const ExhaustConfig& exhaust, const V8ExhaustConfig& v8_exhaust, const EngineConfig& engine) { std::vector<Pipe1D> FourStrokeScenario::create_pipes(const EngineConfig& engine,const ExhaustConfig& exhaust) {
if(engine.cylinders) { validate_engine_config(engine);
if(engine.cylinders<1||engine.cylinders>12||engine.bore<=0||engine.stroke<=0||engine.rod<=engine.stroke*.5f)throw std::invalid_argument("invalid custom engine dimensions"); std::vector<Pipe1D> pipes;
std::vector<Pipe1D> result; for(const auto& pipe:engine.pipes)for(int i=0;i<pipe.count;++i) {
result.emplace_back(area_for_diameter(.075f),.20f,4); if(pipe.silenced)pipes.push_back(make_silenced_exhaust(exhaust));
for(int i=0;i<engine.cylinders;++i)result.emplace_back(area_for_diameter(.035f),.25f,5); else pipes.emplace_back(area_for_diameter(pipe.diameter),pipe.length,pipe.cells);
for(int i=0;i<engine.cylinders;++i)result.emplace_back(area_for_diameter(.038f),.65f,13);
result.emplace_back(area_for_diameter(.060f),1.5f,30);
return result;
} }
const bool saab = preset == FourStrokePreset::SaabB205; return pipes;
if (preset == FourStrokePreset::AmericanV8) {
// One inlet/throttle -> shared plenum -> short individual runners.
// Bank collectors feed twin pipes joined by a finite-length H crossover.
// Cells stay long enough to avoid routine
// extra CFL substeps during high-vacuum coast-down.
std::vector<Pipe1D> result;
result.emplace_back(area_for_diameter(.070f), .18f, 3);
for (int i = 0; i < 8; ++i)
result.emplace_back(area_for_diameter(.038f), .12f, 2);
for (int i = 0; i < 8; ++i)
result.emplace_back(area_for_diameter(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;
std::vector<Pipe1D> result;
result.reserve(4+2*count);
result.emplace_back(area_for_diameter(saab ? .060f : .0275f), saab ? .15f : .06f, saab ? 8 : 10);
for (int i = 0; i < count; ++i)
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), saab ? 1.4f : .40f, saab ? 40 : 10);
if (saab) result.back() = make_silenced_exhaust(exhaust);
return result;
} }
} // namespace fluidsim::port } // namespace fluidsim::port

84
src/scenario_geometry.cpp Normal file
View File

@@ -0,0 +1,84 @@
#include "FluidSim/ScenarioGeometry.hpp"
#include "FluidSim/ScenarioSnapshot.hpp"
#include <cmath>
#include <initializer_list>
namespace fluidsim::port {
namespace {
struct Anchor { float fraction; Vec3 point; };
PipeRoute route(std::initializer_list<Anchor> anchors) {
PipeRoute result;
for (auto a : anchors) {
result.fractions[result.count] = a.fraction;
result.points[result.count++] = a.point;
}
return result;
}
}
std::array<PipeRoute, 32> scenario_pipe_routes(const ScenarioSnapshot& s) {
std::array<PipeRoute, 32> routes{};
if (s.two_stroke) {
routes[0]=route({{0,{-1.05f,.35f,0}},{1,{-.64f,.35f,0}}});
routes[1]=route({{0,{-.46f,.35f,0}},{1.f/3,{-.28f,.35f,0}},{2.f/3,{-.28f,.1f,0}},{1,{-.105f,.1f,0}}});
routes[2]=route({{0,{.105f,.09f,0}},{1.f/3,{.18f,.09f,-.12f}},{2.f/3,{.18f,.35f,-.12f}},{1,{.05f,.35f,0}}});
routes[3]=route({{0,{.055f,.40f,0}},{1.f/3,{.3f,.40f,0}},{2.f/3,{.58f,.23f,0}},{1,{1.45f,.23f,0}}});
return routes;
}
const int n=s.cylinder_count;
const bool banked=s.layout!=EngineLayout::Inline;
const int per_bank=banked?n/2:n;
const float first=-(per_bank-1)*.21f*.5f;
routes[0]=route({{0,{-1.05f,.64f,0}},{.25f,{-.5f,.64f,0}},{.5f,{-.5f,.64f,-.1f}},{.75f,{0,.64f,-.1f}},{1,{0,.64f,0}}});
for(int i=0;i<n;++i) {
const float x=first+(i%per_bank)*.21f;
const float tilt=banked?(i<per_bank?-1.f:1.f)*(s.layout==EngineLayout::Boxer?90.f:s.bank_angle*.5f):0;
const auto port=[&](float y,float dx) {const float a=tilt*.017453293f;return Vec3{x+dx,.1f+(y-.1f)*std::cos(a),(y-.1f)*std::sin(a)};};
const auto intake=port(.45f,-.055f),exhaust=port(.43f,.055f);
routes[1+i]=route({{0,{x,.58f,0}},{.3f,{x,.55f,intake.z*.5f}},{.7f,{intake.x,.52f,intake.z}},{1,intake}});
const int group=s.staged_headers?(n==3?0:(i<(n+1)/2?0:1)):0;
const float z=s.staged_headers?(group==0?-.26f:.26f):.26f;
if(!s.v8_headers)routes[1+n+i]=route({{0,exhaust},{.3f,{x+.12f,exhaust.y,z}},{.6f,{x+.12f,.17f,z}},{1,{.68f,.17f,z}}});
}
if(s.v8_headers) {
const std::array<int,8> pair{0,0,1,1,2,3,2,3};
for(int i=0;i<8;++i) {
const float x=first+(i%4)*.21f,z=i/4?.39f:-.39f,a=(i/4?28.f:-28.f)*.017453293f;
const float end_z=z+(pair[i]%2?.09f:-.09f);
routes[9+i]=route({{0,{x+.055f,.1f+.33f*std::cos(a),.33f*std::sin(a)}},{.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;
routes[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)routes[25]=route({{0,{1.69f,.17f,-.39f}},{1,{1.69f,.17f,.39f}}});
}
if(s.staged_headers) {
const int exit=1+2*n,groups=n==3?1:2,secondary=exit+(s.dual_exhaust?2:1);
for(int group=0;group<groups;++group) {
const float z=group==0?-.26f:.26f;
routes[secondary+group]=route({{0,{.68f,.17f,z}},{1,{1.20f,.17f,s.dual_exhaust?z:0.f}}});
}
for(int bank=0;bank<(s.dual_exhaust?2:1);++bank) {
const float z=s.dual_exhaust?(bank==0?-.26f:.26f):0;
routes[exit+bank]=route({{0,{1.20f,.17f,z}},{1,{2.7f,.17f,z}}});
}
} else for(int bank=0;bank<(s.v8_headers?2:1);++bank) {
const float z=s.v8_headers?(bank==0?-.39f:.39f):.26f;
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;
routes[1+2*n]=route({{0,{.74f,.17f,z}},{1,{.74f+total,.17f,z}}});
} else if(s.v8_headers) {
routes[21+bank]=route({{0,{1.14f,.17f,z}},{1,{1.69f,.17f,z}}});
routes[23+bank]=route({{0,{1.69f,.17f,z}},{1,{2.94f,.17f,z}}});
} else routes[1+2*n]=route({{0,{.74f,.17f,z}},{1,{1.45f,.17f,z}}});
}
return routes;
}
std::array<Vec3,3> scenario_sound_sources(const ScenarioSnapshot& s) {
const auto routes=scenario_pipe_routes(s);
const int exhaust=s.two_stroke?3:s.v8_headers?23:1+2*s.cylinder_count;
const auto end=[&](int index) {const auto& r=routes[index];return r.points[r.count-1];};
return {routes[0].points[0],end(exhaust),s.dual_exhaust?end(exhaust+1):end(exhaust)};
}
}

69
src/scenario_snapshot.cpp Normal file
View File

@@ -0,0 +1,69 @@
#include "FluidSim/Scenario.hpp"
#include <cmath>
namespace fluidsim::port {
ScenarioSnapshot FourStrokeScenario::snapshot() const {
const std::lock_guard<std::mutex> guard(mutex_);
ScenarioSnapshot result;
result.layout=engine_config.layout;
result.bank_angle=engine_config.bank_angle;
result.v8_headers=engine_config.american_headers();
result.staged_headers=engine_config.staged_headers();
result.dual_exhaust=engine_config.dual_outlets();
result.silencers = engine_config.has_silenced_exhaust() && exhaust_config.enabled;
result.exhaust_config = exhaust_config;
result.h_pipe = engine_config.american_headers() && junction_config.h_pipe;
result.crossover_pressures = engine_config.american_headers() ? std::array<float,2>{pipes[21].state(0).pressure,pipes[22].state(0).pressure} : std::array<float,2>{};
result.pipe_solver = pipes.front().solver;
result.solver_substeps = solver_.substep_count();
result.boundary_iterations = boundaries_.nonlinear_iterations;
result.rpm = crankshaft.omega * 60.0f / (2.0f * pi);
result.speed_kmh = vehicle.speed * 3.6f;
result.kinetic_energy = vehicle.kinetic_energy();
result.average_power = crankshaft.average_power;
result.average_torque = crankshaft.average_torque;
result.cylinder_pressure1 = cylinder1.pressure;
result.cylinder_pressure2 = cylinder2.pressure;
result.cylinder_temperature1 = cylinder1.temperature();
result.cylinder_temperature2 = cylinder2.temperature();
result.cylinder_piston_fraction1 = (cylinder1.volume - cylinder1.clearance_volume()) / cylinder1.swept_volume();
result.cylinder_piston_fraction2 = (cylinder2.volume - cylinder2.clearance_volume()) / cylinder2.swept_volume();
result.valve1_intake_area = intake_valve_areas_[0];
result.valve1_exhaust_area = exhaust_valve_areas_[0];
result.valve2_intake_area = intake_valve_areas_[1];
result.valve2_exhaust_area = exhaust_valve_areas_[1];
result.intake_pressure = intake_plenum.pressure();
const int exit=1+2*cylinder_count();
result.exhaust_pressure = result.v8_headers ? pipes[21].state(0).pressure :
result.staged_headers ? pipes[exit].state(0).pressure : exhaust_collector.pressure();
result.second_bank_pressure = result.v8_headers ? pipes[22].state(0).pressure :
result.dual_exhaust ? pipes[exit+1].state(0).pressure : 101325;
result.throttle = ecu.throttle_opening();
result.pedal = throttle;
result.target_afr = carb_?carb_->indicated_afr():ecu.target_afr();
result.idle_control = ecu.idle_active();
result.fuel_cut = ecu.fuel_cut();
result.ignition_cut = ecu.ignition_cut();
result.audio_scene = audio_.scene();
result.clutch = clutch;
result.intake_flow = intake_flow;
result.exhaust_flow = exhaust_flow;
result.gear = vehicle.current_gear;
result.rev_limit1 = cylinder1.rev_limit_active;
result.rev_limit2 = cylinder2.rev_limit_active;
result.cylinder_count = cylinder_count();
result.rev_limit_rpm = ecu.rev_limit_rpm;
const std::array<const FourStrokeCylinder*, 12> engines{&cylinder1, &cylinder2, &cylinder3, &cylinder4,
&cylinder5, &cylinder6, &cylinder7, &cylinder8,&cylinder9,&cylinder10,&cylinder11,&cylinder12};
for (int i = 0; i < cylinder_count(); ++i) {
result.cylinder_angles[i] = crankshaft.angle + engines[i]->phase;
result.cylinder_pressures[i] = engines[i]->pressure;
result.piston_fractions[i] = (engines[i]->volume-engines[i]->clearance_volume())/engines[i]->swept_volume();
result.intake_areas[i] = intake_valve_areas_[i];
result.exhaust_areas[i] = exhaust_valve_areas_[i];
}
result.capture_pipes(pipes);
return result;
}
}

View File

@@ -80,7 +80,7 @@ void TwoStrokeCylinder::handle_cycle_events(float previous_degrees, float curren
const float spark_angle = wrap_degrees(360.0f - spark_advance, 360.0f); const float spark_angle = wrap_degrees(360.0f - spark_advance, 360.0f);
const bool crossed_spark = crossed_angle(previous_degrees,current_degrees,spark_angle); const bool crossed_spark = crossed_angle(previous_degrees,current_degrees,spark_angle);
if (crossed_spark && fuel_injected && !combustion_active && if (crossed_spark && fuel_injected && !combustion_active &&
!(ecu ? ecu->fuel_cut() : rev_limit_active) && !(ecu ? ecu->ignition_cut() : rev_limit_active) &&
random_unit() >= misfire_probability) { random_unit() >= misfire_probability) {
combustion_active = true; combustion_active = true;
burn_fraction = 0.0f; burn_fraction = 0.0f;

View File

@@ -1,5 +1,7 @@
#include "FluidSim/TwoStrokeScenario.hpp" #include "FluidSim/TwoStrokeScenario.hpp"
#include "FluidSim/Constants.hpp" #include "FluidSim/Constants.hpp"
#include "FluidSim/ScenarioGeometry.hpp"
#include "FluidSim/Scenario.hpp"
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
@@ -18,15 +20,20 @@ TwoStrokeScenario::TwoStrokeScenario(int sample_rate, TwoStrokeConfig config)
boundaries_(pipes), solver_(pipes, boundaries_), audio_(sample_rate) { boundaries_(pipes), solver_(pipes, boundaries_), audio_(sample_rate) {
if (!std::isfinite(config.idle_slide_opening) || config.idle_slide_opening<0 || config.idle_slide_opening>1) 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]"); throw std::invalid_argument("carb idle slide opening must be in [0,1]");
static_cast<EcuConfig&>(ecu)=config.ecu;
static_cast<VehicleConfig&>(vehicle)=config.vehicle;
ecu.idle_control = Ecu::IdleControl::FixedOpening; ecu.idle_control = Ecu::IdleControl::FixedOpening;
carburetor.pilot_jet_diameter*=config.jet_scale; carburetor.pilot_jet_diameter*=config.jet_scale*config.pilot_jet_scale;
carburetor.main_jet_diameter*=config.jet_scale; carburetor.main_jet_diameter*=config.jet_scale*config.main_jet_scale;
carburetor.needle_jet_diameter*=config.jet_scale; carburetor.needle_jet_diameter*=config.jet_scale*config.needle_jet_scale;
ecu.throttle_time_constant = .0002f; carburetor.needle_low_scale=config.needle_low_scale;carburetor.needle_mid_scale=config.needle_mid_scale;
ecu.throttle_rate = 5000; carburetor.needle_transition_scale=config.needle_transition_scale;
crankshaft.omega = 3000.0f * 2.0f * pi / 60.0f; carburetor.needle_off_idle_scale=config.needle_off_idle_scale;carburetor.needle_tip_scale=config.needle_tip_scale;
crankshaft.inertia = 0.01f*(config.bore*config.bore*config.stroke)/(.054f*.054f*.0545f); carburetor.pilot_transition_scale=config.pilot_transition_scale;carburetor.pilot_tip_scale=config.pilot_tip_scale;
starter_torque = 8.f; carburetor.needle_quarter_scale=config.needle_quarter_scale;carburetor.needle_upper_scale=config.needle_upper_scale;
crankshaft.omega=config.initial_rpm*2*pi/60;
crankshaft.inertia=config.crankshaft_inertia;
starter_torque=config.starter_torque;
const float pressure_to_torque = cylinder.swept_volume() * 1e5f / (2 * pi); const float pressure_to_torque = cylinder.swept_volume() * 1e5f / (2 * pi);
const float piston_speed_per_omega = config.stroke / pi; const float piston_speed_per_omega = config.stroke / pi;
crankshaft.friction_constant = pressure_to_torque * config.friction_bar; crankshaft.friction_constant = pressure_to_torque * config.friction_bar;
@@ -37,24 +44,14 @@ TwoStrokeScenario::TwoStrokeScenario(int sample_rate, TwoStrokeConfig config)
cylinder.transfer_width = config.transfer_width; cylinder.transfer_width = config.transfer_width;
cylinder.exhaust_width = config.exhaust_width; cylinder.exhaust_width = config.exhaust_width;
cylinder.minimum_burn_seconds = config.minimum_burn_seconds; cylinder.minimum_burn_seconds = config.minimum_burn_seconds;
// Yamaha's 2022 YZ125 published ratios; mass includes a nominal 75 kg rider. cylinder.rev_limit_enabled=ecu.rev_limit_enabled;
vehicle.primary_reduction = 64.f / 19;
vehicle.final_drive_ratio = 49.f / 13;
vehicle.gear_ratios = {31.f/13, 29.f/15, 27.f/17, 23.f/17, 20.f/17, 19.f/18};
vehicle.mass = 95 + 75;
vehicle.wheel_radius = .3313f; // nominal 100/90-19 geometry; loaded radius unmeasured
// Estimated two-stroke ignition calibration, not a factory CDI map.
ecu.ignition_map = {{0, 10}, {3000, 18}, {6000, 24}, {9000, 18},
{11000, 12}, {13000, 6}, {15000, 2}, {18000, 0}};
ecu.idle_rpm = 3000;
ecu.rev_limit_enabled = false;
cylinder.rev_limit_enabled = false;
ecu.idle_base_opening = config.idle_slide_opening; ecu.idle_base_opening = config.idle_slide_opening;
ecu.idle_max_opening = .3f; ecu.idle_max_opening = .3f;
ecu.idle_spark_gain=0; ecu.idle_spark_gain=config.idle_spark_gain;
ecu.overrun_cut_rpm = 5000; ecu.idle_minimum_advance=config.idle_minimum_advance;
ecu.overrun_resume_rpm = 3500; ecu.rev_limit_fuel_cut_enabled = false;
ecu.overrun_fuel_cut_enabled = false; ecu.overrun_fuel_cut_enabled = false;
ecu.overrun_ignition_cut_enabled = true;
cylinder.ecu = &ecu; cylinder.ecu = &ecu;
cylinder.initialize(crankshaft); cylinder.initialize(crankshaft);
cylinder.set_crankcase(crankcase); cylinder.set_crankcase(crankcase);
@@ -92,6 +89,9 @@ TwoStrokeScenario::TwoStrokeScenario(int sample_rate, TwoStrokeConfig config)
solver_.add_component(intake_plenum); solver_.add_component(intake_plenum);
solver_.add_component(crankcase); solver_.add_component(crankcase);
solver_.add_component(cylinder); solver_.add_component(cylinder);
ScenarioSnapshot geometry;
geometry.two_stroke=true;
audio_.set_source_positions(scenario_sound_sources(geometry));
} }
AudioFrame TwoStrokeScenario::process() { AudioFrame TwoStrokeScenario::process() {
@@ -105,7 +105,7 @@ AudioFrame TwoStrokeScenario::process() {
carb_inlet.pressure, intake_plenum.pressure()); carb_inlet.pressure, intake_plenum.pressure());
intake_plenum.add_fuel_mass(carburetor.fuel_flow() * dt_); intake_plenum.add_fuel_mass(carburetor.fuel_flow() * dt_);
max_throttle_area_ = slide_area(configuration.carburetor_diameter, ecu.throttle_opening()); max_throttle_area_ = slide_area(configuration.carburetor_diameter, ecu.throttle_opening());
step_motorcycle(crankshaft, dt_, 10.0f); step_motorcycle(crankshaft, dt_, configuration.maximum_load_torque);
crankcase.pre_step(dt_); crankcase.pre_step(dt_);
cylinder.pre_step(crankshaft, dt_, ecu.throttle_opening()); cylinder.pre_step(crankshaft, dt_, ecu.throttle_opening());
transfer_valve_area_ = cylinder.valve_area(crankshaft.angle * 180.0f / pi, true); transfer_valve_area_ = cylinder.valve_area(crankshaft.angle * 180.0f / pi, true);
@@ -137,6 +137,7 @@ TwoStrokeSnapshot TwoStrokeScenario::snapshot() const {
cylinder.valve_area(crankshaft.angle * 180.0f / pi, false), cylinder.valve_area(crankshaft.angle * 180.0f / pi, false),
reed_open_, reed_open_,
ecu.fuel_cut(), ecu.fuel_cut(),
ecu.ignition_cut(),
throttle, throttle,
audio_.scene()}; audio_.scene()};
} }

View File

@@ -33,5 +33,18 @@ int main() {
camera.orbit({40,20});camera.pan({20,15},500);camera.zoom(2); camera.orbit({40,20});camera.pan({20,15},500);camera.zoom(2);
check(camera.pitch<=80 && camera.pitch>=-75 && camera.distance>.35f,"camera bounds"); check(camera.pitch<=80 && camera.pitch>=-75 && camera.distance>.35f,"camera bounds");
check(camera.target.x!=before.x || camera.target.y!=before.y,"camera panning must move target"); check(camera.target.x!=before.x || camera.target.y!=before.y,"camera panning must move target");
for(float yaw:{-90.f,-28.f,0.f,90.f,180.f})for(float pitch:{-60.f,0.f,70.f}) {
camera.yaw=yaw;camera.pitch=pitch;camera.target={.7f,.4f,-.2f};camera.distance=3;
const auto listener=camera.listener(1.8f,1.1f);
const float y=yaw*.017453293f,p=pitch*.017453293f;
const float dx=listener.position.x-camera.target.x-1.1f;
const float dy=listener.position.y-camera.target.y,dz=listener.position.z-camera.target.z;
const float x1=std::cos(y)*dx+std::sin(y)*dz,z1=-std::sin(y)*dx+std::cos(y)*dz;
check(std::abs(x1)<2e-6f && std::abs(std::cos(p)*dy-std::sin(p)*z1)<2e-6f &&
std::abs(std::sin(p)*dy+std::cos(p)*z1-5.4f)<2e-6f,
"audio listener must coincide with the rendered camera including fit offsets");
check(std::abs(std::cos(y)*listener.right.x+std::sin(y)*listener.right.z-1)<1e-6f,
"audio ears must rotate with the camera right axis");
}
std::cout<<"Desktop layout, spring pedal, keyboard travel and orbit camera checks passed\n"; std::cout<<"Desktop layout, spring pedal, keyboard travel and orbit camera checks passed\n";
} }

View File

@@ -0,0 +1,78 @@
#include "FluidSim/ScenarioCatalog.hpp"
#include "FluidSim/DesktopScenario.hpp"
#include <chrono>
#include <cmath>
#include <fstream>
#include <iostream>
#include <sstream>
#include <stdexcept>
using namespace fluidsim::port;
namespace {
void check(bool condition,const std::string& message) {if(!condition)throw std::runtime_error(message);}
std::string read(const std::filesystem::path& path) {
std::ifstream file(path);std::ostringstream text;text<<file.rdbuf();return text.str();
}
void replace(std::string& text,const std::string& from,const std::string& to) {
const auto pos=text.find(from);check(pos!=std::string::npos,"test fixture missing "+from);text.replace(pos,from.size(),to);
}
struct TemporaryDirectory {
std::filesystem::path path=std::filesystem::temp_directory_path()/
("fluidsim-engine-config-"+std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
TemporaryDirectory() {std::filesystem::create_directory(path);}
~TemporaryDirectory() {std::error_code error;std::filesystem::remove_all(path,error);}
};
}
int main() {
const auto& catalog=scenario_examples();
check(catalog.size()>=32,"bundled engines missing");
const auto& thumper=scenario_example("single-450");
check(thumper.engine.carbureted && std::abs(thumper.engine.cylinder.bore-.096f)<1e-7f,
"four-stroke units or fuel type changed");
check(std::abs(scenario_example("yz-racing").two.idle_slide_opening-.03f)<1e-7f,
"125 idle tune lost");
for(const auto& e:catalog) {
DesktopScenario s(e);s.set_controls(.35f,0,1);
for(int i=0;i<256;++i) {
const auto audio=s.process();
check(std::isfinite(audio.left) && std::isfinite(audio.right),e.id+": invalid audio after TOML load");
}
const auto state=s.snapshot();
check(state.rpm>0 && state.pipe_positivity_repairs==0,e.id+": startup changed");
}
TemporaryDirectory temp;
const auto file=temp.path/"test.toml";
const auto original=read(engine_directory()/"twin.toml");
const auto reject=[&](std::string text,const std::string& expected) {
{std::ofstream output(file);output<<text;}
bool rejected=false;
try {load_engine_file(file);}
catch(const std::exception& error) {
const std::string message=error.what();
check(message.find("test.toml")!=std::string::npos,"diagnostic lost the file path");
check(message.find(expected)!=std::string::npos,"unexpected diagnostic: "+message);
rejected=true;
}
check(rejected,"invalid config accepted: "+expected);
};
auto changed=original;replace(changed,"schema_version = 1","schema_version = 99");reject(changed,"schema_version");
changed=original;replace(changed,"bore_mm = 75.0","bore_mn = 75.0");reject(changed,"bore_mm");
changed=original;replace(changed,"bore_mm = 75.0","bore_mm = -75.0");reject(changed,"cylinder");
changed=original;replace(changed,"cylinders = 2","cylinders = 2.5");reject(changed,"integer");
changed=original;replace(changed,"layout = \"inline\"","layout = \"flatbread\"");reject(changed,"layout");
changed=original;replace(changed,"topology = \"collector\"","topology = \"american_v8\"");reject(changed,"V8");
changed=original;replace(changed,"cells = 10","cells = 1000000");reject(changed,"cell");
changed=original;replace(changed,"phase_degrees = [0.0, 180.0]","phase_degrees = [0.0]");reject(changed,"phase_degrees");
reject(original+"\n[unknown_section]\nvalue = 1\n","unknown field");
reject("schema_version = [","Error");
{std::ofstream output(file);output<<original;}
std::filesystem::copy_file(file,temp.path/"duplicate.toml");
bool duplicate=false;
try {load_engine_directory(temp.path);}catch(const std::exception& error) {
duplicate=std::string(error.what()).find("duplicate engine id")!=std::string::npos;
}
check(duplicate,"duplicate engine IDs accepted");
std::cout<<catalog.size()<<" engine imports, startup smoke checks and invalid-config diagnostics passed\n";
}

View File

@@ -1,5 +1,9 @@
#include "FluidSim/OutdoorAudio.hpp" #include "FluidSim/OutdoorAudio.hpp"
#include "FluidSim/ReedValve.hpp" #include "FluidSim/ReedValve.hpp"
#include "FluidSim/Scenario.hpp"
#include "FluidSim/ScenarioGeometry.hpp"
#include "FluidSim/DesktopScenario.hpp"
#include "FluidSim/ScenarioCatalog.hpp"
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <iostream> #include <iostream>
@@ -98,6 +102,85 @@ int main() {
} }
check(right < left, "source on right does not reach right ear first"); check(right < left, "source on right does not reach right ear first");
// A moved listener must converge to the same propagation as one created
// there, including the reversed ear orientation and the reflected field.
auto moved_config=stereo_config;
moved_config.listener={-.5f,1.5f,4};
moved_config.listener_right={-1,0,0};
OutdoorAudio moving(44100,stereo_config),destination(44100,moved_config);
moving.set_listener({moved_config.listener,moved_config.listener_right});
for(auto* p:{&moving,&destination})p->set_monitor_filter(false);
for(int i=0;i<44100;++i) {moving.process(0,0);destination.process(0,0);}
for(int i=0;i<6000;++i) {
const auto a=moving.process(0,.001f),b=destination.process(0,.001f);
check(std::abs(a.left-b.left)<2e-7f && std::abs(a.right-b.right)<2e-7f,
"moving listener does not update direct/reflected stereo paths");
}
// Exercise rapid pose changes while a wet tail is still ringing.
for(int i=0;i<22050;++i) {
if(i%735==0) {
const float angle=i*.0002f;
moving.set_listener({{3*std::sin(angle),1,3*std::cos(angle)},
{std::cos(angle),0,-std::sin(angle)}});
}
const auto a=moving.process(0,.001f);
check(std::isfinite(a.left) && std::isfinite(a.right) &&
std::abs(a.left)<1 && std::abs(a.right)<1,"moving wet paths became unstable");
}
// Source relocation must use geometry without resetting audio state.
auto source_config=config();source_config.exhaust_position={2,1,-1};
OutdoorAudio relocated(44100,config()),fixed_source(44100,source_config);
relocated.set_source_positions({source_config.intake_position,source_config.exhaust_position,
source_config.second_exhaust_position});
for(int i=0;i<44100;++i) {relocated.process(0,0);fixed_source.process(0,0);}
for(int i=0;i<3000;++i) {
const auto a=relocated.process(0,.001f),b=fixed_source.process(0,.001f);
check(std::abs(a.left-b.left)<2e-7f && std::abs(a.right-b.right)<2e-7f,
"relocated mouth does not match world source position");
}
ScenarioSnapshot geometry;
geometry.two_stroke=true;
auto mouths=scenario_sound_sources(geometry);
check(mouths[0].x==-1.05f && mouths[1].x==1.45f && mouths[1].y==.23f,
"two-stroke mouths are not on the drawn pipe ends");
geometry.two_stroke=false;geometry.cylinder_count=8;geometry.layout=EngineLayout::V;
geometry.v8_headers=true;geometry.dual_exhaust=true;
mouths=scenario_sound_sources(geometry);
check(mouths[1].x==2.94f && mouths[1].z==-.39f && mouths[2].z==.39f,
"dual V8 outlets are not at their separate tailpipe mouths");
geometry.v8_headers=false;geometry.dual_exhaust=false;geometry.silencers=true;
geometry.cylinder_count=4;geometry.layout=EngineLayout::Inline;
const auto& e=geometry.exhaust_config;
mouths=scenario_sound_sources(geometry);
check(std::abs(mouths[1].x-(.74f+e.inlet_length+e.resonator.length+e.connecting_length+
e.muffler.length+e.outlet_length))<1e-6f,
"silenced exhaust source does not follow the configured pipe length");
// Crossing a reflecting plane must not turn its virtual image into a
// source that arrives before the actual pipe mouth.
auto behind_config=config();behind_config.listener={-4,1,0};
OutdoorAudio behind_wall(44100,behind_config);
behind_wall.set_monitor_filter(false);
for(int i=0;i<4410;++i)behind_wall.process(0,0);
int first_behind=-1;
for(int i=0;i<1000;++i) {
const auto a=behind_wall.process(0,.001f);
if(first_behind<0 && std::abs(a.left)>1e-9f)first_behind=i;
}
check(first_behind==514,"reflection crosses wall and precedes the physical source");
for(const auto& example:scenario_examples()) {
DesktopScenario scenario(example.kind,example.two,example.exhaust,example.junctions,example.engine);
scenario.set_listener({{2,1,3},{1,0,0}});
const auto snapshot=scenario.snapshot();
const auto sources=scenario_sound_sources(snapshot);
for(auto source:sources)
check(std::isfinite(source.x) && std::isfinite(source.y) && std::isfinite(source.z),
"catalog scenario has an invalid sound source");
const auto frame=scenario.process();
check(std::isfinite(frame.left) && std::isfinite(frame.right),"catalog listener wiring failed");
}
ReedValve reed; ReedValve reed;
for (int i = 0; i < 10000; i++) for (int i = 0; i < 10000; i++)
reed.step(1.f / 44100, 100); reed.step(1.f / 44100, 100);

View File

@@ -10,7 +10,7 @@ int main() {
std::set<std::string> ids; std::set<std::string> ids;
for(const auto& example:scenario_examples()) { for(const auto& example:scenario_examples()) {
if(!ids.insert(example.id).second)throw std::runtime_error("duplicate example id"); 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); DesktopScenario scenario(example.kind,example.two,example.exhaust,example.junctions,example.engine,example.title);
scenario.set_audio_scene(OutdoorScene::Dry); scenario.set_audio_scene(OutdoorScene::Dry);
const auto started=std::chrono::steady_clock::now(); const auto started=std::chrono::steady_clock::now();
float peak_power=0; float peak_power=0;
@@ -25,9 +25,17 @@ int main() {
if(example.id=="big-carb" && peak_power<10000)throw std::runtime_error("big carb V8 failed to produce power"); 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(!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(example.engine.cylinders && (snapshot.cylinder_count!=example.engine.cylinders || snapshot.layout!=example.engine.layout))throw std::runtime_error("custom cylinder layout lost");
const bool custom_v8=example.engine.american_headers();
if(custom_v8 && (!snapshot.v8_headers || snapshot.h_pipe!=example.junctions.h_pipe || snapshot.pipe_states[25].empty()))
throw std::runtime_error("custom V8 lost American junction headers/crossover");
if(example.engine.staged_headers() && example.engine.layout==EngineLayout::Inline) {
if(!snapshot.staged_headers || snapshot.dual_exhaust)
throw std::runtime_error("inline engine lost staged junction collectors");
}
if(example.engine.staged_headers() && example.engine.layout==EngineLayout::V && (!snapshot.dual_exhaust || !snapshot.staged_headers))
throw std::runtime_error("large V engine lost separate bank outlets");
if(snapshot.example_name!=example.title)throw std::runtime_error("example name 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.engine.has_silenced_exhaust() && snapshot.silencers!=example.exhaust.enabled)throw std::runtime_error("example silencer selection lost");
if(example.kind==ScenarioKind::SaabB205 && snapshot.silencers!=example.exhaust.enabled)throw std::runtime_error("example silencer selection lost");
std::cout<<example.id<<" passed, rpm="<<snapshot.rpm<<", wall/sim="<<std::chrono::duration<double>(std::chrono::steady_clock::now()-started).count()/2<<std::endl; std::cout<<example.id<<" passed, rpm="<<snapshot.rpm<<", wall/sim="<<std::chrono::duration<double>(std::chrono::steady_clock::now()-started).count()/2<<std::endl;
} }
} }

16
third_party/tomlplusplus/LICENSE vendored Normal file
View File

@@ -0,0 +1,16 @@
MIT License
Copyright (c) Mark Gillard <mark.gillard@outlook.com.au>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

8
third_party/tomlplusplus/README.md vendored Normal file
View File

@@ -0,0 +1,8 @@
# toml++
Vendored single-header parser, version **3.4.0**, from
[marzer/tomlplusplus](https://github.com/marzer/tomlplusplus/tree/v3.4.0).
`toml.hpp` is the upstream amalgamated header. Its MIT license is retained in
`LICENSE`. Only `src/engine_config.cpp` includes the third-party parser; public
configuration headers use project types.

17748
third_party/tomlplusplus/toml.hpp vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
#include "FluidSim/Scenario.hpp" #include "FluidSim/Scenario.hpp"
#include "FluidSim/ScenarioCatalog.hpp"
#include "FluidSim/TwoStrokeScenario.hpp" #include "FluidSim/TwoStrokeScenario.hpp"
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
@@ -65,9 +66,13 @@ struct Metrics {
} }
}; };
template <class Scenario> template <class Scenario>
void capture(const std::string &prefix, int frames, float pedal, OutdoorScene scene, TwoStrokeConfig config, PipeSolver solver, float fixed_rpm, float warmup) { void capture(const std::string &prefix, int frames, float pedal, OutdoorScene scene, TwoStrokeConfig config, PipeSolver solver, float fixed_rpm, float warmup, const ScenarioExample* example=nullptr) {
std::unique_ptr<Scenario> s; std::unique_ptr<Scenario> s;
if constexpr (std::is_same_v<Scenario, TwoStrokeScenario>) s = std::make_unique<Scenario>(44100, config); if constexpr (std::is_same_v<Scenario, TwoStrokeScenario>) s = std::make_unique<Scenario>(44100, config);
else if constexpr (std::is_same_v<Scenario, FourStrokeScenario>) {
if(example) s=std::make_unique<Scenario>(44100,*example);
else s=std::make_unique<Scenario>();
}
else s = std::make_unique<Scenario>(); else s = std::make_unique<Scenario>();
s->set_pipe_solver(solver); s->set_pipe_solver(solver);
s->throttle = pedal; s->throttle = pedal;
@@ -101,6 +106,7 @@ void capture(const std::string &prefix, int frames, float pedal, OutdoorScene sc
} // namespace } // namespace
int main(int argc, char **argv) { int main(int argc, char **argv) {
try { try {
std::string example_id;
std::string scenario = "twin", prefix = "build/audio/twin", scene = "wall"; std::string scenario = "twin", prefix = "build/audio/twin", scene = "wall";
float seconds = 6, pedal = 1, fixed_rpm = 0, warmup = 0; float seconds = 6, pedal = 1, fixed_rpm = 0, warmup = 0;
TwoStrokeConfig config; TwoStrokeConfig config;
@@ -110,7 +116,8 @@ int main(int argc, char **argv) {
if (i + 1 >= argc) if (i + 1 >= argc)
throw std::runtime_error("missing option value"); throw std::runtime_error("missing option value");
std::string value = argv[++i]; std::string value = argv[++i];
if (key == "--scenario") if(key=="--example") example_id=value;
else if (key == "--scenario")
scenario = value; scenario = value;
else if (key == "--out") else if (key == "--out")
prefix = value; prefix = value;
@@ -153,7 +160,14 @@ int main(int argc, char **argv) {
if (!std::filesystem::path(prefix).parent_path().empty()) if (!std::filesystem::path(prefix).parent_path().empty())
std::filesystem::create_directories(std::filesystem::path(prefix).parent_path()); std::filesystem::create_directories(std::filesystem::path(prefix).parent_path());
const int frames = int(std::lround(seconds * 44100)); const int frames = int(std::lround(seconds * 44100));
if (scenario == "twin") const auto examples=scenario_examples();
if(!example_id.empty()) {
const auto example=std::find_if(examples.begin(),examples.end(),[&](const auto& e){return e.id==example_id;});
if(example==examples.end()) throw std::runtime_error("unknown example: "+example_id);
if(example->kind==ScenarioKind::TwoStroke)
capture<TwoStrokeScenario>(prefix,frames,pedal,selected,example->two,solver,fixed_rpm,warmup);
else capture<FourStrokeScenario>(prefix,frames,pedal,selected,config,solver,fixed_rpm,warmup,&*example);
} else if (scenario == "twin")
capture<FourStrokeTwinScenario>(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup); capture<FourStrokeTwinScenario>(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup);
else if (scenario == "b205") else if (scenario == "b205")
capture<SaabB205Scenario>(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup); capture<SaabB205Scenario>(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup);

View File

@@ -0,0 +1,80 @@
#include "FluidSim/ScenarioCatalog.hpp"
#include "FluidSim/Scenario.hpp"
#include "FluidSim/TwoStrokeScenario.hpp"
#include <iostream>
#include <cmath>
#include <memory>
using namespace fluidsim::port;
int main(int argc,char** argv) {
const bool sweep=argc>1 && std::string(argv[1])=="--sweep";
const bool check=argc>1 && std::string(argv[1])=="--check";
bool failed=false;
int examples_checked=0;
std::cout<<"example,pedal,rpm,opening,carb_afr,charge_afr,free_idle_rpm,free_idle_afr,repairs\n";
for(auto e:scenario_examples()) {
if(e.kind!=ScenarioKind::TwoStroke && !e.engine.carbureted) continue;
if(sweep && argc>2 && e.id!=argv[2]) continue;
if(argc>1 && !check && !sweep && e.id!=argv[1]) continue;
++examples_checked;
const bool two=e.kind==ScenarioKind::TwoStroke;
if(!sweep && argc>2) {float k=std::stof(argv[2]);e.two.pilot_jet_scale*=k;e.engine.fuel.pilot_jet_diameter*=k;}
if(!sweep && argc>3) {float k=std::stof(argv[3]);e.two.main_jet_scale*=k;e.engine.fuel.main_jet_diameter*=k;}
if(!sweep && argc>4) {float k=std::stof(argv[4]);e.two.needle_jet_scale*=k;e.engine.fuel.needle_jet_diameter*=k;}
if(!sweep && argc>5) {float k=std::stof(argv[5]);e.two.idle_slide_opening=k;e.engine.ecu.idle_base_opening=k;}
if(!sweep && argc>6) {float k=std::stof(argv[6]);e.two.needle_low_scale*=k;e.engine.fuel.needle_low_scale*=k;}
if(!sweep && argc>7) {float k=std::stof(argv[7]);e.two.needle_mid_scale*=k;e.engine.fuel.needle_mid_scale*=k;}
for(int index=0;index<((sweep||check)?8:4);++index) {
const float pedal=(sweep||check)?std::array<float,8>{0,.05f,.1f,.15f,.25f,.5f,.75f,1}[index]:std::array<float,4>{0,.15f,.5f,1}[index];
const int point=pedal==0?0:pedal<.3f?1:pedal<.8f?2:3;
const float rpm=two?std::array<float,4>{3000,4000,6500,9000}[point]:
e.engine.cylinders==1?std::array<float,4>{1400,2500,4000,6000}[point]:std::array<float,4>{800,1800,3000,5000}[point];
std::unique_ptr<TwoStrokeScenario> t;
std::unique_ptr<FourStrokeScenario> f;
if(two) {t=std::make_unique<TwoStrokeScenario>(44100,e.two);t->set_controls(pedal,0,1);}
else {f=std::make_unique<FourStrokeScenario>(44100,e);f->set_controls(pedal,0,1);}
double afr=0,charge=0,opening=0;std::uint64_t repairs=0;
for(int i=0;i<55125;++i) {
if(two) {t->crankshaft.omega=rpm*2*pi/60;t->process();}
else {f->crankshaft.omega=rpm*2*pi/60;f->process();}
if(i>=44100 && i%10==0) {
afr+=(two?t->carburetor.indicated_afr():f->snapshot().target_afr)/1103.;
charge+=(two?t->crankcase.mass()*t->crankcase.air_fraction()/std::max(t->crankcase.fuel_mass(),1e-12f):f->intake_plenum.mass()*f->intake_plenum.air_fraction()/std::max(f->intake_plenum.fuel_mass(),1e-12f))/1103.;
opening+=(two?t->ecu.throttle_opening():f->ecu.throttle_opening())/1103.;
}
}
if(!std::isfinite(afr) || afr<11.5 || afr>(point==3?14.5:15.5)) {
std::cerr<<e.id<<": AFR outside calibration envelope at pedal "<<pedal<<": "<<afr<<'\n';failed=true;
}
float free_rpm=0,free_afr=0;
if(point==0) {
for(int i=0;i<132300;++i) {
if(two)t->process();else f->process();
if(i>=110250 && i%10==0) free_afr+=(two?t->carburetor.indicated_afr():f->snapshot().target_afr)/2205.;
}
free_rpm=(two?t->crankshaft.omega:f->crankshaft.omega)*60/(2*pi);
}
for(const auto& p:two?t->pipes:f->pipes)repairs+=p.positivity_repairs;
if(point==0) {
float idle_min=3000, idle_max=4000;
if(e.id=="fifty") {idle_min=3200;idle_max=4000;}
if(e.id=="fivehundred") {idle_min=2800;idle_max=3500;}
if(e.id=="single-450") {idle_min=1200;idle_max=1600;}
if(e.id=="big-carb") {idle_min=700;idle_max=900;}
if(!std::isfinite(free_rpm) || free_rpm<idle_min || free_rpm>idle_max ||
!std::isfinite(free_afr) || free_afr<11.5 || free_afr>15.5) {
std::cerr<<e.id<<": idle outside target RPM/AFR ("<<free_rpm<<", "<<free_afr<<")\n";failed=true;
}
}
if(repairs) {std::cerr<<e.id<<": pipe positivity repairs\n";failed=true;}
if(e.id=="yz-racing" && std::abs(opening-e.two.idle_slide_opening)>1e-6) {
if(point==0) {std::cerr<<"two-stroke idle stop did not reach its catalog setting\n";failed=true;}
}
if(e.id=="single-450" && (!f->cylinder1.transported_fuel || !e.engine.carbureted)) {
std::cerr<<"450 single must transport carburetor fuel\n";failed=true;
}
std::cout<<e.id<<','<<pedal<<','<<rpm<<','<<opening<<','<<afr<<','<<charge<<','<<free_rpm<<','<<free_afr<<','<<repairs<<std::endl;
}
}
if(!examples_checked) {std::cerr<<"No matching carbureted scenario\n";return 1;}
return (check||sweep) && failed ? 1 : 0;
}

View File

@@ -0,0 +1,71 @@
# Carburetor calibration
The catalog contains seven carbureted two-strokes and two carbureted
four-strokes. The 450 cc four-stroke is now carbureted; the 650 cc stays EFI.
The 125 variants use a 3% fixed slide stop plus closed-throttle ignition trim;
the 50/500 singles use 4.5%. The big-block uses 7%.
## Measurement
The `fluidsim_carb_calibration` tool imposes crankshaft RPM with an external
motoring/absorbing dyno, runs 1 s to settle, and averages the following 0.25 s.
Each operating point starts from a fresh warm scenario. At idle, the dyno is then
removed for a further 3 s to check that the engine continues running without
starter assistance or artificial sustaining torque.
| Family | Idle RPM | Low RPM | Mid RPM | Full-throttle RPM |
| --- | ---: | ---: | ---: | ---: |
| Two-strokes | 3000 | 4000 | 6500 | 9000 |
| 450 single | 1400 | 2500 | 4000 | 6000 |
| Big-block | 800 | 1800 | 3000 | 5000 |
Low points use 5%, 10%, 15% and 25% pedal; mid points use 50% and 75%.
The mechanical stop remains in force if pedal demand is lower than it.
AFR acceptance is 11.5?15.5, narrowed to 11.5?14.5 at full throttle. Free-idle
AFR must also stay within 11.5?15.5, with positive running RPM (300?10000).
The fixed slide sets idle airflow. Closed-throttle ignition timing trims speed
toward 3,000 RPM on the 125s, 3,000 RPM on the 50, and 3,000 RPM on the 500.
Four-stroke targets are 1,400 RPM for the 450 and 800 RPM for the big-block.
## Settled idle and carb-outlet AFR
| Scenario | Stop | Unloaded idle RPM | Idle AFR | 15% AFR | 50% AFR | Full AFR |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| Racing 125 | 3% | 3603 | 13.49 | 14.56 | 13.56 | 13.27 |
| Straight 125 | 3% | 3754 | 13.48 | 14.70 | 13.32 | 13.27 |
| Long chamber 125 | 3% | 3598 | 13.37 | 14.64 | 13.42 | 13.21 |
| Short chamber 125 | 3% | 3657 | 13.61 | 14.58 | 13.66 | 13.26 |
| Wide chamber 125 | 3% | 3679 | 13.44 | 14.37 | 13.58 | 13.27 |
| 50 two-stroke | 4.5% | 3591 | 14.46 | 14.71 | 13.64 | 12.92 |
| 500 two-stroke | 4.5% | 3128 | 13.63 | 13.23 | 13.85 | 13.26 |
| 450 four-stroke | 4.5% | 1362 | 13.54 | 14.10 | 13.67 | 13.07 |
| Big-block | 7% | 763 | 13.52 | 14.68 | 13.12 | 13.17 |
The previous 125 calibration measured approximately 7.6 at idle, 22.6 at low
pedal and 20.2 at full pedal. The previous big-block reached approximately 28.7
at full pedal. Independent pilot and main jet dimensions plus a continuous,
monotone needle annulus profile correct the off-idle and upper-throttle gaps.
These are effective flow calibrations, not measured commercial jet sizes.
The tool reports `charge_afr` separately: crankcase air/fuel for two-strokes,
intake-plenum air/fuel for four-strokes. Reverse flow, residual oxygen depletion,
wet-wall transients and transported unburned fuel make that differ from fresh
carb-outlet AFR. It is diagnostic rather than the calibration target. Combustion
still consumes only the oxygen and fuel actually present; AFR is not clamped to
hide a bad mixture and no feedback loop forces the target values.
## Reproduction
```powershell
cmake --build --preset windows
ctest --preset windows -R carb_calibration --output-on-failure
./out/build/windows-gcc/fluidsim_carb_calibration.exe --sweep yz-racing
```
The calibration sweep includes all eight throttle positions for every carb scenario,
target idle RPM and AFR, zero pipe positivity repairs, the catalog slide-stop settings,
and transported fueling on the 450. Catalog and two-stroke regressions also
exercise throttle changes, coast-down, clutch/gear operation and finite audio.
[Four-point AFR, charge, and idle measurements](carb_calibration.csv) include charge AFR,
actual idle opening, free-running idle RPM and pipe repair counts.

View File

@@ -0,0 +1,38 @@
# Custom exhaust pulse character
Compared the previous `src/scenario.cpp` from HEAD with the updated implementation
using the same capture tool, default Rusanov solver, dry scene, 55% pedal,
3,000 RPM, 0.6 s warmup and 0.5 s recording. No gain normalization or added
noise/distortion was applied. Captures are in `out/audio-character/`.
Energy fractions below use both channels, discard the first 20% of the recording,
remove DC and apply a Hann window. The denominator is spectral energy from 20 Hz
to 6 kHz. Upper harmonics are above 1.5 times the aggregate firing frequency
(RPM / 120 * cylinders); low orders are below 0.8 times that frequency. These
are engineering checks on pulse character, not a perceptual realism score.
| Example | Upper harmonics, before ? after | Low orders, before ? after |
| --- | --- | --- |
| Inline three | 78.6% ? 85.6% | <0.1% ? <0.1% |
| F20 inline four | 53.2% ? 87.0% | <0.1% ? 1.1% |
| Inline five | 51.6% ? 92.5% | <0.1% ? 0.3% |
| RB26 inline six | 21.4% ? 88.4% | <0.1% ? <0.1% |
| Long-stroke inline six | 29.0% ? 81.3% | <0.1% ? <0.1% |
| Big-block V8 | 40.3% ? 59.6% | <0.1% ? 23.2% |
| V10 | 84.6% ? 31.0% | <0.1% ? 14.4% |
| V12 | 52.2% ? 55.1% | 0.1% ? 19.7% |
The V10's energy shifts toward lower bank orders rather than simply becoming
brighter. All eight updated examples also completed fixed-RPM captures at 1,000
and 6,000 RPM using the same warmup/duration/pedal. All 24 updated captures
produced finite audio with zero clipping samples. Maximum rendered absolute
amplitude across these captures was 0.524 full scale.
The catalog regression runs each example through one second of throttle and one
second of pedal release. It checks finite audio, sustained positive RPM, zero
pipe positivity repairs, big-block power, and the new topology flags. The desktop
UI and physics regression tests cover the existing controls and base simulation.
For listening comparisons, match playback loudness: removing large collector
volumes increases level as well as changing spectrum. Geometry and calibration
remain estimates; these measurements do not establish a match to recorded engines.

View File

@@ -0,0 +1,37 @@
example,pedal,rpm,opening,carb_afr,charge_afr,free_idle_rpm,free_idle_afr,repairs
yz-racing,0,3000,0.03,13.4852,17.6506,3602.68,13.2998,0
yz-racing,0.15,4000,0.15,14.5615,14.5141,0,0,0
yz-racing,0.5,6500,0.5,13.5614,11.0273,0,0,0
yz-racing,1,9000,1,13.2665,9.14679,0,0,0
yz-straight,0,3000,0.03,13.4847,13.8691,3753.7,13.4844,0
yz-straight,0.15,4000,0.15,14.6983,14.6936,0,0,0
yz-straight,0.5,6500,0.5,13.3192,8.37308,0,0,0
yz-straight,1,9000,1,13.2662,8.6134,0,0,0
yz-torque,0,3000,0.03,13.3698,17.2726,3597.72,13.4608,0
yz-torque,0.15,4000,0.15,14.6447,14.6351,0,0,0
yz-torque,0.5,6500,0.5,13.4205,9.57616,0,0,0
yz-torque,1,9000,1,13.2114,9.72874,0,0,0
yz-short,0,3000,0.03,13.6119,18.2684,3657.15,12.7677,0
yz-short,0.15,4000,0.15,14.5772,14.5825,0,0,0
yz-short,0.5,6500,0.5,13.6625,11.2674,0,0,0
yz-short,1,9000,1,13.258,8.56493,0,0,0
yz-fat,0,3000,0.03,13.4371,18.0872,3679.1,13.3084,0
yz-fat,0.15,4000,0.15,14.3698,14.3344,0,0,0
yz-fat,0.5,6500,0.5,13.583,11.1147,0,0,0
yz-fat,1,9000,1,13.2664,9.04608,0,0,0
fifty,0,3000,0.045,14.4588,18.5582,3590.97,14.3795,0
fifty,0.15,4000,0.15,14.7126,14.8354,0,0,0
fifty,0.5,6500,0.5,13.6415,13.2838,0,0,0
fifty,1,9000,1,12.9222,9.32989,0,0,0
fivehundred,0,3000,0.045,13.6326,15.6191,3128.38,13.6017,0
fivehundred,0.15,4000,0.15,13.2298,13.1972,0,0,0
fivehundred,0.5,6500,0.5,13.8488,12.409,0,0,0
fivehundred,1,9000,1,13.2616,10.1366,0,0,0
single-450,0,1400,0.0449999,13.5449,14.5374,1362.41,13.4177,0
single-450,0.15,2500,0.15,14.103,14.0662,0,0,0
single-450,0.5,4000,0.499999,13.668,13.9308,0,0,0
single-450,1,6000,0.999999,13.0708,10.7667,0,0,0
big-carb,0,800,0.0699998,13.5196,13.7285,763.257,13.5412,0
big-carb,0.15,1800,0.15,14.6817,14.6857,0,0,0
big-carb,0.5,3000,0.499999,13.1217,13.1227,0,0,0
big-carb,1,5000,0.999999,13.1704,13.174,0,0,0
1 example pedal rpm opening carb_afr charge_afr free_idle_rpm free_idle_afr repairs
2 yz-racing 0 3000 0.03 13.4852 17.6506 3602.68 13.2998 0
3 yz-racing 0.15 4000 0.15 14.5615 14.5141 0 0 0
4 yz-racing 0.5 6500 0.5 13.5614 11.0273 0 0 0
5 yz-racing 1 9000 1 13.2665 9.14679 0 0 0
6 yz-straight 0 3000 0.03 13.4847 13.8691 3753.7 13.4844 0
7 yz-straight 0.15 4000 0.15 14.6983 14.6936 0 0 0
8 yz-straight 0.5 6500 0.5 13.3192 8.37308 0 0 0
9 yz-straight 1 9000 1 13.2662 8.6134 0 0 0
10 yz-torque 0 3000 0.03 13.3698 17.2726 3597.72 13.4608 0
11 yz-torque 0.15 4000 0.15 14.6447 14.6351 0 0 0
12 yz-torque 0.5 6500 0.5 13.4205 9.57616 0 0 0
13 yz-torque 1 9000 1 13.2114 9.72874 0 0 0
14 yz-short 0 3000 0.03 13.6119 18.2684 3657.15 12.7677 0
15 yz-short 0.15 4000 0.15 14.5772 14.5825 0 0 0
16 yz-short 0.5 6500 0.5 13.6625 11.2674 0 0 0
17 yz-short 1 9000 1 13.258 8.56493 0 0 0
18 yz-fat 0 3000 0.03 13.4371 18.0872 3679.1 13.3084 0
19 yz-fat 0.15 4000 0.15 14.3698 14.3344 0 0 0
20 yz-fat 0.5 6500 0.5 13.583 11.1147 0 0 0
21 yz-fat 1 9000 1 13.2664 9.04608 0 0 0
22 fifty 0 3000 0.045 14.4588 18.5582 3590.97 14.3795 0
23 fifty 0.15 4000 0.15 14.7126 14.8354 0 0 0
24 fifty 0.5 6500 0.5 13.6415 13.2838 0 0 0
25 fifty 1 9000 1 12.9222 9.32989 0 0 0
26 fivehundred 0 3000 0.045 13.6326 15.6191 3128.38 13.6017 0
27 fivehundred 0.15 4000 0.15 13.2298 13.1972 0 0 0
28 fivehundred 0.5 6500 0.5 13.8488 12.409 0 0 0
29 fivehundred 1 9000 1 13.2616 10.1366 0 0 0
30 single-450 0 1400 0.0449999 13.5449 14.5374 1362.41 13.4177 0
31 single-450 0.15 2500 0.15 14.103 14.0662 0 0 0
32 single-450 0.5 4000 0.499999 13.668 13.9308 0 0 0
33 single-450 1 6000 0.999999 13.0708 10.7667 0 0 0
34 big-carb 0 800 0.0699998 13.5196 13.7285 763.257 13.5412 0
35 big-carb 0.15 1800 0.15 14.6817 14.6857 0 0 0
36 big-carb 0.5 3000 0.499999 13.1217 13.1227 0 0 0
37 big-carb 1 5000 0.999999 13.1704 13.174 0 0 0