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.
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.
Systems and Libraries
A law reaches a Space in one of three ways, and the way decides what a file can do with it:
| Laws | Runs | On | When |
|---|---|---|---|
| Cell model | Every frame | Parts with [electrochemical] and a capacity above zero | Play |
| Heat conduction | Every frame | Pairs of parts with [material] and [thermodynamic] within 0.5 m | Edit and Play |
| Dents and fracture | On impact | Parts with Destructible on | Play, in Studio |
| Stress tensors, reactors, circuits, fluids, particles | Every frame | Components attached from Rust; no file section creates them | Edit and Play |
| Structures, cycles, propulsion, optics, acoustics, nuclear, plasma, control, numerics | When called | Rune scripts and Rust code | Any time |
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.
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:
| Preset | Part materials | E (GPa) | Yield (MPa) | K_IC (MPa·√m) | Density (kg/m³) |
|---|---|---|---|---|---|
| Steel | Steel, Metal, CorrodedMetal, DiamondPlate | 200 | 250 | 50 | 7,850 |
| Aluminum | Aluminum, Foil | 70 | 270 | 30 | 2,700 |
| Concrete | Concrete, Brick, Granite, Marble, Slate, Cobblestone | 30 | 30 | 1 | 2,400 |
| Glass | Glass | 70 | 45 | 0.7 | 2,500 |
| Ice | Ice | 9 | 1 | 0.1 | 917 |
| Wood (Oak) | Wood, WoodPlanks | 12 | 60 | 10 | 700 |
| Rubber | Rubber, Fabric, Grass | 0.01 | 15 | 5 | 1,100 |
| Plastic (ABS) | Plastic, SmoothPlastic, Neon, Sand, Pebble | 2.3 | 40 | 3 | 1,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.
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:
The dent spreads over a patch of radius √(2 R δ), and its depth is physical, in meters, with no visual exaggeration.
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:
| Preset | G_c (J/m²) | Threshold, 2 m × 1 m × 0.2 m slab |
|---|---|---|
| Ice | 1.1 | 0.22 J |
| Glass | 7 | 1.4 J |
| Concrete | 33 | 6.7 J |
| Plastic (ABS) | 3,900 | 780 J |
| Steel | 12,500 | 2,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:
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:
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.
[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 # KConduction 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).
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.
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:
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.
Charge counting, heat and temperature close the 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.
Life and Fade
Capacity retention is the product of five independent channels, so whichever bites first sets the life:
- 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_ktimes 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_countstack, 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):
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:
| Key | Unit | If absent |
|---|---|---|
capacity_ah | Ah | 0, and the part is not stepped |
soc | 0 to 1 | 1, a full cell |
internal_resistance | Ω at 25 °C | 0, no ohmic loss |
standard_potential_v, entropy_coefficient_v_per_k | V, V/K | Sodium-sulfur values |
electrode_area_m2 | m², all layers | 0.03 m² |
ionic_conductivity, separator_thickness_um | S/m, µm | No transport loss |
thermal_mass_j_per_k, thermal_resistance_k_per_w | J/K, K/W | 625.5 J/K and 2.0 K/W |
ambient_temperature_k | K | 298.15 K |
j_crit_a_per_m2 | A/m² | Monroe-Newman estimate |
stack_pressure_mpa | MPa | 2.0 MPa |
layer_count | layers | 0, series-only mechanisms off |
cell_mass_kg | kg | No specific energy |
[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:
| Group | Keys |
|---|---|
| Terminal | battery.voltage (V), battery.current (A), battery.power (W), battery.c_rate, battery.resistance_ohm |
| State | battery.soc, battery.temperature_c, battery.ambient_c, battery.heat_generation (W), battery.cycle_count |
| Safety | battery.dendrite_risk, battery.pressure_ceiling_active (1 while creep reaches the separator) |
| Life | battery.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 |
| Cost | battery.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:
| Module | Covers | For example |
|---|---|---|
thermodynamics | Gases, work, entropy, heat transfer, free energy | heat_radiation_rate |
thermocycles | Rankine, Brayton, Otto, refrigeration, heat exchangers | brayton_efficiency |
mechanics | Energy, gravity, orbits, friction, springs, inertia | orbital_velocity |
structures | Beams, columns, fatigue, fracture, composites | euler_critical_load |
electrical | Ohm's law, RC, RL and RLC, reactance, AC power | rlc_natural_frequency |
chemistry | Arrhenius, rate laws, equilibrium, pH, enzymes, combustion | arrhenius_rate |
propulsion | Rockets, jets, propellers, electric thrusters | tsiolkovsky_delta_v |
optics | Lenses, mirrors, interference, diffraction, photons | bragg_angle |
acoustics | Sound speed and level, Doppler, room acoustics | sabine_reverberation_time |
nuclear | Decay, shielding, criticality | critical_radius_sphere |
plasma | Debye length, MHD, fusion | lawson_triple_product |
control | Step response, damping, decibels | settling_time_2pct |
numerics | Interpolation, error function, distributions | gaussian_cdf |
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_physicsevaluates one of nine formulas by name:ideal_gas_pressure,kinetic_energy,gravitational_force,heat_transfer_conduction,nernst_potential,escape_velocity,spring_force,drag_forceandbuoyancy_force.query_materialreturns a part material's appearance values and the mechanical constants of the preset it maps to, or says that no preset matches.
{ "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
| Phase | What happens | Tools |
|---|---|---|
| 1. Baseline | Start the run at 10x, confirm Play and the first readings | run_simulation, get_simulation_state, tail_telemetry |
| 2. Detection | When 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 access | None, engine side |
| 3. Repair | Claude reads the temperature trend and the current state, then lowers the charge rate | tail_telemetry, get_simulation_state, feedback_diff, read_file, set_sim_value |
| 4. Verification | Confirm the temperature turns down; a 30-second cooldown prevents alert storms; stop the run, which exports the recording | tail_telemetry, get_simulation_state, stop_simulation, query_audit_log |
| 5. Git loop | Branch fix/thermal-runaway, add a thermal limiter to the script, commit, compare with main | git_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:
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:
| Metric | Expected |
|---|---|
Lines in telemetry.jsonl | At least 30 (30 or more seconds at 1 Hz) |
| Watchman alerts fired | 1 to 3 |
Claude API calls in query_audit_log | 3 to 8 (alert, diagnosis, verification) |
| Simulation commands processed | At least 2 (run and set_sim_value) |
| Recordings exported | 1 JSON file |
Completing a key that starts with bat in get_sim_value | Offers battery.voltage and the other keys |
| Branch after the git loop | fix/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:
| Value | Alerts when |
|---|---|
battery.temperature_c | Above 60 °C |
battery.voltage | Outside 1.8 to 2.5 V |
battery.soc | Outside 0 to 1 |
battery.dendrite_risk | Above 0.8 |
battery.capacity_retention | Below 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.
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.