from __future__ import annotations from typing import Dict, List, Literal, Optional, Union from pydantic import BaseModel, ConfigDict, Field # ------------------------- # HIL blocks (v1.3) # ------------------------- class TankLevelBlock(BaseModel): model_config = ConfigDict(extra="forbid") type: Literal["tank_level"] = "tank_level" level_out: str inlet_cmd: str outlet_cmd: str dt: float = 0.1 area: float = 1.0 max_level: float = 1.0 inflow_rate: float = 0.25 outflow_rate: float = 0.25 leak_rate: float = 0.0 initial_level: Optional[float] = None class BottleLineBlock(BaseModel): """ Minimal bottle + conveyor dynamics (Strada A): - bottle_at_filler_out = 1 when conveyor_cmd <= 0.5 else 0 - bottle_fill_level_out increases when at_filler==1 - bottle_fill_level_out decreases slowly when conveyor ON (new/empty bottle coming) """ model_config = ConfigDict(extra="forbid") type: Literal["bottle_line"] = "bottle_line" conveyor_cmd: str bottle_at_filler_out: str bottle_fill_level_out: str dt: float = 0.1 fill_rate: float = 0.25 # per second drain_rate: float = 0.40 # per second when conveyor ON (reset toward 0) initial_fill: float = 0.0 HILBlock = Union[TankLevelBlock, BottleLineBlock] class IRHIL(BaseModel): model_config = ConfigDict(extra="forbid") name: str logic: str outputs_init: Dict[str, float] = Field(default_factory=dict) blocks: List[HILBlock] = Field(default_factory=list) # ------------------------- # PLC rules (v1.2) # ------------------------- class HysteresisFillRule(BaseModel): model_config = ConfigDict(extra="forbid") type: Literal["hysteresis_fill"] = "hysteresis_fill" level_in: str low: float = 0.2 high: float = 0.8 inlet_out: str outlet_out: str enable_input: Optional[str] = None # Signal range for converting normalized thresholds to absolute values # If signal_max=1000, then low=0.2 becomes 200, high=0.8 becomes 800 signal_max: float = 1.0 # Default 1.0 means thresholds are already absolute class ThresholdOutputRule(BaseModel): model_config = ConfigDict(extra="forbid") type: Literal["threshold_output"] = "threshold_output" input_id: str threshold: float = 0.2 op: Literal["lt"] = "lt" output_id: str true_value: int = 1 false_value: int = 0 # Signal range for converting normalized threshold to absolute value # If signal_max=200, then threshold=0.2 becomes 40 signal_max: float = 1.0 # Default 1.0 means threshold is already absolute PLCRule = Union[HysteresisFillRule, ThresholdOutputRule] class IRPLC(BaseModel): model_config = ConfigDict(extra="forbid") name: str logic: str rules: List[PLCRule] = Field(default_factory=list) class IRSpec(BaseModel): model_config = ConfigDict(extra="forbid") version: Literal["ir_v1"] = "ir_v1" plcs: List[IRPLC] = Field(default_factory=list) hils: List[IRHIL] = Field(default_factory=list)