Skip to content

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:

  1. amplitude (A): the height of the wave's peak, from 0 to 1, read directly off the tick marks on the y-axis
  2. frequency (f): how many complete cycles fit across the drawing width
  3. 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.extensions payload 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 ConceptMastery and MicroSimEngagement summary 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 interacted statement 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 experienced summary. Its controls extension holds, per slider, the concept, the movement count n, min, max, last, and reversals. statements_represented says 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.js sets the default for every MicroSim in the book. Here it is xapi: { compact: true, teaching: false }: compact, and silent.
  • This sim's metadata.json xapi block 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] in sine-wave.js today, 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 — a mastery_score on a ConceptMastery vertex 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 —

  1. Client-side, for the optional local/live preview this demo shows, and
  2. 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.json already carries a pedagogical block 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_score draw 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.