Unit Operations¶
This guide shows practical techniques for working with units in power system components.
… create a component with per-unit fields¶
from typing import Annotated
from infrasys import Component
from r2x_core.units import HasPerUnit, Unit
class Generator(HasPerUnit, Component):
"""Generator with unit-aware fields."""
base_power: Annotated[float, Unit("MVA")]
rated_voltage: Annotated[float, Unit("kV")]
active_power: Annotated[float, Unit("pu", base="base_power")]
terminal_voltage: Annotated[float, Unit("pu", base="rated_voltage")]
gen = Generator(
name="Gen1",
base_power=100.0,
rated_voltage=13.8,
active_power=0.95,
terminal_voltage=1.05
)
… input values in natural units¶
# Instead of calculating per-unit manually, provide natural units
gen = Generator(
name="Gen2",
base_power=250.0,
rated_voltage=22.0,
active_power={"value": 200.0, "unit": "MVA"},
terminal_voltage={"value": 22.5, "unit": "kV"}
)
# Values are automatically converted to per-unit
print(gen.active_power) # 0.8 pu
print(gen.terminal_voltage) # 1.023 pu (approximately)
… switch between display modes¶
from r2x_core.units import UnitSystem, set_unit_system
gen = Generator(
name="Gen3",
base_power=150.0,
rated_voltage=15.0,
active_power=0.90,
terminal_voltage=1.02
)
# Device-base per-unit (default)
set_unit_system(UnitSystem.DEVICE_BASE)
print(gen.active_power) # 0.9 pu
# Natural units
set_unit_system(UnitSystem.NATURAL_UNITS)
print(gen.active_power) # 135 MVA
# System-base (requires system)
from r2x_core.system import System
system = System(100.0, name="Grid")
system.add_component(gen)
set_unit_system(UnitSystem.SYSTEM_BASE)
print(gen.active_power) # 1.35 pu (system)
… use context manager for temporary display mode¶
from r2x_core.units import unit_system, UnitSystem
gen = Generator(
name="Gen4",
base_power=200.0,
rated_voltage=18.0,
active_power=0.75,
terminal_voltage=1.0
)
# Default mode
print(f"Default: {gen.active_power}") # 0.75 pu
# Temporarily change mode
with unit_system(UnitSystem.NATURAL_UNITS):
print(f"In context: {gen.active_power}") # 150 MVA
# Reverts to original mode
print(f"After context: {gen.active_power}") # 0.75 pu
… create components with multiple base references¶
class Transformer(HasPerUnit, Component):
"""Transformer with multiple voltage bases."""
base_power: Annotated[float, Unit("MVA")]
high_voltage: Annotated[float, Unit("kV")]
low_voltage: Annotated[float, Unit("kV")]
impedance: Annotated[float, Unit("pu", base="base_power")]
hv_tap: Annotated[float, Unit("pu", base="high_voltage")]
lv_current: Annotated[float, Unit("pu", base="low_voltage")]
tx = Transformer(
name="TX1",
base_power=100.0,
high_voltage=138.0,
low_voltage=13.8,
impedance=0.10,
hv_tap=1.05,
lv_current={"value": 4.18, "unit": "kA"}
)
… work with components without system base¶
from r2x_core.units import HasUnits
class Bus(HasUnits, Component):
"""Bus with units but no system-base tracking."""
voltage: Annotated[float, Unit("kV")]
angle: Annotated[float, Unit("deg")]
load: Annotated[float, Unit("MW")]
bus = Bus(
name="Bus1",
voltage=138.0,
angle=5.2,
load={"value": 50.0, "unit": "MW"}
)
# HasUnits provides unit annotations but not system-base tracking
# Useful for non-electrical quantities or fixed-unit fields
… serialize and deserialize unit-aware components¶
# Serialization preserves internal per-unit values
data = gen.model_dump()
print(data) # {'name': 'Gen3', 'active_power': 0.9, ...}
# Deserialize from dict
gen_copy = Generator.model_validate(data)
# JSON serialization
json_str = gen.model_dump_json()
# Deserialize from JSON
gen_from_json = Generator.model_validate_json(json_str)
… add unit-aware components to a system¶
from r2x_core.system import System
# Create system with 200 MVA base
system = System(200.0, name="TransmissionSystem")
# Create generators
gen1 = Generator(
name="Plant1",
base_power=500.0,
rated_voltage=22.0,
active_power=0.85,
terminal_voltage=1.03
)
gen2 = Generator(
name="Plant2",
base_power=300.0,
rated_voltage=18.0,
active_power=0.90,
terminal_voltage=1.01
)
# Add components - system base is automatically set
system.add_components(gen1, gen2)
# View in system-base mode
set_unit_system(UnitSystem.SYSTEM_BASE)
for gen in system.get_components(Generator):
print(f"{gen.name}: {gen.active_power}")
# Plant1: 2.125 pu (system) # 425 MVA / 200 MVA base
# Plant2: 1.35 pu (system) # 270 MVA / 200 MVA base
… create systems with different base powers¶
# System with 100 MVA base (common for distribution)
dist_system = System(100.0, name="Distribution")
# System with 100 MVA base using keyword
sub_system = System(system_base_power=500.0, name="Subtransmission")
# System with position and name
trans_system = System(1000.0, name="Transmission")
# Default 100 MVA base
default_system = System(name="DefaultBase")
… handle unit conversion errors gracefully¶
from pydantic import ValidationError
try:
gen = Generator(
name="BadGen",
base_power=100.0,
rated_voltage=13.8,
active_power={"value": 150.0, "unit": "InvalidUnit"}
)
except ValidationError as e:
print(f"Validation error: {e}")
# Handle invalid unit specification
… mix unit-aware and regular fields¶
class HybridComponent(HasPerUnit, Component):
"""Component with both unit-aware and regular fields."""
# Unit-aware fields
base_power: Annotated[float, Unit("MVA")]
output: Annotated[float, Unit("pu", base="base_power")]
# Regular fields without units
efficiency: float # Dimensionless ratio
fuel_type: str # String identifier
commissioning_year: int
hybrid = HybridComponent(
name="SolarFarm",
base_power=50.0,
output=0.65,
efficiency=0.18,
fuel_type="solar",
commissioning_year=2024
)
… validate unit compatibility¶
# R2X Core validates unit conversions automatically
try:
gen = Generator(
name="Gen5",
base_power=100.0,
rated_voltage=13.8,
# Attempting to use length units for power
active_power={"value": 100.0, "unit": "meter"}
)
except ValidationError:
print("Unit type mismatch detected")
… access raw per-unit values¶
gen = Generator(
name="Gen6",
base_power=100.0,
rated_voltage=13.8,
active_power={"value": 75.0, "unit": "MVA"},
terminal_voltage=1.02
)
# Internal storage is always in per-unit
print(gen.active_power) # 0.75 (regardless of display mode)
# Model dump shows internal values
data = gen.model_dump()
print(data['active_power']) # 0.75