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