Metadata-Version: 2.5
Name: dive-fit-parse
Version: 1.0
Summary: Deterministic Garmin freediving .FIT parser — per-dive depth, phase and heart-rate indicators
Project-URL: Homepage, https://github.com/ssim-baa/dive-fit-parse
Project-URL: Source, https://github.com/ssim-baa/dive-fit-parse
Author: ssim-baa
License: MIT
Keywords: apnea,fit,fitparse,freediving,garmin
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Requires-Python: >=3.9
Requires-Dist: fit-parse-core>=0.1
Requires-Dist: fitparse>=1.2
Description-Content-Type: text/markdown

# dive-fit-parse

Deterministic Garmin freediving `.FIT` parser. One record per dive: depth,
phase durations, heart rate and equalization indicators.

## Install

```
pip install dive-fit-parse
```

## Use

```
python -m dive_fit_parse <FIT path...>
python -m dive_fit_parse <FIT path...> --json
```

No correction operations. The swim parser's `--drop-lengths` / `--merge` have
no counterpart here: segmentation follows lap boundaries rather than a
threshold, so the ghost-length failure mode does not arise. Across two
sessions (37 dives) there were zero cases needing correction.

## Indicators, not signals

This parser emits **no anomaly signals** — there is no dive equivalent of the
swim parser's F1–F6. Everything below is an indicator; deciding whether one is
normal belongs to P1, and confirmation to the user.

The reason is empirical. Equalization stops fired in 6 of 8 training dives on
2026-08-19 — all six FIM, zero on both CNF — so they track discipline, not
anomaly. A detector firing in three quarters of cases carries maintenance cost
and no information.

Three things that look like anomalies but are not:

- **Equalization stops** — a discipline characteristic. Read alongside
  discipline, depth and the diver's own account.
- **`hr_min_dive`** — no absolute threshold. Meaningful only as a relative
  position, and relative quantities are a runtime concern.
- **`descent_time + hang_time + ascent_time > bottom_time`** — Garmin counts
  `hang_time` *inside* the descent and ascent intervals. This occurs in every
  lap measured (37/37 across two sessions), so it is an accounting artifact,
  not a physical pause. The parser normalizes the phases to sum to
  `bottom_time` and reports the raw excess as `phase_overlap_s` for audit.

## Segmentation

The lap window scopes the search; the dive is the longest contiguous submerged
run within it. Deterministic, with no tuned threshold on the segmentation
itself. Threshold-based segmentation was rejected — `depth > 1.0` silently
dropped a whole dive (lap17 of 2026-08-19, max 1.725 m).

## Null convention

`None` means unmeasured and is never coerced to zero:

- `surface_interval_s` is null for the first dive — Garmin reports the gap
  since the *previous session* there, not an in-session rest.
- `hr_recovery_60s` is null when the next dive starts inside the 60 s window;
  a truncated window would read as poor recovery.
- `eq_stop_count` is null when there is no usable descent phase. No descent is
  not zero stops.

## Equalization threshold

`vertical speed < 0.10 m/s`, `depth > 3 m`, sustained `>= 2 s`.

Descent speed is bimodal across both sessions (median 0.504 m/s in each), with
a stationary mode centred on zero and a trough at 0.20–0.30. The 0.10 cutoff
sits well inside that trough, so only the stationary mode is captured.

**Stop counts are threshold-sensitive** — one dive ranges from 4 to 7 stops
across plausible cutoffs — while the FIM/CNF contrast is stable at every
cutoff. Treat counts as within-session comparisons, not absolute quantities.

MIT. Author `ssim-baa`.
