Examples

Runnable simulations in one file, organized by experience level. Set PYTHONPATH=python before running any example.

Beginner Advanced For AI / LLM

Beginner Beginner

Each example uses one or two concepts and fits in ~20 lines. Start here to understand the Lessoninputshowrun pattern.

Beginner Accumulation · rate · Mapping

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

Beginner Balance · rule · challenge

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

Beginner Flow · Accumulation · rate

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.

Advanced PhasePlane · Table · multi-rate ODE

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

Advanced Sketch · ShapeCanvas · blob · oval · arrow · line

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

Advanced BioLesson · Balance · Table · domain helper

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

  1. Visible DRIVER — one lesson.input() the learner controls.
  2. Invisible MEDIATOR — the mechanism: a rule() or rate() that computes the concept.
  3. 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 via ShapeCanvas when no built-in concept fits.
  • Diagram / SemanticScene — node-link or role-based layouts with zero coordinate math.
AI Template copy-paste starting point

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
AI Example Diagram · SemanticScene · Sketch — zero coordinate math

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()