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}