description: chiptime.metrics API: analyze, detect_structure, pacing, splits, zones, and load estimators with explicit evidence bases.¶
Python API — metrics¶
The optional analytics package: from chiptime import metrics. Never imported by
the core; everything is a pure function of the parsed model plus optional
AthleteSettings — no state, no
network, no wall clock, deterministic to the byte.
The mental model¶
analyze(result, settings) ──► ActivityReport ──► [WorkoutReport per session]
│
│ composes public pieces you can also call directly:
├─ profile_for(session) which sport language to speak
├─ primary_signal(session) watts or speed, given what exists
├─ distance_splits(session) km/mile splits
├─ detect_structure(session, messages) intervals, with evidence
├─ time_in_zones(...) only when zone bounds were provided
└─ workout_load(session, settings) power+ftp → hr-trimp → omitted
Three rules govern every number that comes out:
- Basis strings. Derived values name their evidence — a load is
power+ftporhr-trimp, structure islaps:manualordetected:power-steps. You always know what a number is standing on. - Omissions over guesses. Thresholds (FTP, max HR, zones) come from your
AthleteSettingsor from messages inside the file — never estimated from the workout. Missing inputs produce an entry inomissions[], not a number. - Compose freely.
analyzeis a convenience over public parts. Only want interval detection? Calldetect_structure(session, result.messages)and skip the rest.
Reports¶
chiptime.metrics.insights.analyze ¶
Report per session from a ParseResult (uses .activity and .messages).
chiptime.metrics.insights.analyze_session ¶
analyze_session(
session: Session,
messages: list[Message] | None = None,
settings: AthleteSettings | None = None,
) -> WorkoutReport
chiptime.metrics.insights.WorkoutReport
dataclass
¶
Everything is optional and null-honest; omissions says what was not
computed and why. basis strings mark where derived numbers came from.
chiptime.metrics.insights.Insight
dataclass
¶
One notable observation: a stable machine code (see
INSIGHT_CODES), a human sentence, and the numbers behind it.
Settings & zones¶
chiptime.metrics.settings.AthleteSettings
dataclass
¶
All fields optional; absent means "don't compute what needs it".
chiptime.metrics.zones.hr_zone_bounds ¶
hr_zone_bounds(
settings: AthleteSettings | None,
messages: list[Message] | None = None,
) -> tuple[tuple[float, ...] | None, str | None]
Ascending upper bounds (bpm) + their basis ("settings" | "file:hr_zone").
chiptime.metrics.zones.power_zone_bounds ¶
power_zone_bounds(
settings: AthleteSettings | None,
messages: list[Message] | None = None,
) -> tuple[tuple[float, ...] | None, str | None]
Ascending upper bounds (W) + their basis ("settings" | "file:power_zone").
Sport profiles¶
chiptime.metrics.sports.SportProfile
dataclass
¶
How one sport measures itself — profiles are data, not subclasses.
Analytics code branches on these fields, never on sport names, so adding a sport is a table row, not a code path.
Attributes:
| Name | Type | Description |
|---|---|---|
key |
str
|
Profile name ( |
pace_style |
PaceStyle
|
How speed is presented — |
primary |
Literal['power', 'speed']
|
Preferred intensity signal when its stream exists
( |
cadence_units |
str
|
Display convention ( |
cadence_double_if_per_leg |
bool
|
Running heuristic — cadence below 130 is per-leg strides on many devices; doubled for display, labeled. |
distance_from_lengths |
bool
|
Pool truth — distance is lengths x pool size, never GPS. |
chiptime.metrics.sports.profile_for ¶
Resolve (sport, sub_sport) → profile; unknown sports get GENERIC (correct-but-shallow beats wrong-but-specific).
chiptime.metrics.sports.primary_signal ¶
The intensity signal actually available: profile preference constrained by which streams exist. Returns (kind, stream_name); kind is "power" | "speed" | "none".
chiptime.metrics.sports.cadence_display ¶
cadence_display(
avg_cadence: float | None, profile: SportProfile
) -> tuple[float | None, str, str | None]
(value, units, note). The doubling heuristic is labeled, never silent.
Pacing & splits¶
chiptime.metrics.pacing.pace_seconds ¶
Seconds per style unit; None for absent/zero speed or style "speed".
chiptime.metrics.pacing.format_pace ¶
"4:20" (/km, /100m) or "1:52.5" (/500m, rowing shows tenths).
Rounding is explicit half-up (int(x + 0.5)) so Python's banker's rounding can never make two runtimes disagree on a boundary value.
chiptime.metrics.pacing.distance_splits ¶
distance_splits(
session: Session,
split_m: float = 1000.0,
*,
style: PaceStyle = "per_km",
) -> list[Split]
Distance-domain splits from the cumulative distance stream.
Boundary crossings are linearly interpolated between records; each record's samples are attributed to the split where its step started (deterministic). No distance stream → [] (pool swims split by lengths instead — F24). HR/power averages are record-domain means (1 Hz files: time-domain too); altitude ascent/descent from consecutive present values.
chiptime.metrics.pacing.session_pace_s ¶
Overall pace from totals, preferring the moving denominator (research §0), falling back timer → elapsed. Returns (pace_s, basis).
chiptime.metrics.pacing.split_500m_to_watts ¶
Concept2 published relation (see CONCEPT2_COEFF).
Intervals¶
chiptime.metrics.intervals.detect_structure ¶
detect_structure(
session: Session,
messages: list[Message] | None = None,
settings: AthleteSettings | None = None,
) -> IntervalStructure
Evidence ladder: workout steps → manual laps → swim sets → band
detection → none. messages (from ParseResult.messages) unlocks the lap
and workout-step rungs and pool length; without it those rungs are
skipped (declared honestly in the note).
chiptime.metrics.intervals.IntervalStructure
dataclass
¶
The structure reading for one session — always with its evidence.
Attributes:
| Name | Type | Description |
|---|---|---|
basis |
str
|
Where the structure came from: |
intervals |
tuple[Interval, ...]
|
The segments, in time order (empty for |
repeats |
tuple[RepeatGroup, ...]
|
Grouped "N x ..." patterns among the work intervals. |
note |
str | None
|
For |
chiptime.metrics.intervals.Interval
dataclass
¶
One segment of the workout, in time order.
Attributes:
| Name | Type | Description |
|---|---|---|
index |
int
|
1-based position. |
kind |
str
|
|
start_time |
datetime | None
|
Segment start. |
end_time |
datetime | None
|
Segment end. |
duration_s |
float | None
|
Length in seconds. |
distance_m |
float | None
|
Distance covered, when a distance stream exists. |
avg_primary |
float | None
|
Mean of the primary signal (W or m/s) over the segment. |
avg_hr |
float | None
|
Mean heart rate, when present. |
lengths |
int | None
|
Pool swims — lengths in this swim; None elsewhere. |
step_index |
int | None
|
Structured workouts — the |
chiptime.metrics.intervals.RepeatGroup
dataclass
¶
N similar consecutive work intervals, in athlete notation.
Attributes:
| Name | Type | Description |
|---|---|---|
count |
int
|
Number of reps. |
kind |
str
|
What repeats ( |
mean_duration_s |
float | None
|
Mean rep duration. |
mean_distance_m |
float | None
|
Mean rep distance, when known. |
mean_primary |
float | None
|
Mean intensity across reps (W or m/s). |
mean_rest_s |
float | None
|
Mean recovery between reps, when detectable. |
label |
str
|
The human line — |
first_index |
int
|
|
Load¶
chiptime.metrics.load.workout_load ¶
Estimator ladder: power+ftp -> hr TRIMP -> None. A missing number beats an invented one; the report records the omission reason.
chiptime.metrics.load.LoadEstimate
dataclass
¶
A load number that says where it came from (ADR-0008 §5).
chiptime.metrics.load.weighted_avg_power ¶
4th-power-weighted mean over a 30-sample rolling mean. Zeros are real (coasting) and stay in; nulls (dropouts) are skipped, never zero-filled. None when fewer than one full window of samples is present.
chiptime.metrics.load.trimp ¶
trimp(
times: list[datetime | None],
hr_values: list[object],
*,
resting_hr: float,
max_hr: float,
sex: str | None = None,
) -> float | None
Banister TRIMP. sex picks the published coefficient (1.92 male /
1.67 female); unset uses the male coefficient — callers surface that in
the basis string. None if the HR reserve is degenerate or no data.
chiptime.metrics.load.fitness_fatigue_form ¶
Impulse-response over a day series. Missing days count as 0 load. Seeds at 0 (an athlete's true starting fitness is unknowable from one archive slice — stated, not guessed).
chiptime.metrics.load.hr_coverage_fraction ¶
Fraction of the session duration covered by present-HR sample pairs. None when there is no HR stream or no duration to compare against.
Basics¶
chiptime.metrics.mean_max ¶
Best rolling average per window size, in the RECORD domain.
At 1 Hz recording (the dominant case) record-domain == time-domain; for smart-recording files interpret windows as record counts. Windows with less than 90% data coverage return None — absence is not zero.
chiptime.metrics.time_in_zones ¶
time_in_zones(
times: list[datetime | None],
values: list[Any],
bounds: list[float],
) -> list[float]
Seconds spent per zone. Zones: (-inf, b0], (b0, b1], ..., (bn, inf) — len(bounds)+1 buckets. dt attribution per record, capped at 30 s (a gap is a gap, not an hour in zone 2). None samples contribute nowhere.
chiptime.metrics.swolf ¶
Per-active-length SWOLF (strokes + seconds) and the mean over lengths where both parts are present. Pool swimming only (#73).