The virtual pendulum lab
Open the live demo · Read the source · View on GitHub
A teaching lab needs a simulation small enough that changing one number visibly changes how a run unfolds, and an instructor needs to know exactly where a student's run stopped matching the assignment. A pendulum is the first, and the engine's own digest trace is the second.
Note.
flutter3d_lab's realPendulumSimulationis not a dependency of this app. This page reimplements its formula by hand, a dozen lines of semi-implicit Euler, and checks it with the realDigestTracethe rest of the simulation package uses.
Step 1: Run the assignment #
Semi-implicit Euler: the velocity updates first, then the position from the updated velocity, the same order the rest of the engine steps everything else with.
/// The lab's own worked example: semi-implicit Euler over a gravity
/// pendulum, the same order of integrator the rest of the engine steps
/// with.
final class _Pendulum {
_Pendulum({required this.lengthMeters, double startAngle = 0.6})
: theta = startAngle,
omega = 0.0;
static const double _gravity = 9.81;
double lengthMeters;
double theta;
double omega;
void step(double dt) {
final alpha = -(_gravity / lengthMeters) * Portable.sin(theta);
omega += alpha * dt;
theta += omega * dt;
}
Map<String, Object?> get state => <String, Object?>{
'theta': theta,
'omega': omega,
};
}
final assignment = DigestTrace(every: 10);
final assignmentPendulum = _Pendulum(lengthMeters: 1.0);
for (var step = 1; step <= 60; step++) {
assignmentPendulum.step(1 / 60);
assignment.observe(step, assignmentPendulum.state);
}
Step 2: Run a student's attempt #
This student changes the pendulum's length partway through the run.
// A student who changed the length partway through the run.
final studentPendulum = _Pendulum(lengthMeters: 1.0);
final student = DigestTrace(every: 10);
for (var step = 1; step <= 60; step++) {
if (step == 30) studentPendulum.lengthMeters = 1.3;
studentPendulum.step(1 / 60);
student.observe(step, studentPendulum.state);
}
Step 3: Find where they parted #
final where = student.divergenceFrom(assignment.digests);
The two runs agree at every checkpoint before the change, and the checkpoint covering step 30 is the first to disagree — exactly where the student's length stopped matching the assignment's.
Step 4: Watch two runs part #
Two pendulums swing side by side, the assignment in green and the student's in
orange, and a row of lamps below them lights a checkpoint at a time: green
while the two runs agree there, red from the first checkpoint that does not.
The student's length changes at the step on the slider, and the orange pendulum
lengthens under your eyes; the first red lamp is the checkpoint covering that
step, which is divergenceFrom working. Move the step or the new length and
the run starts again.
// The same two runs as above, a step at a time: one checkpoint every
// `_every` steps, and the student's length changes when the slider
// says.
_assignmentLive.step(1 / 60);
_assignmentTrace.observe(_step, _assignmentLive.state);
if (_step == changeAt.round()) _studentLive.lengthMeters = newLength;
_studentLive.step(1 / 60);
_studentTrace.observe(_step, _studentLive.state);