The EngineLab DSL is a safe declarative language that compiles to an
EngineConfig. It is used to pick a base and override values with explicit
units. The extensions recognised by the application are .els and .engine.
This language is not compatible with ES2D .mr files and does not yet try to
reproduce all of their expressiveness.
# Comments start with # or //.
preset inline_four
name "Street Turbo I4"
let runner = 30 cm + 20 mm
let primary = 48 cm
set idle_rpm = 900 rpm
set engine.redline_rpm = 7200 rpm
set ignition.rev_limit_rpm = 7350 rpm
set intake.runner_length_mm = runner
set intake.runner_diameter_mm = 42 mm
set exhaust.primary_length_mm = primary
set exhaust.outlet_diameter_mm = 70 mm
set forced_induction.enabled = true
set forced_induction.type = turbo
set forced_induction.pressure_ratio = 1.35
set cylinder.all.bore_mm = 86 mm
set cylinder.2.ignition_offset_deg = -2 deg
ignition clear
ignition point 800 rpm, 10 deg
ignition point 3500 rpm, 29 deg
ignition point 7350 rpm, 28 deg
The repository also contains examples/street-turbo.els.
Without a base instruction, the compiler starts from an inline four. The built-in presets are:
inline_two/i2,inline_four/i4,inline_five/i5;v6,v8;flat_six/boxer_six;radial_five.
A complete existing engine can replace that base:
base "../engines/my-engine.yaml"
name "Track variant"
set engine.redline_rpm = 8200 rpm
base accepts a v1 or v2 engine JSON or YAML. A v1 base is migrated in memory
and the compiled result uses the current v2 schema. The path is relative to the
file that holds the instruction.
include reads another script in the same variable and configuration context:
include "shared/intake.els"
include "shared/track-ignition.els"
Instruction order matters: a preset or base met later replaces the
configuration accumulated so far.
let defines a variable once. Names are case-insensitive. Expressions accept
+, -, *, /, parentheses, unary signs and pi.
Addition and subtraction require the same dimension. Multiplication only accepts one dimensional factor and one dimensionless factor. Dividing by a value of the same dimension produces a ratio; arbitrary compound units are not inferred.
Recognised units:
| Quantity | Symbols |
|---|---|
| ratio | ratio, %, percent, pct |
| length / volume | mm, cm, m / l, ml, cc, cm3 |
| mass | mg, g, kg |
| pressure / temperature | pa, kpa, bar / c, degc, celsius |
| angle / time | deg, degree, rad / us, ms, s, sec |
| frequency / engine speed | hz, khz / rpm |
| area | mm2, cm2, m2 |
| mass flow | mg_s, mgps, mg_per_s, g_s, kg_s |
| specific energy | j_kg, kj_kg |
| velocity / force | mm_s, m_s, mps / n |
| friction / inertia | ns_m / kg_m2 |
| power / torque | w, kw / nm |
For example 20 MPa is rejected because MPa is not in this list; write
200 bar or 20000 kpa.
set idle_rpm = ... is a shorthand for set engine.idle_rpm = .... The
families currently supported cover:
- engine: idle, mechanical rev limiter, inertia, friction, octane, ambient, cooling, bank angle, layout and name;
- global intake and exhaust geometry;
- rev limiter and spark curve;
- injection mode, window, rail, flow, film and cooling;
- physical cycle-to-cycle combustion variability and physical afterfire;
- solver frequency, sub-steps and resolution;
- forced-induction enable and main parameters;
- per-cylinder geometry, masses, friction, journal, bank and attenuation.
A cylinder.all target changes every cylinder. A numeric target uses the
cylinder identifier, never its position in the array:
set cylinder.all.compression_ratio = 10.5 ratio
set cylinder.7.exhaust_primary_length_mm = 620 mm
set cylinder.7.connecting_rod_type = articulated
An unknown property is an error; it is not ignored. The executable reference
list lives in the tables of src/scripting/src/EngineScriptCompiler.cpp.
The physical-audio-lab.els example
enables a moderate combustion spread and the afterfire reaction. The latter
schedules no pops: without unburned fuel, oxygen and hot enough gas in the
exhaust, it correctly stays silent.
Each error carries an ESxxx code, the file, the line and the column. The
compiler checks in particular:
- incompatible units, division by zero and non-finite results;
- missing cylinder identifier and unknown property;
- include cycles and a maximum depth of 32 files;
- sources larger than 2 MiB;
- reading and decoding a base file;
- normalisation and complete final validation of
EngineConfig.
The current runtime ultimately rejects engines that are not four-stroke petrol
engines, even though the reserved symbols two_stroke and diesel exist in
the parser in preparation for a future extension.
After importing a .els or .engine file, a worker watches every dependency
roughly every 250 ms. A save triggers a compilation off the UI thread:
- if it succeeds, a new immutable revision is published and the application builds a new runtime;
- if it fails, the revision and the pointer to the last valid configuration are kept, and the diagnostics are shown.
The watcher stays active after a successful reconfiguration. It also detects
changes to an include or to the JSON/YAML loaded by base.
This hot reload is structural. It avoids restarting the application, but it
replaces the simulated engine and resets engine speed, temperatures,
combustion, transmission and audio. The current CalibrationStore is shared
with the new runtime: live edits and the watched .ecu.json file stay active
without going through a reload. To change AFR, advance or the rev limiter
without resetting the dynamic states, use the ECU tuner
directly.
The language cannot yet declare new node types, user functions, loops,
conditions, part collections or a complete topology from scratch. It overrides
a preset or a canonical configuration. That is more bounded and easier to
validate, but much less expressive than the Piranha/.mr ecosystem of ES2D.