Parameter Validation#

This tutorial demonstrates the strengths of using Pydantic’s BaseModel to define simulation parameters. Pydantic provides type validation, documentation, and error handling that makes component configuration more robust and user-friendly.

Benefits:

  1. Type Validation: Automatic validation of parameter types and values

  2. Range Constraints: Built-in support for numerical bounds (gt, ge, lt, le)

  3. Documentation: Self-documenting parameters with descriptions

  4. Default Values: Clear default parameter values

  5. Error Messages: Detailed error messages for validation failures

  6. IDE Support: Better autocomplete and type hints

  7. Serialization: Easy conversion to/from JSON and dictionaries

[1]:
import traceback
from datetime import date

import polars as pl
from pydantic import Field
from pydantic import ValidationError

from laser_measles.biweekly.components import CaseSurveillanceParams
from laser_measles.biweekly.components import ImportationPressureParams

# Import the component parameter classes
from laser_measles.biweekly.components import InfectionParams
from laser_measles.biweekly.components import SIACalendarParams

Type validation and default values#

Pydantic automatically validates parameter types and provides clear default values:

[2]:
print("=== Type Validation and Default Values ===")

# Create parameters with default values
infection_params = InfectionParams()
print("Default InfectionParams:")
print(f"  beta: {infection_params.beta}")
print(f"  seasonality: {infection_params.seasonality}")
print(f"  season_start: {infection_params.season_start}")

# Create parameters with custom values
custom_infection_params = InfectionParams(beta=50.0, seasonality=0.1, season_start=13)
print("\nCustom InfectionParams:")
print(f"  beta: {custom_infection_params.beta}")
print(f"  seasonality: {custom_infection_params.seasonality}")
print(f"  season_start: {custom_infection_params.season_start}")
=== Type Validation and Default Values ===
Default InfectionParams:
  beta: 0.5714285714285714
  seasonality: 0.0
  season_start: 0

Custom InfectionParams:
  beta: 50.0
  seasonality: 0.1
  season_start: 13

Range constraints and validation#

Pydantic enforces numerical constraints automatically:

[3]:
print("\n=== Range Constraints and Validation ===")
print("Testing range constraints:")

# This will work - beta > 0
try:
    valid_params = InfectionParams(beta=15.0)
    print(f"✓ Valid beta=15.0: {valid_params.beta}")
except ValidationError as e:
    print(f"✗ Validation error: {e}")
    print(traceback.format_exc())

# This will fail - beta must be > 0
try:
    invalid_params = InfectionParams(beta=-5.0)
    print(f"✓ Invalid beta=-5.0: {invalid_params.beta}")
except ValidationError:
    print("✗ Validation error for beta=-5.0:")
    print(traceback.format_exc())

# This will fail - seasonality must be 0 <= value <= 1
try:
    invalid_params = InfectionParams(seasonality=1.5)
    print(f"✓ Invalid seasonality=1.5: {invalid_params.seasonality}")
except ValidationError:
    print("✗ Validation error for seasonality=1.5:")
    print(traceback.format_exc())

=== Range Constraints and Validation ===
Testing range constraints:
✓ Valid beta=15.0: 15.0
✗ Validation error for beta=-5.0:
Traceback (most recent call last):
  File "/tmp/ipykernel_1218/1649787988.py", line 14, in <module>
    invalid_params = InfectionParams(beta=-5.0)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/docs/checkouts/readthedocs.org/user_builds/institute-for-disease-modeling-laser-measles/envs/latest/lib/python3.12/site-packages/pydantic/main.py", line 253, in __init__
    validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for InfectionParams
beta
  Input should be greater than or equal to 0 [type=greater_than_equal, input_value=-5.0, input_type=float]
    For further information visit https://errors.pydantic.dev/2.11/v/greater_than_equal

✗ Validation error for seasonality=1.5:
Traceback (most recent call last):
  File "/tmp/ipykernel_1218/1649787988.py", line 22, in <module>
    invalid_params = InfectionParams(seasonality=1.5)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/docs/checkouts/readthedocs.org/user_builds/institute-for-disease-modeling-laser-measles/envs/latest/lib/python3.12/site-packages/pydantic/main.py", line 253, in __init__
    validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for InfectionParams
seasonality
  Input should be less than or equal to 1 [type=less_than_equal, input_value=1.5, input_type=float]
    For further information visit https://errors.pydantic.dev/2.11/v/less_than_equal

Self-documenting parameters#

Pydantic Field descriptions provide built-in documentation:

[4]:
print("\n=== Self-Documenting Parameters ===")

# Display parameter documentation
print("InfectionParams Documentation:")
schema = InfectionParams.model_json_schema()["properties"]
for field_name, field_schema in schema.items():
    print(f"  {field_name}: {field_schema.get('description', 'No description')}")

print("\nImportationPressureParams Documentation:")
for field_name, field_info in ImportationPressureParams.model_fields.items():
    print(f"  {field_name}: {field_info.description} (default: {field_info.default})")

=== Self-Documenting Parameters ===
InfectionParams Documentation:
  beta: Base transmission rate (infections per day)
  seasonality: Seasonality factor, default is no seasonality
  season_start: Season start tick (0-25)
  distance_exponent: Distance exponent
  mixing_scale: Mixing scale
  mixer: Mixing object

ImportationPressureParams Documentation:
  crude_importation_rate: Yearly crude importation rate per 1k population (default: 1.0)
  importation_start: Start time for importation (in days) (default: 0)
  importation_end: End time for importation (in days) (default: -1)

Complex parameter types#

Pydantic handles complex types like DataFrames and functions with proper configuration:

[5]:
print("\n=== Complex Parameter Types ===")

# Create a sample SIA schedule DataFrame
sia_schedule = pl.DataFrame(
    {
        "id": ["country:state1:lga1", "country:state1:lga2", "country:state2:lga3"],
        "date": [date(2024, 3, 15), date(2024, 6, 20), date(2024, 9, 10)],
    }
)


# Custom filter function
def filter_northern_states(node_id: str) -> bool:
    """Filter to include only northern states"""
    return "north" in node_id.lower()


# Create SIACalendarParams with complex types
sia_params = SIACalendarParams(
    sia_efficacy=0.95,
    filter_fn=filter_northern_states,
    aggregation_level=2,
    sia_schedule=sia_schedule,
    date_column="date",
    group_column="id",
)

print("SIA Calendar Parameters created successfully:")
print(f"  Efficacy: {sia_params.sia_efficacy}")
print(f"  Aggregation level: {sia_params.aggregation_level}")
print(f"  Schedule shape: {sia_params.sia_schedule.shape}")
print(f"  Filter function test: {sia_params.filter_fn('country:north_state:lga1')}")

=== Complex Parameter Types ===
SIA Calendar Parameters created successfully:
  Efficacy: 0.95
  Aggregation level: 2
  Schedule shape: (3, 2)
  Filter function test: True

Parameter serialization and persistence#

Pydantic makes it easy to save and load parameter configurations:

[6]:
print("\n=== Parameter Serialization and Persistence ===")

# Serialize parameters to dictionary
infection_dict = custom_infection_params.model_dump()
print("Serialized InfectionParams:")
print(infection_dict)

# Recreate from dictionary
recreated_params = InfectionParams(**infection_dict)
print(f"\nRecreated parameters match: {recreated_params == custom_infection_params}")

# JSON serialization (excluding complex types)
importation_params = ImportationPressureParams(crude_importation_rate=2.5, importation_start=2, importation_end=8)

json_str = importation_params.model_dump_json()
print(f"\nJSON representation: {json_str}")

# Load from JSON
from_json = ImportationPressureParams.model_validate_json(json_str)
print(f"From JSON matches: {from_json == importation_params}")

=== Parameter Serialization and Persistence ===
Serialized InfectionParams:
{'beta': 50.0, 'seasonality': 0.1, 'season_start': 13, 'distance_exponent': 1.5, 'mixing_scale': 0.001, 'mixer': <laser_measles.mixing.gravity.GravityMixing object at 0x721cf27886b0>}

Recreated parameters match: True

JSON representation: {"crude_importation_rate":2.5,"importation_start":2,"importation_end":8}
From JSON matches: True

Parameter validation in practice#

Let’s see how validation helps prevent common configuration errors:

[7]:
print("\n=== Parameter Validation in Practice ===")
print("Testing type validation:")

try:
    # This will be automatically converted
    params = CaseSurveillanceParams(detection_rate="0.15")  # string instead of float
    print(f"✓ String '0.15' converted to float: {params.detection_rate} (type: {type(params.detection_rate)})")
except ValidationError as e:
    print(f"✗ Type conversion failed: {e}")

try:
    # This will fail - can't convert non-numeric string
    params = CaseSurveillanceParams(detection_rate="high")
    print(f"✓ String 'high' converted: {params.detection_rate}")
except ValidationError:
    print("✗ Invalid string conversion:")
    print(traceback.format_exc())

# Common mistake: out of range values
# Note: ImportationPressureParams validation happens in the component's _validate_params method
# Let's demonstrate with a negative importation rate instead:
try:
    params = ImportationPressureParams(crude_importation_rate=-1.0)
    print(f"✓ Negative importation rate accepted: {params.crude_importation_rate}")
except ValidationError:
    print("✗ Negative importation rate caught:")
    print(traceback.format_exc())

# Time range validation happens at component level, not parameter level
params_with_bad_time_range = ImportationPressureParams(importation_start=10, importation_end=5)
print(
    f"✓ Parameters created (time range validation happens in component): start={params_with_bad_time_range.importation_start}, end={params_with_bad_time_range.importation_end}"
)

=== Parameter Validation in Practice ===
Testing type validation:
✓ String '0.15' converted to float: 0.15 (type: <class 'float'>)
✗ Invalid string conversion:
Traceback (most recent call last):
  File "/tmp/ipykernel_1218/4013018586.py", line 13, in <module>
    params = CaseSurveillanceParams(detection_rate="high")
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/docs/checkouts/readthedocs.org/user_builds/institute-for-disease-modeling-laser-measles/envs/latest/lib/python3.12/site-packages/pydantic/main.py", line 253, in __init__
    validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for CaseSurveillanceParams
detection_rate
  Input should be a valid number, unable to parse string as a number [type=float_parsing, input_value='high', input_type=str]
    For further information visit https://errors.pydantic.dev/2.11/v/float_parsing

✗ Negative importation rate caught:
Traceback (most recent call last):
  File "/tmp/ipykernel_1218/4013018586.py", line 23, in <module>
    params = ImportationPressureParams(crude_importation_rate=-1.0)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/docs/checkouts/readthedocs.org/user_builds/institute-for-disease-modeling-laser-measles/envs/latest/lib/python3.12/site-packages/pydantic/main.py", line 253, in __init__
    validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for ImportationPressureParams
crude_importation_rate
  Input should be greater than or equal to 0 [type=greater_than_equal, input_value=-1.0, input_type=float]
    For further information visit https://errors.pydantic.dev/2.11/v/greater_than_equal

---------------------------------------------------------------------------
ValidationError                           Traceback (most recent call last)
Cell In[7], line 30
     27     print(traceback.format_exc())
     29 # Time range validation happens at component level, not parameter level
---> 30 params_with_bad_time_range = ImportationPressureParams(importation_start=10, importation_end=5)
     31 print(
     32     f"✓ Parameters created (time range validation happens in component): start={params_with_bad_time_range.importation_start}, end={params_with_bad_time_range.importation_end}"
     33 )

File ~/checkouts/readthedocs.org/user_builds/institute-for-disease-modeling-laser-measles/envs/latest/lib/python3.12/site-packages/pydantic/main.py:253, in BaseModel.__init__(self, **data)
    251 # `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks
    252 __tracebackhide__ = True
--> 253 validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
    254 if self is not validated_self:
    255     warnings.warn(
    256         'A custom validator is returning a value other than `self`.\n'
    257         "Returning anything other than `self` from a top level model validator isn't supported when validating via `__init__`.\n"
    258         'See the `model_validator` docs (https://docs.pydantic.dev/latest/concepts/validators/#model-validators) for more details.',
    259         stacklevel=2,
    260     )

ValidationError: 1 validation error for ImportationPressureParams
importation_end
  Value error, importation_end must be greater than importation_start [type=value_error, input_value=5, input_type=int]
    For further information visit https://errors.pydantic.dev/2.11/v/value_error

Parameter inheritance and customization#

You can easily extend parameter classes for specialized use cases:

[8]:
print("\n=== Parameter Inheritance and Customization ===")


# Extend InfectionParams for a specific study
class SeasonalInfectionParams(InfectionParams):
    """Extended infection parameters with seasonal variations"""

    winter_multiplier: float = Field(default=1.2, description="Winter transmission multiplier", gt=0.0)
    summer_multiplier: float = Field(default=0.8, description="Summer transmission multiplier", gt=0.0)
    humidity_effect: float = Field(default=0.05, description="Humidity effect on transmission", ge=0.0, le=0.5)


# Create extended parameters
seasonal_params = SeasonalInfectionParams(beta=40.0, seasonality=0.15, winter_multiplier=1.5, humidity_effect=0.1)

print("Extended Seasonal Parameters:")
print(f"  Base beta: {seasonal_params.beta}")
print(f"  Seasonality: {seasonal_params.seasonality}")
print(f"  Winter multiplier: {seasonal_params.winter_multiplier}")
print(f"  Summer multiplier: {seasonal_params.summer_multiplier}")
print(f"  Humidity effect: {seasonal_params.humidity_effect}")

# Validation still works for extended class
try:
    invalid_seasonal = SeasonalInfectionParams(humidity_effect=0.8)  # > 0.5
except ValidationError as e:
    print(f"\n✗ Extended validation works: {e.errors()[0]['msg']}")

=== Parameter Inheritance and Customization ===
Extended Seasonal Parameters:
  Base beta: 40.0
  Seasonality: 0.15
  Winter multiplier: 1.5
  Summer multiplier: 0.8
  Humidity effect: 0.1

✗ Extended validation works: Input should be less than or equal to 0.5

Configuration management#

Pydantic makes it easy to manage multiple parameter sets for different scenarios:

[9]:
print("\n=== Configuration Management ===")

# Define parameter sets for different scenarios
scenarios = {
    "baseline": {"infection": InfectionParams(), "importation": ImportationPressureParams(), "surveillance": CaseSurveillanceParams()},
    "high_transmission": {
        "infection": InfectionParams(beta=60.0, seasonality=0.2),
        "importation": ImportationPressureParams(crude_importation_rate=3.0),
        "surveillance": CaseSurveillanceParams(detection_rate=0.2),
    },
    "low_surveillance": {
        "infection": InfectionParams(beta=25.0),
        "importation": ImportationPressureParams(crude_importation_rate=0.5),
        "surveillance": CaseSurveillanceParams(detection_rate=0.05),
    },
}

# Display scenario configurations
for scenario_name, params in scenarios.items():
    print(f"\n{scenario_name.upper()} Scenario:")
    print(f"  Transmission rate: {params['infection'].beta}")
    print(f"  Importation rate: {params['importation'].crude_importation_rate}/1k/year")
    print(f"  Detection rate: {params['surveillance'].detection_rate * 100}%")


# Easy parameter comparison
def compare_scenarios(scenario1, scenario2, param_type):
    """Compare parameters between scenarios"""
    params1 = scenarios[scenario1][param_type]
    params2 = scenarios[scenario2][param_type]

    print(f"\nComparing {param_type} parameters: {scenario1} vs {scenario2}")
    for field_name in params1.__class__.model_fields:
        val1 = getattr(params1, field_name)
        val2 = getattr(params2, field_name)
        if val1 != val2:
            print(f"  {field_name}: {val1}{val2}")


compare_scenarios("baseline", "high_transmission", "infection")

=== Configuration Management ===

BASELINE Scenario:
  Transmission rate: 0.5714285714285714
  Importation rate: 1.0/1k/year
  Detection rate: 10.0%

HIGH_TRANSMISSION Scenario:
  Transmission rate: 60.0
  Importation rate: 3.0/1k/year
  Detection rate: 20.0%

LOW_SURVEILLANCE Scenario:
  Transmission rate: 25.0
  Importation rate: 0.5/1k/year
  Detection rate: 5.0%

Comparing infection parameters: baseline vs high_transmission
  beta: 0.5714285714285714 → 60.0
  seasonality: 0.0 → 0.2
  mixer: <laser_measles.mixing.gravity.GravityMixing object at 0x721cf79caf60> → <laser_measles.mixing.gravity.GravityMixing object at 0x721cf79cb740>

IDE support and type hints#

Pydantic provides excellent IDE support with autocomplete and type checking:

[10]:

print("\n=== IDE Support and Type Hints ===") # Demonstrate type hints and IDE support def create_infection_component_params(transmission_rate: float, seasonal_variation: float) -> InfectionParams: """Create infection parameters with type hints for better IDE support""" return InfectionParams(beta=transmission_rate, seasonality=seasonal_variation, season_start=0) # Function with proper type annotations def validate_parameter_ranges(params: InfectionParams) -> bool: """Validate parameter ranges with type checking""" # IDE will provide autocomplete for params.beta, params.seasonality, etc. return 0 < params.beta < 100 and 0 <= params.seasonality <= 1 and 0 <= params.season_start <= 25 # Test the functions test_params = create_infection_component_params(35.0, 0.08) is_valid = validate_parameter_ranges(test_params) print(f"Created parameters are valid: {is_valid}") print(f" Beta: {test_params.beta}") print(f" Seasonality: {test_params.seasonality}")

=== IDE Support and Type Hints ===
Created parameters are valid: True
  Beta: 35.0
  Seasonality: 0.08