""" Layer 1 entry: resolve a multi-dimensional training evaluation from a template. Pure calculation orchestration — no DB, no HTTP, no formatting for KI/charts. """ from __future__ import annotations from collections import defaultdict from typing import Any, Dict, List, Mapping, Optional from data_layer.training_profile.algorithms.registry import get_algorithm from data_layer.training_profile.models import ( CalculationTemplate, DimensionResult, DimensionSpec, TrainingBaseProfile, TrainingEvaluationResult, ) def _required_inputs_present( activity_inputs: Mapping[str, Any], keys: tuple[str, ...] ) -> tuple[bool, List[str]]: missing: List[str] = [] for k in keys: if k not in activity_inputs or activity_inputs[k] is None: missing.append(k) return (len(missing) == 0, missing) def _confidence_level(total_dims: int, dims_with_any_missing: int) -> str: if total_dims == 0: return "insufficient" if dims_with_any_missing == 0: return "high" if dims_with_any_missing >= total_dims: return "insufficient" if dims_with_any_missing == 1: return "medium" return "low" def _filter_dimensions( template: CalculationTemplate, base_profile: Optional[TrainingBaseProfile] ) -> tuple[DimensionSpec, ...]: if base_profile is None or base_profile.allowed_dimension_keys is None: return template.dimensions allowed = base_profile.allowed_dimension_keys return tuple(d for d in template.dimensions if d.key in allowed) def resolve_training_evaluation( *, activity_inputs: Mapping[str, Any], template: CalculationTemplate, base_profile: Optional[TrainingBaseProfile] = None, include_trace: bool = False, ) -> TrainingEvaluationResult: """ Run all template dimensions, aggregate Focus Area contributions, attach evidence. activity_inputs: flat dict (e.g. avg_hr, duration_min, distance_km) supplied by caller. """ dimensions = _filter_dimensions(template, base_profile) dimension_results: List[DimensionResult] = [] contributions: Dict[str, float] = defaultdict(float) evidence: Dict[str, Any] = { "dimensions_total": len(dimensions), "inputs_keys": sorted(activity_inputs.keys()), } trace: Optional[Dict[str, Any]] = {} if include_trace else None dims_with_missing = 0 for spec in dimensions: ok, missing = _required_inputs_present(activity_inputs, spec.inputs) if not ok: dims_with_missing += 1 dimension_results.append( DimensionResult( dimension_key=spec.key, algorithm_id=spec.algorithm_id, raw_score=0.0, normalized_score=0.0, missing_inputs=list(missing), evidence={"skipped": True, "reason": "required_inputs_missing"}, ) ) if trace is not None: trace[spec.key] = {"skipped": True, "missing": missing} continue algo = get_algorithm(spec.algorithm_id) slice_inputs = {k: activity_inputs[k] for k in spec.inputs} run = algo(inputs=slice_inputs, params=dict(spec.params)) if run.missing_inputs: dims_with_missing += 1 dimension_results.append( DimensionResult( dimension_key=spec.key, algorithm_id=spec.algorithm_id, raw_score=run.raw_score, normalized_score=run.normalized_score, missing_inputs=list(run.missing_inputs), evidence={"algorithm_detail": run.detail}, ) ) for m in spec.maps_to: contributions[m.focus_area_key] += run.normalized_score * m.weight if trace is not None: trace[spec.key] = { "inputs": dict(slice_inputs), "params": dict(spec.params), "run": { "raw_score": run.raw_score, "normalized_score": run.normalized_score, "missing_inputs": run.missing_inputs, "detail": run.detail, }, "maps_to": [(x.focus_area_key, x.weight) for x in spec.maps_to], } conf = _confidence_level(len(dimensions), dims_with_missing) evidence["dimensions_with_missing_or_failed"] = dims_with_missing return TrainingEvaluationResult( template_id=template.id, template_version=template.version, base_profile_key=base_profile.key if base_profile else None, dimension_results=dimension_results, focus_area_contributions=dict(contributions), confidence=conf, evidence=evidence, trace=trace, ) def resolve_for_base_profile( *, activity_inputs: Mapping[str, Any], base_profile_key: str, include_trace: bool = False, ) -> TrainingEvaluationResult: """Convenience: load profile + default template from registries.""" from data_layer.training_profile.profiles.registry import get_training_base_profile from data_layer.training_profile.templates.registry import get_calculation_template profile = get_training_base_profile(base_profile_key) template = get_calculation_template(profile.default_template_id) return resolve_training_evaluation( activity_inputs=activity_inputs, template=template, base_profile=profile, include_trace=include_trace, )