from __future__ import annotations import math from app.schemas.visual_lesson import DifferentialEquationSpec from app.services.differential_equation_compiler import DifferentialEquationCompiler class DifferentialEquationValidationError(ValueError): pass class DifferentialEquationValidator: def __init__(self, compiler: DifferentialEquationCompiler) -> None: self.compiler = compiler def validate(self, spec: DifferentialEquationSpec) -> None: if spec.sample_count != self.compiler.SAMPLE_COUNT: raise DifferentialEquationValidationError("Differential-equation sample count is unsupported") if not math.isfinite(spec.t_start) or not math.isfinite(spec.t_end) or spec.t_end <= spec.t_start: raise DifferentialEquationValidationError("Differential-equation time bounds are invalid") required_views = {"trajectory", "phase_portrait"} if spec.family == "damped_oscillator" else {"trajectory", "direction_field"} if not required_views <= set(spec.enabled_views): raise DifferentialEquationValidationError("The required visual views are missing") claim_ids = {claim.claim_id for claim in spec.evidence_claims} if "ode-initial-value" not in claim_ids: raise DifferentialEquationValidationError("The visualization is missing its initial-value evidence claim") if spec.family != "damped_oscillator" and "ode-direction-field" not in claim_ids: raise DifferentialEquationValidationError("The visualization is missing its direction-field evidence claim") if spec.family == "logistic" and "ode-logistic" not in claim_ids: raise DifferentialEquationValidationError("The logistic visualization is missing its model claim") if spec.family == "damped_oscillator" and "ode-numerical-method" not in claim_ids: raise DifferentialEquationValidationError("The oscillator visualization is missing its numerical-method disclosure") compiled = self.compiler.compile_spec(spec) if not compiled.assertions_passed: raise DifferentialEquationValidationError("Differential-equation compiler assertions failed")