Skip to main content

surge_network/network/
op_limits.rs

1// SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0
2//! IEC 61970-302 Operational Limits — CIM-aligned limit model.
3//!
4//! This module provides the full operational-limits hierarchy from the CIM
5//! OperationalLimits package. It complements the existing `Branch.rating_a_mva/b/c`
6//! and `Bus.voltage_min_pu/vmax` fields (which remain the primary inputs for solvers)
7//! by preserving the complete limit metadata: duration categories (PATL/TATL/IATL),
8//! direction, limit kinds (MW/MVA/A/kV), and CIM traceability via mRIDs.
9
10use std::collections::HashMap;
11
12use serde::{Deserialize, Serialize};
13
14/// Duration category for operational limits (IEC 61970-302).
15#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
16pub enum LimitDuration {
17    /// Permanent Admissible Transmission Loading (PATL) — normal continuous rating.
18    Permanent,
19    /// Temporary Admissible (TATL) — time-limited overload, duration in seconds.
20    Temporary(f64),
21    /// Instantaneous Admissible (IATL) — very short duration (typically 0 s).
22    Instantaneous,
23}
24
25/// Direction of an operational limit.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub enum LimitDirection {
28    High,
29    Low,
30    AbsoluteValue,
31}
32
33/// A single operational limit value with its classification.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct OperationalLimit {
36    /// Limit value in engineering units (MW, MVA, A, or kV).
37    pub value: f64,
38    /// Duration category.
39    pub duration: LimitDuration,
40    /// Direction.
41    pub direction: LimitDirection,
42    /// CIM `OperationalLimitType` mRID for traceability.
43    pub limit_type_mrid: Option<String>,
44}
45
46/// What physical quantity a limit constrains.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
48pub enum LimitKind {
49    /// Active power (MW).
50    ActivePower,
51    /// Apparent power (MVA).
52    ApparentPower,
53    /// Current (A).
54    Current,
55    /// Voltage (kV).
56    Voltage,
57}
58
59/// Complete set of operational limits for one terminal/equipment.
60#[derive(Debug, Clone, Default, Serialize, Deserialize)]
61pub struct OperationalLimitSet {
62    /// CIM mRID.
63    pub mrid: String,
64    /// Human-readable name.
65    pub name: String,
66    /// Internal bus number where limits apply (0 if unresolved).
67    pub bus: u32,
68    /// Equipment mRID (branch circuit or generator).
69    pub equipment_mrid: Option<String>,
70    /// Whether this is from-end (`true`) or to-end (`false`) of a branch.
71    pub from_end: Option<bool>,
72    /// All limits in this set, grouped by kind.
73    pub limits: Vec<(LimitKind, OperationalLimit)>,
74}
75
76/// Container for all operational limits on the Network.
77#[derive(Debug, Clone, Default, Serialize, Deserialize)]
78pub struct OperationalLimits {
79    /// All limit sets, keyed by CIM mRID.
80    pub limit_sets: HashMap<String, OperationalLimitSet>,
81}
82
83impl OperationalLimits {
84    /// Returns `true` when no limit sets have been populated.
85    pub fn is_empty(&self) -> bool {
86        self.limit_sets.is_empty()
87    }
88
89    /// Total number of individual limit values across all sets.
90    pub fn total_limit_count(&self) -> usize {
91        self.limit_sets.values().map(|s| s.limits.len()).sum()
92    }
93
94    /// Iterate all limit sets attached to a given equipment mRID.
95    pub fn sets_for_equipment<'a>(
96        &'a self,
97        equipment_mrid: &'a str,
98    ) -> impl Iterator<Item = &'a OperationalLimitSet> {
99        self.limit_sets.values().filter(move |s| {
100            s.equipment_mrid
101                .as_deref()
102                .map(|e| e == equipment_mrid)
103                .unwrap_or(false)
104        })
105    }
106
107    /// Iterate all limit sets attached to a given bus number.
108    pub fn sets_for_bus(&self, bus: u32) -> impl Iterator<Item = &OperationalLimitSet> {
109        self.limit_sets.values().filter(move |s| s.bus == bus)
110    }
111}