- Introduced new routes and API endpoints for managing personal reference values. - Updated the SettingsPage to include a section for reference values with navigation to manage them. - Enhanced the backend to support reference values in the data layer and versioning. - Added necessary imports and UI components for a seamless user experience.
161 lines
5.4 KiB
Python
161 lines
5.4 KiB
Python
"""
|
|
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,
|
|
)
|