Python reimplementation proof-of-concept of the HYPOELLIPSE local-earthquake locator, validated against the original Fortran 77 program as an oracle.
The goal of the PoC is to reproduce a Fortran HYPOELLIPSE location run within
geophysical tolerance and structural equivalence — starting from the example/
run shipped with the oracle (3 Norcia-2016 events) and generalised to further
reference datasets.
Status: the PoC is complete and generalises beyond the original example. The
Python pipeline (read inputs -> weighted least-squares location -> write
.arc/.sum) reproduces the example's 3 events within tolerance, with
structural equivalence and 97/106 .arc lines byte-identical to the Fortran
output. It has additionally been validated on two private reference datasets
(107 further events, see Reference datasets below).
Magnitude is out of scope: coda/amplitude magnitude is computed by other software. The magnitude code path stays faithful to the Fortran (it emits a blank when the input carries no amplitude/coda data), but it is not a goal and not gated. The gate is a location gate.
- Docker (all builds, runs and tests happen inside containers — nothing on the host).
- Git with submodule support.
The Fortran oracle (INGV/hypoellipse) is pinned as a git submodule under
ext/hypoellipse. After cloning, initialise it:
git submodule update --initThe reference .arc/.sum outputs are not committed upstream in
INGV/hypoellipse (only the inputs are). They are regenerated here by running
the pristine Fortran oracle on the committed example inputs, and snapshotted into
tests/golden/ as the versioned reference.
# 1. Build the oracle image from the pinned submodule source
bash scripts/oracle-build.sh
# 2. Regenerate and verify the golden (runs the oracle twice for a determinism
# check, then diffs against tests/golden/; exits non-zero on any mismatch)
bash scripts/oracle-golden.shThe Fortran location run is deterministic (ran3 uses a fixed seed), so the two
runs are byte-identical and the snapshot is stable.
To intentionally re-baseline the golden (e.g. after bumping the oracle version):
GOLDEN_UPDATE=1 bash scripts/oracle-golden.shThe Python code lives in src/pyhypoellipse/ and runs inside a dedicated poc
container image — never on the host.
# Build the Python poc image (Python 3.12 + numpy + fortranformat + pytest)
bash scripts/poc-build.sh
# Run the test suite in the container (repo is live-mounted, so editing code
# needs no rebuild). Includes the primary gate: generated .arc/.sum vs golden.
bash scripts/test.sh
bash scripts/test.sh -v -k matches_golden # extra pytest args pass through
# (runs just the .arc/.sum gate)
# Run the pipeline end-to-end on the example inputs; writes the generated
# .arc/.sum into scratch/ (gitignored)
bash scripts/poc-run.shThe primary gate (tests/test_writers.py) runs the whole pipeline and compares
the generated .arc/.sum against tests/golden/ with numeric tolerance
(src/pyhypoellipse/tolerances.py) plus structural equivalence (same stations,
same weight codes). tests/test_multidataset_gate.py applies the same gate,
event by event, to every reference dataset present.
Beyond the example, the gate runs over any dataset placed under
tests/datasets/<name>/{input,golden}/, discovered at collection time. Datasets
are not published in this repository: the ones used so far consist of
colleague-provided phase picks and station lists that are not ours to
redistribute, so tests/datasets/*/ is gitignored and each dataset stays local
to the machine that holds it. See tests/datasets/README.md for the expected
layout. A fresh clone therefore runs the example gate only, and that is the
intended behaviour, not a broken checkout.
| Path | Purpose |
|---|---|
ext/hypoellipse |
Pinned Fortran oracle source (git submodule) |
src/pyhypoellipse/ |
The Python reimplementation (readers, travel time, locate, writers, magnitude, comparator) |
tests/ |
pytest suite; tests/golden/ holds the reference .arc/.sum |
tests/datasets/ |
Extra reference datasets, local-only (gitignored) |
docker/poc/ |
Dockerfile for the Python poc image |
scripts/ |
Bash orchestration wrappers around docker build/run |
pyproject.toml |
Package metadata and dependencies |
scratch/ |
Throwaway container run dirs (gitignored) |
This is a proof-of-concept, not a production replacement. It ports the location
critical path exercised by the reference datasets; branches those datasets never
reach (linear-gradient and travel-time-table velocity models, variable Vp/Vs
models, scatter/RNG, station-calibration magnitude, azimuthal and boxcar
weighting, ...) raise NotImplementedError rather than silently mis-handling
input. First-motion plotting is out of scope.
Success is defined as a hybrid match rather than byte-exactness: gated numeric fields within geophysical tolerances (position ~0.1 km, depth ~0.5 km, origin time ~0.05 s, RMS ~0.02 s) plus structural equivalence. Byte-exactness is infeasible — the Fortran mixes single and double precision — and is tracked only as an observation.