Skip to content
5 changes: 5 additions & 0 deletions examples/pydantic/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# © 2026 The Radiativity Company
# Licensed under the Apache License, Version 2.0
# See the LICENSE file for details.

"""Pydantic integration examples for ucon."""
133 changes: 133 additions & 0 deletions examples/pydantic/pharma/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Pharmaceutical Compounding Example

An advanced example demonstrating sophisticated Pydantic patterns with `Number[Dimension]` for safety-critical pharmaceutical calculations.

## Overview

This example shows:

1. **Composite dimension validation** — `mg/kg/day` dose rates
2. **Custom validators** — Positivity constraints, range checking
3. **Cross-field validation** — Ensuring `min < max` for temperature ranges
4. **Uncertainty propagation** — Drug concentration uncertainty flows through calculations
5. **Computed properties** — Derived values like total infusion volume

## Files

- `config.yaml` — Drug, patient, and dosing parameters
- `models.py` — Advanced Pydantic models with custom validation
- `main.py` — Example usage with error demonstrations

## Usage

```bash
# Install dependencies
pip install pyyaml

# Run with default config
python main.py

# Run with custom config
python main.py /path/to/custom/config.yaml
```

## Example Output

```
=== Drug Information ===
Name: Amoxicillin
Concentration: <250 ± 5 mg/mL>
Stability: <14 day>
Storage temp: <2 degC> to <8 degC>

=== Calculated Values ===
Daily dose: <1750 ± 35 mg>
Single dose: <500 ± 10 mg> # Clamped to max
Volume/dose: <2.0 ± 0.05 mL>
```

## Key Patterns

### Composite Dimension Validation

```python
@field_validator('target_dose')
@classmethod
def validate_dose_rate_dimension(cls, v: Number) -> Number:
expected = Dimension.mass / Dimension.mass / Dimension.time
if v.dimension != expected:
raise ValueError(f"expected {expected.name}, got {v.dimension.name}")
return v
```

### Custom Validators with Annotated

```python
def must_be_positive(n: Number) -> Number:
if n.quantity <= 0:
raise ValueError(f"must be positive, got {n.quantity}")
return n

PositiveNumber = Annotated[PydanticNumber, AfterValidator(must_be_positive)]
```

### Cross-Field Validation

```python
class TemperatureRange(BaseModel):
min: PydanticNumber[Dimension.temperature]
max: PydanticNumber[Dimension.temperature]

@model_validator(mode='after')
def min_less_than_max(self) -> 'TemperatureRange':
min_k = self.min.to(units.kelvin).quantity
max_k = self.max.to(units.kelvin).quantity
if min_k >= max_k:
raise ValueError("min must be less than max")
return self
```

### Uncertainty Propagation

```yaml
# config.yaml
concentration: { quantity: 250, unit: "mg/mL", uncertainty: 5 }
```

```python
# Uncertainty flows through calculations automatically
volume = settings.calculate_volume_per_dose()
print(volume) # <2.0 ± 0.05 mL>
```

### Computed Methods with Bounds Clamping

```python
def calculate_single_dose(self) -> Number:
daily_dose = self.calculate_daily_dose()
single_dose = daily_dose / self.dosing.frequency

# Clamp to configured bounds
clamped = max(min_mg, min(max_mg, dose_mg))
return mg(clamped, uncertainty=single_dose.uncertainty)
```

## Validation Examples

The example demonstrates catching various errors:

```python
# Wrong dimension
concentration: { quantity: 250, unit: "mg" } # Missing /mL
# → ValidationError: concentration must have dimension 'density'

# Range violation
storage_temp:
min: { quantity: 10, unit: "degC" }
max: { quantity: 5, unit: "degC" }
# → ValidationError: min must be less than max

# Negative value
weight: { quantity: -70, unit: "kg" }
# → ValidationError: must be positive
```
5 changes: 5 additions & 0 deletions examples/pydantic/pharma/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# © 2026 The Radiativity Company
# Licensed under the Apache License, Version 2.0
# See the LICENSE file for details.

"""Pharmaceutical compounding example demonstrating advanced Pydantic patterns."""
43 changes: 43 additions & 0 deletions examples/pydantic/pharma/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
drug:
name: "Amoxicillin"
concentration:
quantity: 250
unit: "mg/mL"
uncertainty: 5
stability_duration:
quantity: 14
unit: "day"
storage_temp:
min:
quantity: 2
unit: "degC"
max:
quantity: 8
unit: "degC"

patient:
weight:
quantity: 70
unit: "kg"
uncertainty: 0.5
age_category: "adult"

dosing:
target_dose:
quantity: 25
unit: "mg/kg/day"
frequency: 3
max_single_dose:
quantity: 500
unit: "mg"
min_single_dose:
quantity: 100
unit: "mg"

infusion:
rate:
quantity: 100
unit: "mL/h"
duration:
quantity: 0.5
unit: "h"
177 changes: 177 additions & 0 deletions examples/pydantic/pharma/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#!/usr/bin/env python
# © 2026 The Radiativity Company
# Licensed under the Apache License, Version 2.0
# See the LICENSE file for details.

"""
Pharmaceutical compounding example.

Demonstrates:
- Complex Pydantic models with Number[Dimension] validation
- Custom dimension validation (mg/kg/day)
- Cross-field validation (min < max)
- Uncertainty propagation through calculations
- Computed properties and methods

Usage:
python main.py
python main.py path/to/custom/config.yaml
"""

import sys
from pathlib import Path

import yaml

from ucon import Scale, units

from models import PharmacySettings

# Define milliliter for convenience
milliliter = Scale.milli * units.liter


def load_settings(config_path: Path) -> PharmacySettings:
"""Load and validate configuration from YAML."""
with open(config_path) as f:
data = yaml.safe_load(f)
return PharmacySettings(**data)


def main(config_path: Path) -> None:
print(f"Loading configuration from {config_path}\n")
settings = load_settings(config_path)

print("=== Drug Information ===")
print(f" Name: {settings.drug.name}")
print(f" Concentration: {settings.drug.concentration}")
print(f" Stability: {settings.drug.stability_duration}")
print(f" Storage temp: {settings.drug.storage_temp.min} to "
f"{settings.drug.storage_temp.max}")
print()

print("=== Patient Information ===")
print(f" Weight: {settings.patient.weight}")
print(f" Age category: {settings.patient.age_category}")
print()

print("=== Dosing Parameters ===")
print(f" Target dose: {settings.dosing.target_dose}")
print(f" Frequency: {settings.dosing.frequency} doses/day")
print(f" Dose range: {settings.dosing.min_single_dose} to "
f"{settings.dosing.max_single_dose}")
print()

# Calculate doses (uncertainty propagates automatically)
daily_dose = settings.calculate_daily_dose()
single_dose = settings.calculate_single_dose()
volume = settings.calculate_volume_per_dose()

print("=== Calculated Values ===")
print(f" Daily dose: {daily_dose}")
print(f" Single dose: {single_dose}")
print(f" Volume/dose: {volume}")
print(f" {volume.to(milliliter)}")
print()

# Calculate doses per vial
vial_volume = milliliter(100)
doses_per_vial = settings.calculate_doses_per_vial(vial_volume)
print(f" Doses per {vial_volume} vial: {doses_per_vial}")
print()

# Infusion calculations (if configured)
if settings.infusion:
print("=== Infusion ===")
print(f" Rate: {settings.infusion.rate}")
print(f" Duration: {settings.infusion.duration}")
print(f" Total volume: {settings.infusion.total_volume}")
print()

# Demonstrate validation errors
print("=== Validation Examples ===")
demonstrate_validation_errors()


def demonstrate_validation_errors() -> None:
"""Show how various validation errors are caught."""
import yaml
from pydantic import ValidationError

# Wrong dimension for concentration
bad_config_1 = """
drug:
name: "Test"
concentration: { quantity: 250, unit: "mg" }
stability_duration: { quantity: 14, unit: "day" }
storage_temp:
min: { quantity: 2, unit: "degC" }
max: { quantity: 8, unit: "degC" }
patient:
weight: { quantity: 70, unit: "kg" }
age_category: "adult"
dosing:
target_dose: { quantity: 25, unit: "mg/kg/day" }
frequency: 3
max_single_dose: { quantity: 500, unit: "mg" }
min_single_dose: { quantity: 100, unit: "mg" }
"""
try:
PharmacySettings(**yaml.safe_load(bad_config_1))
except ValidationError as e:
print(f" Wrong dimension: {e.errors()[0]['msg'][:60]}...")

# Temperature range violation
bad_config_2 = """
drug:
name: "Test"
concentration: { quantity: 250, unit: "mg/mL" }
stability_duration: { quantity: 14, unit: "day" }
storage_temp:
min: { quantity: 10, unit: "degC" }
max: { quantity: 5, unit: "degC" }
patient:
weight: { quantity: 70, unit: "kg" }
age_category: "adult"
dosing:
target_dose: { quantity: 25, unit: "mg/kg/day" }
frequency: 3
max_single_dose: { quantity: 500, unit: "mg" }
min_single_dose: { quantity: 100, unit: "mg" }
"""
try:
PharmacySettings(**yaml.safe_load(bad_config_2))
except ValidationError as e:
print(f" Range violation: {e.errors()[0]['msg'][:60]}...")

# Negative value
bad_config_3 = """
drug:
name: "Test"
concentration: { quantity: 250, unit: "mg/mL" }
stability_duration: { quantity: 14, unit: "day" }
storage_temp:
min: { quantity: 2, unit: "degC" }
max: { quantity: 8, unit: "degC" }
patient:
weight: { quantity: -70, unit: "kg" }
age_category: "adult"
dosing:
target_dose: { quantity: 25, unit: "mg/kg/day" }
frequency: 3
max_single_dose: { quantity: 500, unit: "mg" }
min_single_dose: { quantity: 100, unit: "mg" }
"""
try:
PharmacySettings(**yaml.safe_load(bad_config_3))
except ValidationError as e:
print(f" Negative value: {e.errors()[0]['msg'][:60]}...")


if __name__ == "__main__":
if len(sys.argv) > 1:
config_file = Path(sys.argv[1])
else:
config_file = Path(__file__).parent / "config.yaml"

main(config_file)
Loading