Sine Wave¶
Run the Sine Wave MicroSim Fullscreen
In this demo, we use three range control sliders to change the ways a sine wave is drawn on a canvas, following y = A·sin(2πf·x + φ). The three parameters are:
- amplitude (A): the height of the wave's peak, from 0 to 1, read directly off the tick marks on the y-axis
- frequency (f): how many complete cycles fit across the drawing width
- phase (φ): the angle, in radians from −π to π, where the wave starts its cycle
The period is not a slider. It is shown at the top of the drawing as T = 1/f, because it is not a separate setting: it is always the reciprocal of the frequency. Doubling the frequency halves the period, and the readout shows that as you drag.
Phase is an angle, not a distance, and the drawing shows it in both radians and degrees. At φ = π/2 (90°) the sine wave starts at its peak and becomes a cosine wave. At φ = ±π (180°) it is flipped upside down. An angle means the same thing at any frequency. A horizontal shift in pixels, which is what this slider used to be, does not: the same shift moves a high-frequency wave through more of its cycle than a low-frequency one.
Simulating an xAPI Event Stream¶
Two panels below the sliders turn this MicroSim into a small demo of how this project's Learning Record Store turns raw interactions into evidence of understanding:
- The Raw xAPI Event Stream panel shows one xAPI statement (Actor–Verb–Object, with a
result.extensionspayload carrying the new slider value) every time a student drags the amplitude, frequency, or phase slider. This is intentionally noisy — a single drag can generate dozens of statements — to illustrate why an LRS never stores one graph vertex per statement. Its controls (Full/Compact, Simulate Done) are described below. - The Show MicroSim Summary checkbox compresses that same stream into the kind of
evidence the LRS spec's
ConceptMasteryandMicroSimEngagementsummary vertices hold: whether the student tried all three controls, how much of each slider's range they explored, how many times they reversed direction (a signal of active comparison rather than a single accidental nudge), and a heuristic, clearly-labeled estimated probability that the student understands amplitude, frequency, and phase — not a substitute for a real formative assessment.
Three Concepts: Amplitude, Frequency, and Phase¶
Each slider is evidence for exactly one concept, carried as the statement's concept_id,
and each slider is named for that concept. The control a student drags and the
concept the stream says it was evidence for use the same word:
| Slider | Control IRI fragment | concept_id |
|---|---|---|
| Amplitude | #amplitude-slider |
amplitude |
| Frequency | #frequency-slider |
frequency |
| Phase | #phase-slider |
phase |
An earlier version of this sim had a Period slider whose statements were tagged
frequency. That worked, but it put two words on one piece of evidence: the student
dragged "Period" while the LRS recorded "frequency". Physics and signals courses, and most
wave simulations, have students control frequency and read the period off it. That is
also the more intuitive direction: slide right and you get more waves.
The MicroSim Summary panel estimates, per concept, the probability that the student understands it. The estimate combines three pieces of evidence: how much of the slider's range was explored, how many times the student reversed direction (active comparison rather than one accidental nudge), and how many movements were made. It is a heuristic from exploration alone: nothing in this sim asks a question, so it can show that a student explored a concept, not that they can answer questions about it.
Full vs. Compact xAPI Streams¶
The xAPI events: Full / Compact radio buttons at the top of the Raw xAPI Event Stream panel switch between the two streams this book describes, on the same slider moves:
- Full (the full LRS, high-bandwidth network, robust back end). One
interactedstatement per detected movement, as described above. Every statement appears as you drag. - Compact (LRS-Lite, a small
browser store, no server). Nothing while you drag. Movements are folded into one
session, and when the sim loses focus it emits one
experiencedsummary. Itscontrolsextension holds, per slider, theconcept, the movement countn,min,max,last, andreversals.statements_representedsays how many Full statements the summary replaced.
Simulate Done does what the host page does when the student moves on — switches tabs,
leaves the page, scrolls the sim mostly out of view for offscreenMs, or stops for
idleMs. In Compact mode that ends the session and emits the summary
(end_reason: "simulated-done"). In Full mode there is nothing to flush: this sim has no
Start/Pause, so there is no open run interval, and every movement has already been sent.
Why the compact summary carries reversals
Coverage (min, max) and the movement count survive compression unchanged. Direction
changes don't: a Full stream is an ordered sequence of values, so a reader can count
reversals by walking it, but a summary has no order left to walk. So the compact
summary carries reversals explicitly. Without it, the Compact stream could not
support the same understanding estimate the Full stream does.
Try it: leave the radio on Full, drag each slider back and forth, and count the
statements. Switch to Compact and repeat the same moves: the log stays silent. Press
Simulate Done: one statement appears, and its statements_represented is roughly the
count you saw in Full mode. Then press View Formatted JSON ↗ in the Raw xAPI Event Stream panel
to read that statement properly. Switching from Compact back to Full also ends the
session first (end_reason: "mode-switch"), so folded movements are never lost.
Reading a Statement: View Formatted JSON¶
In the log, each statement is one long line of JSON, which is accurate but hard to read. View Formatted JSON ↗ opens the most recent statement in a new tab, and clicking any line in the log opens that statement instead. The tab shows:
- the statement pretty-printed with line numbers and colors, exactly as it was emitted;
- the fields compact mode adds highlighted (
statements_represented,end_reason,controls, …), so you can see what a summary adds to an ordinary xAPI statement; - a How to read it table explaining each field that is present;
- Copy JSON and Download .json buttons.
It opens in a new tab, not inside the sim, because the sim is embedded in a fixed-height frame. A 70-line statement would either be cut off inside it or force the frame much taller for every reader. The tab is built in your browser; nothing is sent to a server.
Where the mode comes from¶
Two config files decide the mode, and an agent can change either one:
- The textbook's
docs/js/lrs-config.jssets the default for every MicroSim in the book. Here it isxapi: { compact: true, teaching: false }: compact, and silent. - This sim's
metadata.jsonxapiblock overrides it for this one sim:
"xapi": { "compact": false, "teaching": true, "idleMs": 90000, "offscreenMs": 10000, "blurMs": 30000 }
teaching: true is what shows the statement log, the Full/Compact switch, Simulate Done,
and View Formatted JSON. They exist only to teach what xAPI events are. A production
MicroSim leaves teaching off: it emits the same statements, silently, in whichever mode
its config says. This sim starts on Full, like the Bouncing Ball sim, so a reader sees the per-movement stream first and then watches Compact shrink it.
The MicroSim Summary panel works the same in both modes, because it is computed from
the movements themselves. Compact mode is Option B in the trade-off below, with one
difference that matters: the summary is itself a statement in the log. The log still
records that a session happened and how much it compressed, just not each individual
movement. make test-sims checks both modes, the live switch, and Simulate Done in
headless Chromium.
Architecture Trade-off: Where Should the Scoring Happen?¶
To keep the demo self-contained, the compression above runs entirely in the
browser: sine-wave.js computes the three probability scores from this one
session's slider events and never sends anything to a server. That shortcut
is fine for a teaching demo, but it sidesteps a real design decision every
xAPI-emitting MicroSim has to make when it's wired up to an actual LRS: how
often should the client actually talk to the server?
Option A — Send every raw interaction. The browser fires off an xAPI
statement for essentially every input event a slider produces —
the finest possible grain, with nothing held back client-side.
Option B — Send only a final summary. The browser scores the session
itself (the same heuristic this demo runs) and, when the MicroSim loses
focus, is closed, or the tab is hidden (blur / visibilitychange), sends
one small payload — e.g. { student, concept: "amplitude", mastery_score:
0.42, evidence_count: 7 } — instead of any of the interactions behind it.
Option C — Send an event only when a new threshold is crossed. The
browser tracks the slider locally but only emits (and sends) an xAPI
statement when the value has moved past a meaningful limit since the last
statement — e.g. amplitude has to move by 10 units, not 1, before another
interacted statement fires. This is often called deadband or
exception-based reporting, and it's a standard pattern in sensor/IoT
telemetry for the same reason a teacher proposes it here: most of the
in-between values carry no new information. This demo already does a
version of this — handleSliderInput() in sine-wave.js computes
emitStep = range / 60 for each slider and only calls emitXapiStatement()
once the value has moved by at least that much, which is why one full slider
sweep produces on the order of 60 statements instead of the hundreds of raw
input events the browser actually fires. A real implementation could tune
that threshold — or make it adaptive — independently for amplitude, frequency,
and phase.
| Dimension | A: every raw interaction | B: summary only, sent on blur | C: threshold-triggered events |
|---|---|---|---|
| Network volume | Highest — one statement per raw input tick; easily hundreds per drag. |
Lowest — one message per session, however long the student explores. | Middle — bounded by how many thresholds exist, not by how long or how finely the student drags (~60 per full sweep in this demo). |
| Why §5.5 sizes the LRS the way it does | This is the traffic pattern §5.5 is built for: ≥ 10,000 statements/sec sustained, bursting to 50,000/sec district-wide. | Barely registers against that target — a few messages per student per session. | A small, predictable fraction of Option A's volume — most of the burst-capacity headroom without giving up a real statement stream. |
| Source of truth | The immutable statement log is the system of record (F-1); every statement is real evidence. | The client's derived number is the record — there is no underlying log to fall back on. | Still a real, replayable statement log (F-1) — just a sparser one. Each statement is still genuine evidence, not a derived guess. |
| Re-analysis | Full fidelity: every historical summary can be replayed and rebuilt from the log (C-2, §12.4) if the mastery heuristic improves later. | Impossible: the scoring logic at the moment of play is baked in permanently. A better algorithm next year can't improve last year's scores. | Mostly possible: replay reproduces coarse-grained mastery signals (range explored, roughly how many times direction changed) but can't reconstruct movement within a threshold band that never crossed it. |
| Trust & integrity | The log lets the server verify how a score was derived and catch a buggy or gamed client-side calculation. | The client is not a trusted execution environment; a tampered page or a bug in computeConceptScore ships a wrong score with nothing to audit it against. |
Same audit story as A, at a coarser resolution — still real statements the server can independently re-aggregate and sanity-check. |
| Cross-session/device view | The server can join this MicroSim's statements with everything else the student has done, on any device. | The client only ever sees its own session; it can't know what the student did yesterday or in a different MicroSim. | Same as A — it's still a real statement stream the server can join with other activity. |
| Offline / low-bandwidth resilience | Needs a durable client-side queue if the network drops mid-session (the LRS's ingest queue, §5.3, only starts once statements arrive). | Naturally resilient — one small sync at session end is easy to retry or defer. | More resilient than A by construction — fewer, larger gaps between sends means fewer statements at risk if a queued batch is lost. |
| Fits this project's data-minimization rule | Only if the "raw interaction" is already a declared learning event (like this demo's slider input), not literal per-pixel mouse coordinates — §12.3 requires "only learning telemetry," not behavioral surveillance. |
Yes, and most conservatively — least data ever leaves the device. | Yes — arguably the best fit: it's still declared learning events, just sampled by an explicit, tunable rule instead of continuously. |
Why this project's LRS spec favors A or C over B
lrs-spec-v1.md §5.6 requires that summary
vertices be compressed server-side from a durable log, never computed
once on the client and thrown away: "Summary vertices MUST be reproducible
by replaying the statement log... They are projections, never sources of
truth" (C-2). Both A and C satisfy that — they differ only in
sampling rate, and the log is still real and replayable either way.
B does not: once only the derived score is sent, the raw evidence
behind it is gone, so there is nothing left to replay.
In short: what's actually at stake with Option B isn't the validity of the
number it sends — a mastery_score computed and stored today stays an
accurate record of what that formula said about that session, forever.
What's lost is the raw evidence behind the number: the coverage, reversal,
and interaction counts that produced it. Some systems are perfectly fine
with that trade — a credit score, for instance, is deliberately frozen to
"the best model at the time," and nobody expects a 2010 score to be
recalculated under a 2024 model. This project's spec chose the opposite: C-2
requires a mastery_score to be reproducible by replaying the log,
specifically because a heuristic this new — revised more than once in this
very demo already — is expected to keep changing as real classroom outcomes
get compared against it. Option A keeps that door open at the highest
bandwidth cost; Option C, what this demo's raw-event panel already
approximates, usually keeps it open more cheaply. Option B closes that door
permanently, by design, in exchange for the smallest possible payload — a
deliberate and reasonable trade for some systems, just not this one.
Server-Side Compression Logic, and the Two-Codebases Problem¶
Options A and C both push real work onto the server the moment a statement
lands. This demo's client-side JavaScript (handleSliderInput,
handleSliderChanged, computeConceptScore in sine-wave.js) has a
server-side counterpart that has to exist for either option to actually
produce a ConceptMastery or MicroSimEngagement vertex.
What the compression pipeline needs, per §5.6
For every (student, concept) grain and every (student, microsim) grain,
the pipeline has to maintain a running aggregate and update it incrementally
as statements arrive:
- Per-grain state — min/max value seen, the last value seen, the
direction of the last observed move, a reversal counter, an event counter,
first_seen/last_seen. This is the same shape as
stats[key]insine-wave.jstoday, just persisted per student on the server instead of per browser tab. - The scoring formula itself.
computeConceptScore()'s coverage / reversal / interaction-count weighting has to be re-implemented (or shared) server-side — amastery_scoreon aConceptMasteryvertex has to be derived from the log, per §5.6; it can't be the number the client already computed and discarded. - Idempotent, absolute-value writes (C-3). The pipeline can't "add one reversal" every time a statement is reprocessed — redelivering the same statement twice has to leave the graph unchanged, so rollups are computed fresh (or from a persisted aggregate), not accumulated destructively.
- Timestamp-driven, not arrival-driven (§5.3).
The browser sees slider moves in true real-time order; the server does not
— statements can arrive out of order from retries, multiple tabs, or
network jitter. An order-dependent signal like "reversals" has to be
computed by sorting a grain's statements by
timestamp, not by processing arrival order, which the client-side version never has to worry about. - Accept-first onboarding (§5.4). If this MicroSim's slider ranges ever change (say amplitude becomes 0–300), the new activity IDs just show up as provisional data — ingestion doesn't block, but a server-side scoring rule still tuned for the old 0–200 range will now silently misjudge "range explored" until someone updates it.
The two-codebases problem
This is the real cost of Options A and C. Today, the MicroSim authoring
workflow's whole value is "one prompt, one self-contained bundle" —
sine-wave.js, main.html, metadata.json, and index.md, generated
together and verified with nothing more than mkdocs serve. Real xAPI
wiring breaks that: the scoring formula now has to exist in two places
that must agree —
- Client-side, for the optional local/live preview this demo shows, and
- Server-side, in the LRS's compression pipeline — a different
repository, most likely a different language, a different deploy
pipeline, and a different set of reviewers than this docs site's
mkdocs gh-deploy.
If those two implementations drift — someone tweaks the reversal weighting
in sine-wave.js and forgets the server — the number a teacher sees live in
the browser will quietly disagree with the number that lands in the
instructor dashboard weeks later, and nobody notices until a report looks
wrong.
Two ways to shrink that gap, rather than pretend it isn't there:
- Push the scoring formula into data, not code.
metadata.jsonalready carries apedagogicalblock per MicroSim. A small declarative scoring-rule descriptor — weights, thresholds, which fields count as "coverage" vs. "reversal" — could live there too, generated once by the same authoring pass that builds the rest of the bundle. A single generic "slider-exploration scorer" in the compression pipeline would read that descriptor at runtime instead of every MicroSim shipping bespoke server-side code. Config can still drift, but it's far easier to diff, version, and validate than two hand-written implementations in two languages. - Stop expecting the two numbers to match at all. Treat the client-side
score explicitly as a provisional "your progress so far" estimate — which
is already how this demo's caveat text is worded — and let the
server-side
mastery_scoredraw on more evidence than any single MicroSim session could ever see (other sessions, other devices, other MicroSims covering the same concept). That turns the two locations from an accidental duplication bug into a deliberate split: fast local feedback versus slow, authoritative truth.
Either way, wiring up real xAPI turns MicroSim authoring from a one-repo, one-review change into at least a two-repo, two-review change, with a version-coordination problem between them that this project's current generation workflow has no step for yet.