Learn/Realism

Realism

Realism is Eustress's library of physical laws in SI units: materials and fracture, heat, electrochemistry, structures and more. A few of the laws run every frame on the parts that opt in through their files; the rest are functions you call from Rune or Rust.

Time20 min readLevelAdvancedUpdatedUpdated Sep 2026

01Overview

What Realism Is

Realism is the physical-law layer of Eustress. Each law is a plain function over SI quantities, such as the Nernst potential or the Euler buckling load, kept in one library that Studio, eustress-headless and scripts share. A few laws are also wired into systems that update parts while a run plays, which is how a battery drains, a concrete slab cracks and a hot block warms its neighbor.

The plugin that registers those systems is part of the core simulation tier, so it is always on, in Studio and in eustress-headless. Rigid-body motion belongs to Avian and is covered in Physics; Realism adds the quantities rigid bodies do not carry, and Simulation covers the clock and the recordings that capture them.

8
Named material presets
24
Cell readouts
published every Play frame
5
Fade channels
13
Rune law modules

Systems and Libraries

A law reaches a Space in one of three ways, and the way decides what a file can do with it:

LawsRunsOnWhen
Cell modelEvery frameParts with [electrochemical] and a capacity above zeroPlay
Heat conductionEvery framePairs of parts with [material] and [thermodynamic] within 0.5 mEdit and Play
Dents and fractureOn impactParts with Destructible onPlay, in Studio
Stress tensors, reactors, circuits, fluids, particlesEvery frameComponents attached from Rust; no file section creates themEdit and Play
Structures, cycles, propulsion, optics, acoustics, nuclear, plasma, control, numericsWhen calledRune scripts and Rust codeAny time
Info
Not in this build

The realism library also holds GPU compute for fluid particles, a symbolic solver and quantum statistics. They sit behind build features that Eustress Engine does not enable, so they are not part of the app.

02Opting In

Sections in a Part File

A part joins a realism system through a section in its file. The loader reads five sections on any class:

SectionHoldsUsed by
[material]Mechanical and thermal constantsHeat conduction; dents and fracture
[thermodynamic]Temperature, pressure, volume, energy, entropy, molesHeat conduction; the cell model's temperature
[electrochemical]A cell's design and starting stateThe cell model
[nuclear]Starting state of an ArcReactorCoreThe ARC-1 reactor model (not yet active)
[plasma]Densities, temperatures, ionization, fieldNo per-frame system yet

Destructible parts need no section at all. Set destructible in [properties] and the part takes its mechanical constants from its material name:

_instance.toml
[properties]
material = "Concrete"
destructible = true    # dents and cracks during Play

[metadata]
class_name = "Part"
Advanced
A [material] block replaces the preset

The loader reads a [material] block as written: a key it leaves out is 0, not the preset's value. The dent model then falls back to generic plastic constants for each missing mechanical value, and heat conduction ignores a part with no conductivity, density or specific heat. Name the material in [properties] instead, and write a [material] block only for a material the presets do not cover, with every constant filled in.

In Properties

Select a part and Properties shows Destructible under Physics, and the realism sections its file declares under Material, Thermodynamic and Electrochemical. For a destructible part with no [material] block, the Material category lists the constants its material name implies, without writing them to the file.

Those rows show the file's values. The live state of a running model, such as a cell's charge or temperature, is published as sim values: read it with the simulation tools or in the run's recording.

Units and Constants

Every law works in SI units, and so do the file keys, with a few named exceptions:

QuantityUnitExceptions
Lengthmµm for separator_thickness_um, plated_thickness_um
TemperatureK ([thermodynamic] defaults to 298.15)°C in battery.temperature_c and battery.ambient_c
Pressure, stress, moduliPa (pressure defaults to 101,325)MPa for stack_pressure_mpa, creep_threshold_mpa
Fracture toughnessPa·√mNone
Current, capacityA (positive discharges), AhNone
Activation energieseVNone
State of charge, retention, dendrite riskFractions from 0 to 1Their watchpoints are labeled %

Physical constants come from one module: R = 8.314462618 J/(mol·K), F = 96,485.33212 C/mol, k_B = 1.380649e-23 J/K, N_A = 6.02214076e23 /mol, G = 6.67430e-11 N·m²/kg² and the Stefan-Boltzmann constant 5.670374419e-8 W/(m²·K⁴).

03Materials & Fracture

Material Presets

A material's constants are 14 numbers: Young's modulus, Poisson's ratio, yield and ultimate strength, fracture toughness K_IC, hardness, thermal conductivity, specific heat, thermal expansion, melting point, density, static and kinetic friction, and restitution, plus any custom numeric keys. Eight presets supply them by name, and a part's appearance material maps onto one:

PresetPart materialsE (GPa)Yield (MPa)K_IC (MPa·√m)Density (kg/m³)
SteelSteel, Metal, CorrodedMetal, DiamondPlate200250507,850
AluminumAluminum, Foil70270302,700
ConcreteConcrete, Brick, Granite, Marble, Slate, Cobblestone303012,400
GlassGlass70450.72,500
IceIce910.1917
Wood (Oak)Wood, WoodPlanks126010700
RubberRubber, Fabric, Grass0.011551,100
Plastic (ABS)Plastic, SmoothPlastic, Neon, Sand, Pebble2.34031,050

Names match without regard to case, spaces, underscores or hyphens. A name with no preset, such as Gold, gives a destructible part generic plastic-like constants. The library derives the rest from these: shear modulus G = E / (2(1 + ν)), bulk modulus, the Lamé constants, thermal diffusivity k / (ρ c_p) and the speed of sound √(E / ρ). It calls a material brittle when K_IC is below 8 MPa·√m or its yield strength is at least 90 % of its ultimate strength.

Dents

During Play, a destructible part that is struck takes a dent computed from contact mechanics. The impact energy comes from the closing speed, treating the collision as fully inelastic. A static part counts as immovable, and the striking body is treated as a sphere whose radius is half its smallest dimension.

E = ½ m_eff v², 1 / m_eff = 1 / m₁ + 1 / m₂
Impact energy from the closing speed v
1 / E* = (1 - ν₁²) / E₁ + (1 - ν₂²) / E₂
Reduced contact modulus of the pair

Below Johnson's yield-onset energy the dent is elastic and springs back; above it the dent is plastic, using Tabor hardness H = 3σ_y, and stays:

δ = (E / ((8/15) E* √R))^(2/5)
Elastic depth (Hertz)
δ = √((E - E_y) / (π R H)), E_y = 10 R³ σ_y (σ_y / E*)⁴
Plastic depth above the yield-onset energy E_y

The dent spreads over a patch of radius √(2 R δ), and its depth is physical, in meters, with no visual exaggeration.

Advanced
Pausing undoes dents

Dents exist only while a run is playing. Pause or Stop restores every dented mesh, and resuming does not bring the dents back. Fracture fragments stay until Stop.

Cracks and Fragments

When the impact energy exceeds the part's Griffith threshold, the part cracks instead of denting. The threshold is the material's critical energy release rate times the part's smallest cross-section, the cheapest path for a crack:

G_c = K_IC² / E, E_crack = G_c × A_min
Griffith fracture threshold
PresetG_c (J/m²)Threshold, 2 m × 1 m × 0.2 m slab
Ice1.10.22 J
Glass71.4 J
Concrete336.7 J
Plastic (ABS)3,900780 J
Steel12,5002,500 J

So a 10 kg block that hits an anchored slab at 2 m/s brings 20 J: enough to crack glass or concrete, and only a dent in plastic or steel.

A crack splits the part into two dynamic bodies along a plane that contains the impact direction, so a part struck from above splits down through itself. The original part is hidden rather than deleted, and Stop brings it back and removes the fragments. Four guards stop a chain reaction: a fragment can be fractured again at most twice, a cut that would leave a piece under 0.05 m long is refused, a new fragment cannot crack for 0.3 s, and at most 4 fractures happen per frame.

The Stress Library

The library's stress and strain tensors are 3 by 3 and symmetric. A stress tensor caches its von Mises stress, principal stresses, hydrostatic stress and maximum shear. Functions convert between the two with generalized Hooke's law, including plane stress and plane strain, and test yield:

σ_vm = √(½ [(σxx - σyy)² + (σyy - σzz)² + (σzz - σxx)² + 6 (τxy² + τyz² + τzx²)])
Von Mises equivalent stress
σ = λ tr(ε) I + 2μ ε, λ = E ν / ((1 + ν)(1 - 2ν)), μ = E / (2(1 + ν))
Generalized Hooke's law

A material yields when σ_vm ≥ σ_y (von Mises) or τ_max ≥ σ_y / 2 (Tresca), and its safety factor is σ_y / σ_vm. These are Rust functions today: a per-frame system updates stress from strain on entities that carry both tensors, and no file section attaches them yet.

04Heat

Conduction Between Parts

Parts that carry both [material] and [thermodynamic] exchange heat by conduction. Every 0.5 s the engine pairs any two such parts whose centers are within 0.5 m, and every frame it moves heat across each pair by Fourier's law, using the harmonic mean of the two conductivities:

Q̇ = k_eff A ΔT / L, k_eff = 2 k_a k_b / (k_a + k_b)
Fourier conduction across a contact
ΔT_part = Q̇ Δt / (ρ V c_p)
Temperature change of each part in a frame

The contact area A is the smaller X extent of the pair times the smaller Z extent, L is the distance between the centers (at least 1 mm), and V is each part's volume from its size. Each contact changes a part's temperature by at most 50 K per frame, and pairs less than 0.01 K apart are skipped. A contact, once made, is kept even if the parts move apart.

_instance.toml
[material]
name = "Aluminum"
density = 2700.0              # kg/m3
specific_heat = 900.0         # J/(kg K)
thermal_conductivity = 237.0  # W/(m K)

[thermodynamic]
temperature = 350.0           # K
Advanced
Conduction runs while you edit

Conduction steps on real frame time whenever the Space is open, in Edit as well as Play, and does not follow the time scale. Temperatures therefore drift while you edit, a run starts from wherever they are when you press Play, and Stop returns them to that point.

Heat Laws and Cycles

The thermodynamics library covers the rest of heat as functions: ideal and van der Waals gases, work in isobaric, isothermal and adiabatic processes, heat capacities, entropy changes, Carnot efficiency and heat-pump COP, conduction, convection and radiation rates, phase change, enthalpy, and the Gibbs and Helmholtz free energies. The thermocycles library adds steady-state Rankine, Brayton, Otto and Diesel, refrigeration and heat-exchanger analysis (LMTD and effectiveness-NTU).

Q̇ = h A (T_surface - T_fluid)
Convection (Newton's law of cooling)
η = 1 - T_cold / T_hot
Carnot efficiency

05The Cell Model

One Step

The cell model turns a part's [electrochemical] section into a live battery. Every Play frame it steps each such part whose capacity is above zero, cutting the frame's simulated time into clock timesteps (1/60 s by default; see limits of compression). The cell with the largest capacity is the one driven by sim values and published. With a [thermodynamic] section the cell's temperature evolves; without one it stays at 298.15 K.

One cell-model stepOpen-circuit voltageNernst, from SOC and TLossesIR, activation, transportTerminal voltageOCV and lossesState of chargecoulomb countingHeat and temperatureohmic, reaction, entropicLifedendrite risk, five fadesRepeated in clock timesteps until the frame's simulated time is used.
The cell model's step, in the order the engine computes it. The temperature from one step feeds the voltage and the fade rates of the next.

Open-circuit voltage follows the Nernst equation, with the reaction quotient taken from the state of charge and n = 2 electrons. The standard potential is the cell's own standard_potential_v, or the sodium-sulfur 2.23 V when unset:

E = E° - (RT / nF) ln Q, Q = (1 - SOC) / SOC
Open-circuit voltage (Nernst)

Three losses come off it. Resistance rises in the cold by an Arrhenius law (0.35 eV unless set). Activation loss is the exact inverse of symmetric Butler-Volmer, with an exchange current density of 50 A/m². Transport loss rises steeply as the current density j approaches the limiting current σ(RT/F) / (1.5 t), set by the separator's ionic conductivity σ and thickness t, and is zero when no separator is set.

R(T) = R₂₅ exp[(E_a / k_B)(1/T - 1/298.15 K)]
Temperature-dependent resistance
η_ct = (2RT / F) asinh(j / 2j₀), η_conc = -(RT / 2F) ln(1 - j / j_lim)
Activation and transport losses
V = E - (I R + η_ct + η_conc) discharging, V = E + (I R + η_ct + η_conc) charging
Terminal voltage, with I positive on discharge

Charge counting, heat and temperature close the step:

SOC ← SOC - I Δt / (3600 Q_nom r)
State of charge (Q_nom in Ah, r the capacity retention)
Q̇ = I² R + |I| |η_ct| - T I (dE/dT)
Heat: ohmic, reaction and reversible entropic terms
T ← T_ss + (T - T_ss) e^(-Δt/τ), T_ss = T_amb + Q̇ R_th, τ = R_th C_th
Lumped thermal model, solved exactly for each step

Dendrite risk compares the plating current density on charge with a critical current density: the cell's j_crit_a_per_m2, or a Monroe-Newman estimate from sodium constants (G = 30 GPa, δ = 5 nm, V_m = 23.7 cm³/mol, about 131 A/m²) when unset. Discharge scores zero, and the risk is capped at 1.

risk = j_plating / j_crit, j_crit = 2 G δ / (F V_m)
Dendrite risk and the Monroe-Newman critical current

Life and Fade

Capacity retention is the product of five independent channels, so whichever bites first sets the life:

retention = (1 - f_Li)(1 - f_crack)(1 - f_cal)(1 - f_creep)(1 - f_short)
Capacity retention, kept between 0.01 and 1
  • Lithium inventory: each unit of charge loses a little metal to interphase, more for deeper cycles (depth to the power 0.8) and at higher temperature. A reservoir (li_reservoir_frac) buffers this channel only.
  • Cathode cracking: fatigue damage per unit charge of crack_k times depth to the power 1.5, optionally scaled by stack pressure.
  • Calendar ageing: grows with the square root of equivalent hours, which accrue on every step, faster at high charge and temperature; the default coefficient is 2.14e-4.
  • Creep: above creep_threshold_mpa (1 MPa by default) the plated metal creeps at a rate with exponent 6.6 in the overpressure, and only strain beyond the accommodation void (creep_accommodation_frac) counts.
  • Bridged layers: creep that crosses the separator shorts whole layers of a layer_count stack, counted as layers lost rather than fade.

With sei_thickness_nm set, the coulombic loss behind the lithium channel is derived from deposit roughness, areal capacity q and stack pressure P instead of a fitted efficiency (0.995 by default):

1 - CE = R δ ρ_Li F / (M_Li q), R = 1 + k (j / j_crit)^1.5 (2 / P)^1.5
Derived coulombic loss (k = 105.7 by default)

battery.cycle_count counts equivalent full cycles: the charge moved divided by twice the effective capacity.

Keys and Readouts

The main [electrochemical] keys, and what the model does when a key is absent:

KeyUnitIf absent
capacity_ahAh0, and the part is not stepped
soc0 to 11, a full cell
internal_resistanceΩ at 25 °C0, no ohmic loss
standard_potential_v, entropy_coefficient_v_per_kV, V/KSodium-sulfur values
electrode_area_m2m², all layers0.03 m²
ionic_conductivity, separator_thickness_umS/m, µmNo transport loss
thermal_mass_j_per_k, thermal_resistance_k_per_wJ/K, K/W625.5 J/K and 2.0 K/W
ambient_temperature_kK298.15 K
j_crit_a_per_m2A/m²Monroe-Newman estimate
stack_pressure_mpaMPa2.0 MPa
layer_countlayers0, series-only mechanisms off
cell_mass_kgkgNo specific energy
_instance.toml
[electrochemical]
capacity_ah = 40.0
soc = 1.0
internal_resistance = 0.002      # ohm at 25 C
electrode_area_m2 = 0.74         # all layers
thermal_mass_j_per_k = 3000.0
thermal_resistance_k_per_w = 0.6
stack_pressure_mpa = 2.0
cell_mass_kg = 3.5

[thermodynamic]
temperature = 298.15             # K

[metadata]
class_name = "Part"

Every Play frame the model publishes 24 readouts as sim values:

GroupKeys
Terminalbattery.voltage (V), battery.current (A), battery.power (W), battery.c_rate, battery.resistance_ohm
Statebattery.soc, battery.temperature_c, battery.ambient_c, battery.heat_generation (W), battery.cycle_count
Safetybattery.dendrite_risk, battery.pressure_ceiling_active (1 while creep reaches the separator)
Lifebattery.capacity_retention, battery.fade_lithium, battery.li_inventory_lost, battery.fade_cathode, battery.fade_calendar, battery.calendar_hours, battery.fade_creep, battery.creep_strain_total, battery.fade_short, battery.shorted_layers
Costbattery.reservoir_mass_g (g), battery.specific_energy_wh_kg (Wh/kg)

Because they are sim values, every run records all 24, and nine of them carry watchpoints; see sim values for the keys the model reads back, battery.mode and battery.target_current.

06Scripts & Tools

Rune Law Modules

Rune scripts reach the law library through 13 modules under eustress::realism. They take and return f64 values and wrap the same Rust functions the engine uses:

ModuleCoversFor example
thermodynamicsGases, work, entropy, heat transfer, free energyheat_radiation_rate
thermocyclesRankine, Brayton, Otto, refrigeration, heat exchangersbrayton_efficiency
mechanicsEnergy, gravity, orbits, friction, springs, inertiaorbital_velocity
structuresBeams, columns, fatigue, fracture, compositeseuler_critical_load
electricalOhm's law, RC, RL and RLC, reactance, AC powerrlc_natural_frequency
chemistryArrhenius, rate laws, equilibrium, pH, enzymes, combustionarrhenius_rate
propulsionRockets, jets, propellers, electric thrusterstsiolkovsky_delta_v
opticsLenses, mirrors, interference, diffraction, photonsbragg_angle
acousticsSound speed and level, Doppler, room acousticssabine_reverberation_time
nuclearDecay, shielding, criticalitycritical_radius_sphere
plasmaDebye length, MHD, fusionlawson_triple_product
controlStep response, damping, decibelssettling_time_2pct
numericsInterpolation, error function, distributionsgaussian_cdf
Rune
use eustress::log_info;
use eustress::realism::structures;

pub fn on_init() {
    // A 2 m steel column, 50 mm square, pinned at both ends (K = 1).
    let i = structures::moment_of_area_rectangle(0.05, 0.05);         // m^4
    let p_cr = structures::euler_critical_load(200.0e9, i, 2.0, 1.0); // about 257 kN
    log_info(`Euler buckling load: ${p_cr} N`);

    // A 1 mm edge crack (Y = 1.12) in glass under 20 MPa of tension.
    let k = structures::stress_intensity_factor(20.0e6, 0.001, 1.12);
    if structures::fracture_occurs(k, 0.7e6) {
        log_info("K exceeds glass's K_IC of 0.7 MPa m^0.5: the crack runs.");
    }
}

Euler buckling is P_cr = π² E I / (K L)², and the stress intensity is K = Y σ √(π a). See Scripting for how scripts are attached and run.

Agent Tools

Two tools of the MCP server expose the library to agents:

  • calculate_physics evaluates one of nine formulas by name: ideal_gas_pressure, kinetic_energy, gravitational_force, heat_transfer_conduction, nernst_potential, escape_velocity, spring_force, drag_force and buoyancy_force.
  • query_material returns a part material's appearance values and the mechanical constants of the preset it maps to, or says that no preset matches.
MCP
{ "tool": "calculate_physics", "arguments": {
    "equation": "nernst_potential",
    "params": {
        "standard_potential": 2.23,
        "temperature_k": 298.15,
        "electron_count": 2,
        "reaction_quotient": 1.0
    }
} }

Running, stepping and comparing models uses the simulation tools listed in Simulation.

07Case Study: V-Cell

The Walkthrough

The V-Cell is Voltec's battery cell, and its case study is the end-to-end test of the cell model and the tools around it. The walkthrough, docs/architecture/VCELL_CASE_STUDY.md in the repository, sets out to show that Eustress can detect a simulation anomaly, diagnose it with the Workshop agent, correct it through MCP tools and verify the fix, with no manual step after the run starts. The cell is the subject because it produces continuous telemetry with well-defined safety thresholds: voltage, temperature and dendrite risk.

Its prerequisites:

  • A Universe with a Space containing a V-Cell prototype entity.
  • The cell model producing battery.* sim values.
  • A Rune SoulScript attached to the V-Cell entity.
  • The Workshop panel open with a valid BYOK API key.
  • The Watchman enabled, which it is by default.

Detect, Repair, Verify

PhaseWhat happensTools
1. BaselineStart the run at 10x, confirm Play and the first readingsrun_simulation, get_simulation_state, tail_telemetry
2. DetectionWhen battery.temperature_c passes 60 °C, the Watchman catches it on its next 5-second poll and posts a [Watchman Alert] message to the Workshop, which Claude receives with full tool accessNone, engine side
3. RepairClaude reads the temperature trend and the current state, then lowers the charge ratetail_telemetry, get_simulation_state, feedback_diff, read_file, set_sim_value
4. VerificationConfirm the temperature turns down; a 30-second cooldown prevents alert storms; stop the run, which exports the recordingtail_telemetry, get_simulation_state, stop_simulation, query_audit_log
5. Git loopBranch fix/thermal-runaway, add a thermal limiter to the script, commit, compare with maingit_branch, write_file, git_status, git_commit, feedback_diff

The limiter the walkthrough adds halves the current whenever the cell passes 55 °C. As a complete script:

Rune
use eustress::{get_sim_value, set_sim_value};

pub fn on_update(dt) {
    let temp = get_sim_value("battery.temperature_c");
    if temp > 55.0 {
        let reduced = get_sim_value("battery.current") * 0.5;
        set_sim_value("battery.current", reduced);
    }
}

A complete cycle is judged against the walkthrough's checklist:

MetricExpected
Lines in telemetry.jsonlAt least 30 (30 or more seconds at 1 Hz)
Watchman alerts fired1 to 3
Claude API calls in query_audit_log3 to 8 (alert, diagnosis, verification)
Simulation commands processedAt least 2 (run and set_sim_value)
Recordings exported1 JSON file
Completing a key that starts with bat in get_sim_valueOffers battery.voltage and the other keys
Branch after the git loopfix/thermal-runaway

What Models It

Everything the walkthrough exercises is the machinery on this page and on Simulation: the V-Cell's [electrochemical] section drives the cell model, its readouts are the battery.* sim values, telemetry is written once a second, the runtime snapshot four times a second, and Stop exports the recording. The Watchman runs in Studio during Play and ships with these thresholds:

ValueAlerts when
battery.temperature_cAbove 60 °C
battery.voltageOutside 1.8 to 2.5 V
battery.socOutside 0 to 1
battery.dendrite_riskAbove 0.8
battery.capacity_retentionBelow 0.7

It polls every 5 seconds, waits 30 seconds before repeating an alert for the same value, and stops after 10 alerts in a run. In the Workshop, a set_sim_value call runs without an approval prompt.

Advanced
Lower the charge rate with the target current

Phase 3 of the walkthrough writes battery.current = -1.5 with set_sim_value. A current written by a tool is replaced before the cell model reads it, so from a tool set battery.mode to 1 and battery.target_current to 1.5, which charges at 1.5 A. Inside a script, as in the limiter above, battery.current takes effect directly.

08What's Next

Laws Without a File Section

Several laws already run as systems but have no way in from a Space file: continuous and batch chemical reactors, circuit elements with motors and power converters, particle fluids with buoyancy and aerodynamic drag, and stress tensors on parts. They will gain file sections or classes, so a part can carry a circuit or a reactor the way it carries a cell today.

The ARC-1 reactor model already exists as systems: one-group point kinetics, thermal-hydraulics, power conversion, a battery buffer, a three-loop PID controller and a scram monitor, publishing arc1.* watchpoints. A [nuclear] section already records an ArcReactorCore's starting state. The plugin that attaches the reactor's components to such instances is not yet added to the app; once it is, the reactor will run in any Space.

Simulated Time for Every Law

Only the cell model integrates against the simulation clock today. Heat conduction, the reactor and the reaction and circuit systems step on frame time, several of them limited to 0.05 s per frame, so a time scale does not speed them up. Moving them onto the clock will make one time scale apply to every model in a run. The GPU fluid path and the symbolic solver will follow.

Name the material, write the section, and the laws do the rest.

Loading Eustress Engine...