feat(reports): reference-baseline mode (typed limits / prior averages)
Baseline can now come from fixed values typed per location, not just captured
data — for a spec limit ("L10 = 85") or a prior report's averages when the raw
data isn't available.
- SoundReportConfig.baseline_mode ("captured" | "reference").
- report_pipeline: _location_reference_baseline() reads per-location values from
location_metadata; build_*_night_report honor baseline_mode (reference cells
use the typed value; unset metrics compare against nothing).
- reports router: GET/PUT /reports/baseline (mode on config + per-location values
in location_metadata); config carries baseline_mode; manual view/run fall back
to the saved config's baseline when no explicit dates are given.
- orchestrator + scheduler tick thread baseline_mode through.
Verified end-to-end: PUT/GET /baseline, reference deltas (L10 66.6 vs 85 -> -18.4),
unset metrics compare against nothing, captured-mode regression intact.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -72,6 +72,7 @@ def run_nightly_report(
|
||||
*,
|
||||
metric_keys: Optional[list[str]] = None,
|
||||
windows: Optional[list[Window]] = None,
|
||||
baseline_mode: str = "captured",
|
||||
baseline_start: Optional[date] = None,
|
||||
baseline_end: Optional[date] = None,
|
||||
recipients: Optional[list[str]] = None,
|
||||
@@ -86,6 +87,7 @@ def run_nightly_report(
|
||||
report = build_project_night_report(
|
||||
db, project_id, night_date,
|
||||
metric_keys=metric_keys, windows=windows,
|
||||
baseline_mode=baseline_mode,
|
||||
baseline_start=baseline_start, baseline_end=baseline_end,
|
||||
)
|
||||
|
||||
|
||||
@@ -266,6 +266,36 @@ class LocationNightReport:
|
||||
notes: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def _location_reference_baseline(loc) -> dict:
|
||||
"""A location's manually-entered reference baseline, from its metadata.
|
||||
|
||||
Shape: {window_key: {metric_key: float}} e.g. {"nighttime": {"l10": 85.0}}.
|
||||
Used when baseline_mode == "reference" — fixed targets/limits or prior-report
|
||||
averages typed in, rather than computed from captured nights.
|
||||
"""
|
||||
if not loc:
|
||||
return {}
|
||||
try:
|
||||
meta = json.loads(loc.location_metadata or "{}")
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return {}
|
||||
ref = meta.get("report_baseline") or {}
|
||||
out: dict[str, dict[str, float]] = {}
|
||||
if isinstance(ref, dict):
|
||||
for wkey, mvals in ref.items():
|
||||
if not isinstance(mvals, dict):
|
||||
continue
|
||||
clean = {}
|
||||
for mkey, val in mvals.items():
|
||||
try:
|
||||
clean[mkey] = float(val)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
if clean:
|
||||
out[wkey] = clean
|
||||
return out
|
||||
|
||||
|
||||
def build_location_night_report(
|
||||
db: Session,
|
||||
location_id: str,
|
||||
@@ -273,15 +303,18 @@ def build_location_night_report(
|
||||
*,
|
||||
metric_keys: Optional[list[str]] = None,
|
||||
windows: Optional[list[Window]] = None,
|
||||
baseline_mode: str = "captured",
|
||||
baseline_start: Optional[date] = None,
|
||||
baseline_end: Optional[date] = None,
|
||||
) -> LocationNightReport:
|
||||
"""Build the night-vs-baseline data model for one location.
|
||||
|
||||
`night_date` is the *evening* date of the night being reported (e.g. the
|
||||
7/7 in "night of 7/7 → morning 7/8"). Baseline is the typical-night value
|
||||
across the eligible nights in [baseline_start, baseline_end]; pass neither
|
||||
to skip the comparison (baseline cells become None).
|
||||
7/7 in "night of 7/7 → morning 7/8"). Baseline comes from one of:
|
||||
- "captured": the typical-night value across eligible nights in
|
||||
[baseline_start, baseline_end] (computed from recorded data);
|
||||
- "reference": fixed values typed per location (a spec limit like
|
||||
"L10 = 85", or a prior report's averages).
|
||||
"""
|
||||
metric_keys = metric_keys or DEFAULT_METRICS
|
||||
metrics = [METRIC_REGISTRY[k] for k in metric_keys]
|
||||
@@ -293,8 +326,10 @@ def build_location_night_report(
|
||||
all_rows = _location_leq_rows(db, location_id)
|
||||
night_rows = _rows_in_night(all_rows, night_date)
|
||||
|
||||
reference = _location_reference_baseline(loc) if baseline_mode == "reference" else {}
|
||||
|
||||
baseline_nights: list[date] = []
|
||||
if baseline_start and baseline_end:
|
||||
if baseline_mode != "reference" and baseline_start and baseline_end:
|
||||
baseline_nights = _eligible_nights(all_rows, baseline_start, baseline_end)
|
||||
# Don't let the reported night double as its own baseline.
|
||||
baseline_nights = [n for n in baseline_nights if n != night_date]
|
||||
@@ -304,13 +339,16 @@ def build_location_night_report(
|
||||
table[w.key] = {}
|
||||
for m in metrics:
|
||||
last_night_val = _window_value(night_rows, m, w)
|
||||
baseline_val = None
|
||||
if baseline_nights:
|
||||
if baseline_mode == "reference":
|
||||
baseline_val = reference.get(w.key, {}).get(m.key)
|
||||
elif baseline_nights:
|
||||
per_night = [
|
||||
_window_value(_rows_in_night(all_rows, nd), m, w)
|
||||
for nd in baseline_nights
|
||||
]
|
||||
baseline_val = _combine_across_nights(per_night, m.agg)
|
||||
else:
|
||||
baseline_val = None
|
||||
table[w.key][m.key] = CellPair(last_night_val, baseline_val)
|
||||
|
||||
interval_series = []
|
||||
@@ -325,7 +363,10 @@ def build_location_night_report(
|
||||
notes: list[str] = []
|
||||
if not night_rows:
|
||||
notes.append(f"No data found for the night of {night_date:%m/%d/%y}.")
|
||||
if (baseline_start or baseline_end) and not baseline_nights:
|
||||
if baseline_mode == "reference":
|
||||
if not any(reference.values()):
|
||||
notes.append("Reference-baseline mode is on but no reference values are set for this location.")
|
||||
elif (baseline_start or baseline_end) and not baseline_nights:
|
||||
notes.append("No baseline nights with data in the configured range.")
|
||||
|
||||
return LocationNightReport(
|
||||
@@ -358,6 +399,7 @@ def build_project_night_report(
|
||||
*,
|
||||
metric_keys: Optional[list[str]] = None,
|
||||
windows: Optional[list[Window]] = None,
|
||||
baseline_mode: str = "captured",
|
||||
baseline_start: Optional[date] = None,
|
||||
baseline_end: Optional[date] = None,
|
||||
) -> ProjectNightReport:
|
||||
@@ -375,6 +417,7 @@ def build_project_night_report(
|
||||
build_location_night_report(
|
||||
db, loc.id, night_date,
|
||||
metric_keys=metric_keys, windows=windows,
|
||||
baseline_mode=baseline_mode,
|
||||
baseline_start=baseline_start, baseline_end=baseline_end,
|
||||
)
|
||||
for loc in locations
|
||||
|
||||
@@ -828,6 +828,7 @@ class SchedulerService:
|
||||
result = run_nightly_report(
|
||||
db, cfg.project_id, night_date,
|
||||
metric_keys=metric_keys,
|
||||
baseline_mode=cfg.baseline_mode,
|
||||
baseline_start=cfg.baseline_start,
|
||||
baseline_end=cfg.baseline_end,
|
||||
recipients=recipients,
|
||||
|
||||
Reference in New Issue
Block a user