commit 2759bd3e564bb16ae205942f3e117407843ec575 Author: Mika Date: Sat Jan 17 17:01:57 2026 +0000 Add trace_agg/src/trace_agg/core.py diff --git a/trace_agg/src/trace_agg/core.py b/trace_agg/src/trace_agg/core.py new file mode 100644 index 0000000..81f1ac8 --- /dev/null +++ b/trace_agg/src/trace_agg/core.py @@ -0,0 +1,56 @@ +from __future__ import annotations +import statistics +from typing import List, Dict, Any + + +class InvalidTraceDataError(ValueError): + """Raised when trace data validation fails.""" + pass + + +def _validate_record(record: Dict[str, Any]) -> None: + required_fields = { + 'run_id': str, + 'mischfenster_p50': (int, float), + 'mischfenster_p95': (int, float), + 'mischfenster_max': (int, float), + 'step_order_stability': (int, float), + 'read_between_steps': int + } + for field, ftype in required_fields.items(): + if field not in record: + raise InvalidTraceDataError(f"Missing field '{field}' in trace record") + if not isinstance(record[field], ftype): + raise InvalidTraceDataError( + f"Field '{field}' has invalid type: {type(record[field])}, expected {ftype}" + ) + + +def generate_summary(trace_data: List[Dict[str, Any]]) -> Dict[str, Any]: + """Aggregiert Trace-Daten und erzeugt eine zusammengefasste JSON-Statistik pro Run.""" + assert isinstance(trace_data, list), "trace_data muss eine Liste sein." + if not trace_data: + raise InvalidTraceDataError("Eingabeliste trace_data darf nicht leer sein.") + + for rec in trace_data: + _validate_record(rec) + + p50_values = [float(r['mischfenster_p50']) for r in trace_data] + p95_values = [float(r['mischfenster_p95']) for r in trace_data] + max_values = [float(r['mischfenster_max']) for r in trace_data] + stability_values = [float(r['step_order_stability']) for r in trace_data] + read_values = [int(r['read_between_steps']) for r in trace_data] + + result = { + 'num_records': len(trace_data), + 'mischfenster_p50_mean': statistics.mean(p50_values), + 'mischfenster_p95_mean': statistics.mean(p95_values), + 'mischfenster_max_mean': statistics.mean(max_values), + 'mischfenster_p50_median': statistics.median(p50_values), + 'mischfenster_p95_median': statistics.median(p95_values), + 'mischfenster_max_median': statistics.median(max_values), + 'step_order_stability_avg': statistics.mean(stability_values), + 'read_between_steps_total': sum(read_values), + } + + return result