Source code for epimodels.validation.specs
"""
Specification classes for parameters, state variables, and constraints.
"""
from dataclasses import dataclass, field
from typing import Any
from enum import Enum
[docs]
class DomainType(Enum):
"""Type of numeric domain for parameters and variables."""
CONTINUOUS = "continuous"
DISCRETE = "discrete"
CATEGORICAL = "categorical"
INTEGER = "integer"
[docs]
@dataclass
class ParameterSpec:
"""
Rich specification for a model parameter.
Attributes:
name: Parameter identifier (used in code)
symbol: LaTeX representation for display
description: Human-readable description
domain_type: Type of numeric domain
bounds: Optional (min, max) tuple. Use None for unbounded.
Example: (0, None) means positive, (0, 1) means probability
dtype: Expected Python type (float, int, list, etc.)
default: Default value if parameter is optional
required: Whether parameter must be provided
constraints: List of constraint expressions (e.g., "value > 0", "value < other_param")
units: Physical units (e.g., "1/day", "individuals")
typical_range: Typical range for documentation (min, max)
Example:
>>> spec = ParameterSpec(
... name="beta",
... symbol=r"$\\beta$",
... description="Transmission rate",
... bounds=(0, None),
... constraints=["value > 0"]
... )
"""
name: str
symbol: str
description: str = ""
domain_type: DomainType = DomainType.CONTINUOUS
bounds: tuple[float | None, float | None] | None = None
dtype: type = float
default: Any | None = None
required: bool = True
constraints: list[str] = field(default_factory=list)
units: str | None = None
typical_range: tuple[float, float] | None = None
def __post_init__(self):
"""Validate spec consistency."""
if self.bounds is not None:
min_val, max_val = self.bounds
if min_val is not None and max_val is not None and min_val >= max_val:
raise ValueError(
f"Invalid bounds for parameter '{self.name}': "
f"min ({min_val}) must be less than max ({max_val})"
)
[docs]
@dataclass
class VariableSpec:
"""
Specification for a state variable.
Attributes:
name: Variable identifier (used in code)
symbol: LaTeX representation
description: Human-readable description
bounds: Optional (min, max) tuple
non_negative: Whether variable must be >= 0
constraints: List of constraint expressions
units: Physical units
Example:
>>> spec = VariableSpec(
... name="S",
... symbol="S",
... description="Susceptible individuals",
... non_negative=True
... )
"""
name: str
symbol: str
description: str = ""
bounds: tuple[float | None, float | None] | None = None
non_negative: bool = True
constraints: list[str] = field(default_factory=list)
units: str | None = None
def __post_init__(self):
"""Validate spec consistency."""
if self.bounds is not None:
min_val, max_val = self.bounds
if min_val is not None and max_val is not None and min_val >= max_val:
raise ValueError(
f"Invalid bounds for variable '{self.name}': "
f"min ({min_val}) must be less than max ({max_val})"
)
if self.non_negative and self.bounds is None:
self.bounds = (0.0, None)
elif self.non_negative and self.bounds is not None:
min_val, max_val = self.bounds
if min_val is not None and min_val < 0:
raise ValueError(
f"Variable '{self.name}' is marked non_negative but has min bound {min_val}"
)
[docs]
@dataclass
class ModelConstraint:
"""
Cross-parameter or model-level constraint.
Attributes:
expression: Constraint expression (Python expression or SymPy-parseable)
Can reference parameter names directly.
Examples: "beta > gamma", "p + q <= 1", "R0 > 1"
description: Human-readable description of the constraint
severity: "error" (raises exception) or "warning" (logs warning)
name: Optional name for the constraint
Example:
>>> constraint = ModelConstraint(
... expression="beta / gamma > 1",
... description="R0 > 1 required for epidemic",
... severity="warning"
... )
"""
expression: str
description: str = ""
severity: str = "error"
name: str | None = None
def __post_init__(self):
"""Validate constraint."""
if self.severity not in ("error", "warning"):
raise ValueError(f"Invalid severity '{self.severity}'. Must be 'error' or 'warning'")
if not self.description:
self.description = f"Constraint: {self.expression}"