Getting Started

Everything on this site is generated by the clocks repository. To run it yourself:

Prerequisites

  • Python 3.12+
  • uv — handles the virtual environment and every dependency.

Install

git clone https://github.com/jbwhit/clocks.git
cd clocks
uv sync

Run the demos

Six animated demos plus the density figure:

uv run demo-1d                  # → output/demo_1d.gif
uv run demo-2d                  # → output/demo_2d.gif
uv run demo-multi-mass          # → output/demo_multi_mass.gif
uv run demo-multi-mass-2d       # → output/demo_multi_mass_2d.gif
uv run demo-model-comparison    # → output/demo_model_comparison.gif
uv run demo-echolocation-3d     # → output/demo_echolocation_3d.gif
uv run demo-density             # → output/demo_density.png

The echolocation range study behind the site page:

uv run scripts/scan_echolocation_range.py --seed-block 0 --workers 8 --per-run

This runs the declared development grid on seeds 0–11. The code locks protected blocks starting at 400 to the scenario-default controls and rejects command-line overrides. Those defaults and the acceptance tolerances are scientifically frozen from the Task 11 development calibration. The 400–411 block was then run exactly once; its tracked results are fixed-case regression evidence, not a population reliability estimate.

Use the library

A complete simulate-then-infer round trip, executed live on this page — if you can read the output below, the install instructions above work:

import numpy as np

from clocks import (
    ClockArray,
    InferenceConfig,
    MassConfig,
    NoiseConfig,
    PriorConfig,
    SimulationConfig,
    infer,
    simulate,
)

clock_array = ClockArray(
    positions=np.array([[-6.0], [-3.0], [0.0], [3.0], [6.0]]),
    track_offset=1.0,
)
ground_truth = MassConfig(
    positions=np.array([[-2.0], [3.0]]),
    masses=np.array([0.045, 0.030]),
)

simulation = simulate(
    SimulationConfig(
        clock_array=clock_array,
        ground_truth=ground_truth,
        noise=NoiseConfig(observation_std=0.005),
        n_observations=40,
        seed=42,
    )
)

result = infer(
    simulation.observations,
    InferenceConfig(
        clock_array=clock_array,
        noise=NoiseConfig(observation_std=0.005),
        prior=PriorConfig(position_range=(-8.0, 8.0), mass_range=(0.005, 0.15)),
        n_particles=1500,
        n_masses=(1, 2, 3),
        ess_target=0.8,
        rejuvenation_steps=2,
        proposal_scale=2.38,
        seed=42,
    ),
)

print(result.best_model)
print(result.posterior_by_model)
2
{1: 2.788513947108434e-205, 2: 0.6086294618645575, 3: 0.39137053813545686}

For fixed-K inference, pass an integer to n_masses; to drive the filter observation-by-observation (e.g. for animation), use build_particle_filter — see The Particle Filter. The position and mass ranges are true support: the point-mass API conditions their uniform rectangle on strict mass ordering and \(|2\Phi|\le0.1\) physical validity.

Next: Reproducibility — seeds, tests, and CI.