Examples
Runnable simulations in one file, organized by experience level.
Set PYTHONPATH=python before running any example.
Beginner Beginner
Each example uses one or two concepts and fits in ~20 lines. Start here to understand the Lesson → input → show → run pattern.
Population Growth
Two sliders control birth and death rate. The tank fills or drains as the difference changes; the curve shows the analytical exponential solution alongside the live integration.
import math, sys; sys.path.insert(0, "python")
from ember.edu import Lesson, Accumulation, Mapping
lesson = Lesson("Biology: Population Growth", theme="studio")
lesson.input("birth_rate", min=0.0, max=0.5, default=0.1, unit="/yr")
lesson.input("death_rate", min=0.0, max=0.5, default=0.05, unit="/yr")
lesson.state.params["population"] = 10.0
lesson.rate("population",
lambda s: s["population"] * (s["birth_rate"] - s["death_rate"]))
lesson.show(Accumulation("population", max_val=1000, label="Population"))
lesson.show(Mapping("time", "N",
fn=lambda t, s: 10.0 * math.exp(t * (s["birth_rate"] - s["death_rate"])),
x_range=(0, 20), y_range=(0, 1000)))
lesson.explain(
lambda: f"Net growth: {(lesson.state['birth_rate'] - lesson.state['death_rate'])*100:.1f} %/yr")
lesson.run()
▶ PYTHONPATH=python python3 examples/population_sim.py
Supply & Demand
The balance beam tips left or right as supply and demand diverge.
A built-in challenge fires when the learner finds the equilibrium price — demonstrating
the challenge() gamification hook.
import sys; sys.path.insert(0, "python")
from ember.edu import Lesson, Balance
lesson = Lesson("Economics: Supply & Demand", theme="studio")
lesson.input("price", min=0, max=100, default=30, unit="$")
lesson.rule("supply", lambda s: s["price"] * 0.8)
lesson.rule("demand", lambda s: 100 - s["price"] * 0.6)
lesson.rule("gap", lambda s: abs(s["supply"] - s["demand"]))
lesson.show(Balance("supply", "demand", ("Supply", "Demand")))
lesson.challenge({"gap": 0.0}, tolerance=1.0, message="Equilibrium reached!")
lesson.explain(
"Raise the price until Supply equals Demand — that is the market equilibrium.")
lesson.run()
▶ PYTHONPATH=python python3 examples/lesson_flow.py
Water Tank
Classic stock-and-flow: inflow fills a tank while outflow drains it.
The Flow concept animates the pipe; Accumulation shows the
water level. Adjust either slider to change the net rate.
import sys; sys.path.insert(0, "python")
from ember.edu import Lesson, Flow, Accumulation
lesson = Lesson("Physics: Water Tank", theme="midnight")
lesson.input("inflow", min=0.0, max=5.0, default=2.0, unit="L/s")
lesson.input("outflow", min=0.0, max=4.0, default=0.5, unit="L/s")
lesson.state.params["tank"] = 0.0
lesson.rate("tank", lambda s: s["inflow"] - s["outflow"])
lesson.show(Flow("inflow", label="Inflow"))
lesson.show(Accumulation("tank", max_val=100, label="Tank Level"))
lesson.explain(lambda: (
f"Tank {'filling' if lesson.state['inflow'] > lesson.state['outflow'] else 'draining'}"
f" — net {abs(lesson.state['inflow'] - lesson.state['outflow']):.1f} L/s"))
lesson.run()
▶ PYTHONPATH=python python3 examples/population_sim.py
Advanced Advanced
Multi-concept simulations with ODE systems, custom Sketch drawing, domain helpers, and phase-space visualizations. Each fits in ~35–50 lines.
SIR Epidemic Model
A three-compartment SIR model with interactive transmission and recovery rates. The phase portrait traces the S–I trajectory as the simulation runs; R₀ is derived live. The epidemic dies when R₀ < 1.
import sys; sys.path.insert(0, "python")
from ember.edu import Lesson, PhasePlane, Table
lesson = Lesson("Epidemiology: SIR Model", theme="midnight")
lesson.input("beta", min=0.01, max=1.0, default=0.3, unit="contact")
lesson.input("gamma", min=0.01, max=1.0, default=0.1, unit="recovery")
lesson.state.params["S"] = 990.0 # Susceptible
lesson.state.params["I"] = 10.0 # Infected
lesson.state.params["R"] = 0.0 # Recovered
lesson.rate("S", lambda s: -s["beta"] * s["S"] * s["I"] / 1000)
lesson.rate("I", lambda s: s["beta"] * s["S"] * s["I"] / 1000 - s["gamma"] * s["I"])
lesson.rate("R", lambda s: s["gamma"] * s["I"])
lesson.rule("R0", lambda s: s["beta"] / s["gamma"])
lesson.show(PhasePlane(
dx_fn=lambda x, y, s: -s["beta"] * x * y / 1000,
dy_fn=lambda x, y, s: s["beta"] * x * y / 1000 - s["gamma"] * y,
x_range=(0, 1000), y_range=(0, 500),
state_keys=("S", "I"), label="S–I Phase Portrait"))
lesson.show(Table(["S", "I", "R", "R0"], label="Compartment Counts"))
lesson.explain(lambda: f"R₀ = {lesson.state['R0']:.2f} — epidemic grows when R₀ > 1")
lesson.run()
▶ PYTHONPATH=python python3 examples/disease_sim.py
Fajan's Rule: Ionic Polarization
Custom Sketch drawing shows how a small, high-charge
cation distorts the electron cloud of a large anion. Every shape is drawn with
ShapeCanvas primitives — no coordinates are hard-coded outside normalized
(−1 … +1) space.
from math import cos, pi, sin
import sys; sys.path.insert(0, "python")
from ember.edu import Lesson, Sketch
BLUE, GOLD, RED, WHITE = (80, 170, 255), (255, 205, 70), (255, 90, 110), (235, 250, 255)
def cloud(cx, cy, pull):
return [(cx - 0.16*pull
+ (0.31 + 0.24*pull*max(0,-cos(a)) - 0.06*pull*max(0,cos(a)))*cos(a),
cy + (0.23 - 0.05*pull*max(0,-cos(a)))*sin(a))
for a in (2*pi*i/64 for i in range(64))]
def draw(c, s):
pull = 0.18 + 0.72*(0.5 + 0.5*sin(s["_time"]*1.55))
cov = int(20 + 75*pull)
c.blob(cloud(0.38, -0.04, 0.0), BLUE, alpha=36, outline=BLUE)
c.blob(cloud(0.38, -0.04, pull), BLUE, "I⁻", alpha=150)
for y in (-0.14, -0.04, 0.06):
c.arrow((0.08, y), (-0.42, y*0.45), GOLD, width=3, alpha=170)
c.oval(-0.62, -0.04, 0.11, 0.11, GOLD, "Al³⁺", alpha=230, pulse=0.04*pull)
c.line([(-0.46, 0.62), (0.46, 0.62)], WHITE, width=2, alpha=80, smooth=False)
c.line([(-0.46, 0.62), (-0.46 + 0.0092*cov, 0.62)], RED, width=9)
c.text("← ionic | covalent →", 0, 0.72, WHITE, bold=True)
lesson = Lesson("Chemistry: Fajan's Rule", theme="neon")
lesson.rate("_time", lambda s: 1.0)
lesson.show(Sketch(draw, "Electron cloud distortion by a small, high-charge cation"))
lesson.explain("Stronger anion polarization gives more covalent bond character.")
lesson.run()
▶ PYTHONPATH=python python3 examples/fajan_sim_test.py
Herd Immunity Threshold
Uses ember.domains.bio.BioLesson — a thin domain
wrapper that sets sensible biology defaults. Two sliders (R₀ and vaccine efficacy) drive
the derived threshold. The balance tips as required coverage overshoots or undershoots.
import sys; sys.path.insert(0, "python")
from ember.domains.bio import BioLesson
from ember.edu import Balance, Table
lesson = BioLesson("Herd Immunity Threshold")
lesson.input("R0", min=1.0, max=18.0, default=2.5, unit="basic-R₀")
lesson.input("v_eff", min=0.5, max=1.0, default=0.95, unit="efficacy")
lesson.rule("threshold", lambda s: 1 - 1 / s.params["R0"])
lesson.rule("required", lambda s: s.derived["threshold"](s) / s.params["v_eff"])
lesson.show(Balance("required", "threshold", ("Coverage needed", "Threshold")))
lesson.show(Table(["R0", "v_eff", "threshold", "required"], label="Herd Immunity"))
lesson.explain(
"When vaccinated fraction > 1 − 1/R₀, each infection reaches "
"fewer than one susceptible on average — outbreaks collapse.")
lesson.run()
▶ PYTHONPATH=python python3 examples/herd_immunity.py
For AI / LLM AI
Design rules and a copy-paste template for language models generating ember simulations. Follow the three-part pattern below and every generated simulation will be correct on the first try.
The Three-Part Design Pattern
- Visible DRIVER — one
lesson.input()the learner controls. - Invisible MEDIATOR — the mechanism: a
rule()orrate()that computes the concept. - Visible RESPONSE — one visual concept (
lesson.show()) that reacts.
One mechanism per simulation. Clarity beats completeness. Keep files to 40–65 lines.
Concept Selection Guide
Accumulation— any quantity that fills or drains (population, charge, water).Flow— something that moves through a channel (current, traffic, data).Balance— two quantities competing (supply/demand, acid/base, forces).Mapping— a function plot where a slider changes the curve shape.Pipeline— ordered stages with an active highlight (CPU pipeline, metabolism).Network— nodes connected by edges with spreading or weighted values.PhasePlane— 2D ODE system visualized as a trajectory and vector field.Sketch— custom drawing viaShapeCanvaswhen no built-in concept fits.Diagram/SemanticScene— node-link or role-based layouts with zero coordinate math.
Minimal Simulation Template
Replace DRIVER, CONCEPT, and the
show() call. Add rate() if the quantity integrates over time;
use rule() if it is computed instantly from other state values.
import sys; sys.path.insert(0, "python")
from ember.edu import Lesson, Accumulation # swap Accumulation for any Concept
# ── 1. Lesson shell ───────────────────────────────────────────────────────────
lesson = Lesson("Subject: Mechanism Name", theme="midnight") # or "studio"/"neon"
# ── 2. DRIVER: one interactive slider ────────────────────────────────────────
lesson.input("driver_name", min=0.0, max=10.0, default=1.0, unit="units")
# ── 3. MEDIATOR: the concept (rule = instant; rate = integrated over time) ───
lesson.rule("response", lambda s: s["driver_name"] * 2.5) # or lesson.rate(...)
# ── 4. RESPONSE: one visual concept ──────────────────────────────────────────
lesson.show(Accumulation("response", max_val=25.0, label="Output"))
# ── 5. Narrative hint ─────────────────────────────────────────────────────────
lesson.explain(lambda: f"driver = {lesson.state['driver_name']:.1f}")
lesson.run() # interactive window
# lesson.record(seconds=5, filename="out.gif") # headless GIF export
TCP Three-Way Handshake (Diagram)
Shows how to build an animated process diagram without writing
a single pixel coordinate. SemanticScene places objects by role; the clock
drives animation automatically.
import sys; sys.path.insert(0, "python")
from ember.edu import Lesson, SemanticScene
lesson = Lesson("Networking: TCP Handshake", theme="midnight")
lesson.clock("t", speed=0.4) # built-in time driver
# Objects declared by role — layout is computed automatically
objects = [
{"role": "source", "label": "Client", "glyph": "rect", "color": (80, 160, 255)},
{"role": "target", "label": "Server", "glyph": "rect", "color": (80, 220, 140)},
]
def handshake_relations(state):
t = state["t"] % 3.0
if t < 1.0:
return [{"from": "Client", "to": "Server", "label": "SYN", "color": (255, 200, 80)}]
elif t < 2.0:
return [{"from": "Server", "to": "Client", "label": "SYN-ACK", "color": (255, 140, 80)}]
else:
return [{"from": "Client", "to": "Server", "label": "ACK", "color": (80, 220, 140)}]
lesson.show(SemanticScene(objects, handshake_relations, label="TCP Handshake"))
lesson.explain("SYN → SYN-ACK → ACK: three messages to open a reliable connection.")
lesson.run()