Coverage for mcp/metric_readers/aggregate.py: 100%
23 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-15 15:07 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-15 15:07 +0000
1"""Reduce a sequence of observed values down to one number.
3Several metric sources carry history rather than a single point: a training
4run logs a loss every step, a CSV column holds one value per row, a log tail
5matches a pattern many times. A threshold check, though, wants exactly one
6number. This module bridges that gap with a single pure function,
7:func:`reduce_sequence`, that collapses a sequence into one finite number
8according to a caller-chosen mode.
10The supported modes are:
12* ``last`` — the most recent number (the last one in the original order).
13* ``first`` — the earliest number (the first one in the original order).
14* ``min`` — the smallest number.
15* ``max`` — the largest number.
16* ``mean`` — the arithmetic mean of every number.
18Non-numeric entries (booleans, NaN, the infinities, strings, ``None``,
19containers) are never counted: every mode reduces only the values that pass
20the numeric guard, so ``last`` and ``first`` mean "the most recent / earliest
21*number*", skipping anything in between that is not one.
23The reducer distinguishes two empty-ish failures on purpose. A sequence that
24carried no entries at all fails with :attr:`~.shape.ErrorCode.EMPTY_SEQUENCE`;
25a sequence that had entries but none that were numbers fails with
26:attr:`~.shape.ErrorCode.NO_NUMERIC_VALUE`. Keeping them apart lets a caller
27tell "the source was empty" from "the source had data but none of it was a
28number".
29"""
31from __future__ import annotations
33from collections.abc import Sequence
34from typing import Literal, cast
36from . import shape
38# The aggregation modes a caller may ask for. Kept as both a typing Literal
39# (for signatures) and a runtime frozenset (for membership checks).
40AggregationMode = Literal["last", "first", "min", "max", "mean"]
41VALID_MODES: frozenset[str] = frozenset({"last", "first", "min", "max", "mean"})
44def reduce_sequence(values: Sequence[object], mode: str) -> float:
45 """Collapse ``values`` to a single finite number using ``mode``.
47 The reduction proceeds in fixed order so the failure reported is always
48 the most specific one that applies:
50 1. An unrecognized ``mode`` raises
51 :class:`~.shape.MetricReaderError` with code
52 :attr:`~.shape.ErrorCode.INVALID_AGGREGATION_MODE`.
53 2. A sequence with no entries at all raises
54 :attr:`~.shape.ErrorCode.EMPTY_SEQUENCE`.
55 3. Entries that are not real, finite numbers are dropped (booleans, NaN,
56 the infinities, strings, ``None``, containers — anything
57 :func:`~.shape.is_numeric_value` rejects).
58 4. If nothing survives the filter, raise
59 :attr:`~.shape.ErrorCode.NO_NUMERIC_VALUE`.
60 5. Otherwise reduce the surviving numbers: ``last`` and ``first`` pick the
61 most recent / earliest survivor in the original order; ``min``,
62 ``max``, and ``mean`` reduce across all of them.
64 The returned value is always a real, finite number.
65 """
66 if mode not in VALID_MODES:
67 raise shape.MetricReaderError(
68 shape.ErrorCode.INVALID_AGGREGATION_MODE,
69 {"mode": mode, "valid_modes": sorted(VALID_MODES)},
70 )
72 if len(values) == 0:
73 raise shape.MetricReaderError(
74 shape.ErrorCode.EMPTY_SEQUENCE,
75 {"mode": mode},
76 )
78 # Keep only real, finite numbers, preserving their original order so
79 # ``last`` and ``first`` stay meaningful.
80 numeric: list[float] = [cast(float, v) for v in values if shape.is_numeric_value(v)]
82 if not numeric:
83 raise shape.MetricReaderError(
84 shape.ErrorCode.NO_NUMERIC_VALUE,
85 {"mode": mode, "entry_count": len(values)},
86 )
88 if mode == "last":
89 return numeric[-1]
90 if mode == "first":
91 return numeric[0]
92 if mode == "min":
93 return min(numeric)
94 if mode == "max":
95 return max(numeric)
96 # mode == "mean": membership in VALID_MODES guarantees this is the only
97 # remaining possibility.
98 return sum(numeric) / len(numeric)