thunderprint
Work out where and when an audio recording was made using only the thunder in it.
Lightning networks log every strike with millisecond timing and positions accurate to a few hundred metres. Each strike's thunder reaches a listener after a delay set by distance. A recording of a storm therefore carries the listener's position, as the delay pattern across dozens of strikes. thunderprint searches a day of strikes from MET Norway's Frost API for the place and start time whose predicted thunder best matches the recording. It outputs a probability map with a 90% credible region.
The output is a self-contained HTML page, shown above for a real 4-minute recording from northern Norway. The map shows the posterior heatmap and the best candidate with its 90% ellipse. The panel below it plots modeled (red) against observed (blue) thunder over UTC time, above a spectrogram of the clip.
Install
Pick one:
pip install thunderprint # CLI + Python API, prebuilt for Linux, macOS, Windows
cargo install thunderprint # CLI from crates.io
or download a prebuilt binary from Releases.
You also need:
-
ffmpegonPATHto decode audio. Any format ffmpeg reads works. -
A free Frost client ID from frost.met.no, set as
FROST_CLIENT_IDin the environment or in a.envfile in the working directory:FROST_CLIENT_ID=your-client-id
To build from source (Rust stable, edition 2024): git clone https://github.com/Hexay/thunderprint && cargo build --release.
Quickstart
You don't need a real recording to try it. sim synthesises thunder from real strikes at a listener you
choose, and you can then check whether locate finds that listener:
thunderprint sim --date 2025-07-19 --lat 65.089 --lon 19.339 --start 14:40:30 \
--temp=12 --wind-e=4 --wind-n=4 --noise 0.03 --seed 25 --out data/sim/demo
thunderprint locate --audio data/sim/demo/audio.wav --date 2025-07-19 --out out/demo
thunderprint eval --run out/demo --truth data/sim/demo/truth.toml
eval should report an error of about 0.4 km. Open out/demo/index.html to see the map, with modeled and
observed thunder and a spectrogram underneath it. Lightning for each UTC day is downloaded once and cached in data/frost/
(override with THUNDERPRINT_CACHE).
Python
pip install thunderprint
The wheel bundles the compiled binary, so Rust isn't needed. ffmpeg still has to be on PATH.
import thunderprint as tp
res = tp.locate("storm.opus", "2025-07-19", frost_client_id="...") # or set FROST_CLIENT_ID
res.best # Region(rank=1, lat=65.659, lon=14.117, start=datetime(...), reliable=True, ...)
res.regions # all candidates, best first
env = res.envelope() # .times (UTC datetimes), .observed, .modeled
res.spectrogram # .freqs_hz, .offsets_s, .power[band][frame]
res.viewer # path to the interactive HTML map
res.geopackage # path to result.gpkg for ArcGIS / QGIS (geopandas.read_file(res.geopackage, layer="cells"))
# Your own lightning data: a CSV path, a list of dicts, or a pandas DataFrame
res = tp.locate("storm.opus", "2023-06-02", strikes=df) # columns: time, lat, lon[, peak_ka, cloud, error_km]
tp.simulate("2025-07-19", 65.089, 19.339, "14:40:30", "sim/", temp=12, seed=25) # synthetic test clip
tp.load("out/run") # re-read a previous run
Frost downloads are cached in the user cache directory. To use a different location, set THUNDERPRINT_CACHE.
Usage
| Command | What it does |
|---|---|
locate --audio F --date D [--start HH:MM:SS] |
Searches every strike on UTC day D for where and when F was recorded. If you know the start time, --start narrows the search to ±0.5 s. |
eval --run DIR --truth T |
Scores a finished run against a truth file (lat/lon TOML). |
sim --date D --lat .. --lon .. --start .. |
Writes a synthetic audio.wav and truth.toml. Run --help for noise, wind, stereo and propagation options. |
fetch --date D |
Downloads and caches one day of Frost lightning ahead of time. |
probe ... |
Diagnostics: scores one point at every pipeline stage. |
locate, sim and probe take --strikes file.csv to use lightning data from any network instead of Frost:
| Column | Required | Meaning |
|---|---|---|
time |
yes | UTC, as epoch seconds or ISO-8601 (2025-07-19T16:00:01.25Z) |
lat, lon |
yes | Strike position, in degrees |
peak_ka |
no | Peak current in kA. Default 10. |
cloud |
no | 1/true for in-cloud, otherwise cloud-to-ground |
error_km |
no | Location error, as a 50% radius. Default 0.5. |
Accuracy depends on the network's timing and position errors. Frost/NORDLIS reaches about 200 m; Blitzortung's public feed is about 3 km.
locate writes five files to --out (default out/run):
index.html: the map viewer. Under the map, it plots the modeled and observed thunder envelopes over UTC time, with a 10–1000 Hz spectrogram on the same time axis. Clicking a candidate region switches the modeled curve to that region.result.gpkg: a GeoPackage for ArcGIS Pro or QGIS (drag it in), in WGS 84. It has four layers:cells: posterior grid polygonscandidates: pointsellipses: 90% regions, linked to candidates byrankonsets: onset-solve fixes
heatmap.geojson: the same features in a single GeoJSON filecandidates.json: ranked regions, each with its start time, fitted atmosphere, 90% area and areliableflagtrace.json: the envelope curves and spectrogram shown in the viewer
Models and overrides
--model improved(the default) is the model described below.--model baselinereimplements the method from the video that inspired this project (see Prior art). It's there for comparison.--overrides file.tomloverrides search settings. The fields are declared insrc/config.rsandexperiments/has examples.
How it works
- Observed envelope. The audio is band-passed to 20–250 Hz, where thunder carries far and through walls. It's then reduced to a smoothed, compressed loudness envelope.
- Predicted envelope. For a candidate listener position, every strike is rendered as a thunder pulse.
- Cloud-to-ground strikes are modelled as extended vertical channels, intra-cloud strikes as horizontal sources aloft.
- Arrival time is slant distance divided by the speed of sound, corrected for wind.
- Loudness scales with peak current and falls off with range.
- Each pulse has a rumble tail whose length grows with the channel's spread of arrival times.
- Match. The predicted envelope is correlated with the observed one at every possible start time in the day, using FFT cross-correlation.
- Search, coarse to fine. A coarse grid covers the whole strike region, then mid and fine grids refine the best peaks.
- The fine stage marginalises over nuisance atmospheres: temperature (speed of sound), wind, refraction lapse rate and the effective height thunder onsets are timed from.
- Posterior. Scores are tempered into a posterior, from which the tool reports ranked regions and 90% credible ellipses. A region is marked unreliable when fewer than 30 strikes would have been audible from it.
Two physical findings drove the model. Both are written up in research/:
- CG onset height. Real thunder onsets behave as if they come from about 6 km up, not from the channel base (see empirical_thunder_response.md).
- Refraction shadow. Upward refraction hides the low part of distant channels (see thunder_physics_lit.md).
Accuracy and limitations
| Case | Error |
|---|---|
| Synthetic clips, listener near active storms | 0.4–1.0 km |
| Synthetic twin of the real clip | 1.4 km |
| Real 4-minute recording, northern Norway | 1.3 km, versus 2.2 km for the baseline method. Not blind: the truth was known during development. |
Limitations:
- Coverage. Frost lightning covers the Nordic detection network (Norway and neighbouring countries) and nothing else. For anywhere else, use
--strikeswith data from another network. - Recording length. You need several minutes of unedited audio recorded inside an active storm. Short or edited clips don't hear enough strikes to fix a position.
- Geometry. If every strike comes from one direction, the result is a stretched ellipse. Strikes from several directions give a tight fix.
- Video clocks. In a video, the visible flashes give the frame clock. The audio track can be seconds away from that, so
--starttaken from flashes needs care. - Real clips fit worse than simulations. The model is still incomplete, and the credible regions on real audio should be read as optimistic.
Prior art
- The baseline method comes from colsto's videos Can you geolocate a video using background thunder sounds? and I located a fan's video using thunder sounds. The method is a point source, 343 m/s, inverse-square attenuation and cosine similarity.
- Related work on timing and locating recordings is surveyed in
research/prior_art_receiver_location.md.
Data and attribution
- Lightning data: from MET Norway via the Frost API, licensed under NLOD 2.0 / CC BY 4.0. Raw strike files are not redistributed here; each user downloads them with their own client ID.
- Map tiles in the viewer: © Esri, HERE, Garmin and © OpenStreetMap contributors.
License
Metadata
Release files for thunderprint 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| thunderprint-0.1.1.tar.gz | 78.3 kB | Without provenance | Details for thunderprint-0.1.1.tar.gz |
Built distributions (wheels)
| File | Actions Reset | |||
|---|---|---|---|---|
| thunderprint-0.1.1-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details for thunderprint-0.1.1-py3-none-win_amd64.whl |
| thunderprint-0.1.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | Python 3 | none | Linux glibc 2.17+ x86-64 | Details for thunderprint-0.1.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
| thunderprint-0.1.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | Python 3 | none | Linux glibc 2.17+ ARM64 | Details for thunderprint-0.1.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
| thunderprint-0.1.1-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details for thunderprint-0.1.1-py3-none-macosx_11_0_arm64.whl |
| thunderprint-0.1.1-py3-none-macosx_10_12_x86_64.whl | Python 3 | none | macOS 10.12+ x86-64 | Details for thunderprint-0.1.1-py3-none-macosx_10_12_x86_64.whl |
Total release size: 17.0 MB
Release files / thunderprint-0.1.1.tar.gz
| Download URL | thunderprint-0.1.1.tar.gz |
|---|---|
| Size | 78.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3647bb14d8963ed63a6030773b82bd6b4666a44bb9c1ecc5aad7cf73accc09f7
|
|
BLAKE2b-256 checksum How to use checksums |
eabd64e9cf9b7f31f8fdfb864d7d0ca697edb4eee4a5b2406510702f9c324f15
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / thunderprint-0.1.1-py3-none-win_amd64.whl
| Download URL | thunderprint-0.1.1-py3-none-win_amd64.whl |
|---|---|
| Size | 3.5 MB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
915ed02d5e7e0aa66757bb9769285ea431c34e3fdfe66cfa840d92d36c3c6f47
|
|
BLAKE2b-256 checksum How to use checksums |
fa2d16fd929ee7b4e25bad9047c1e48da49c70f69b2d73d66ee66e052ebd391c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / thunderprint-0.1.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | thunderprint-0.1.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 3.7 MB |
| Tags | Linux glibc 2.17+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
e160c1a0e32c702bfc3d890343cb37ecc1d85f5e846d793d34c10e55360b2775
|
|
BLAKE2b-256 checksum How to use checksums |
ce559f19251117ae63eea26d1797b775f94e4a01505e1a6575a00bd7d95ac31b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / thunderprint-0.1.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | thunderprint-0.1.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 3.2 MB |
| Tags | Linux glibc 2.17+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
18119f6d77789740e8f2772813c261d6b06666754989e4ca168d34939f461e67
|
|
BLAKE2b-256 checksum How to use checksums |
77e90a4df73f9169567ba85f11db324bd22824528eddc0f515aa389d9160de73
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / thunderprint-0.1.1-py3-none-macosx_11_0_arm64.whl
| Download URL | thunderprint-0.1.1-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 3.0 MB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
c60fec27b985b29e9b980a8b38af481d1e2d7b62c9e0be11821a9c00da7af2a1
|
|
BLAKE2b-256 checksum How to use checksums |
304355587d58b3d66423befd6f3885b2978bd6dd2f0c0a1507d480b89d3b0cce
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / thunderprint-0.1.1-py3-none-macosx_10_12_x86_64.whl
| Download URL | thunderprint-0.1.1-py3-none-macosx_10_12_x86_64.whl |
|---|---|
| Size | 3.4 MB |
| Tags | Python 3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
d57a9dd70718f085abc7749c2e0dbbe3c46fa7edef0b4a6acc8e3658cb911470
|
|
BLAKE2b-256 checksum How to use checksums |
6d0e66c50e61c993fbf859feab1942e41c421eafdedc7cc23021b8367c0ca528
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for thunderprint 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.