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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
// SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0
//! FACTS (Flexible AC Transmission System) device data structures.
//!
//! Covers SVC, STATCOM (shunt-only), TCSC (series-only), and UPFC
//! (series + shunt) devices. PSS/E RAW section: "FACTS DEVICE DATA".
use serde::{Deserialize, Serialize};
/// Operating mode of a FACTS device (PSS/E MODE field).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum FactsMode {
/// Device is out of service.
#[default]
OutOfService = 0,
/// Series element only (TCSC in impedance/power mode).
SeriesOnly = 1,
/// Shunt element only (SVC or STATCOM at bus_from).
ShuntOnly = 2,
/// Shunt and series combined (UPFC).
ShuntSeries = 3,
/// Series element with active power control.
SeriesPowerControl = 4,
/// Direct impedance modulation.
ImpedanceModulation = 5,
}
impl FactsMode {
/// Convert a PSS/E MODE integer to a `FactsMode`. Out-of-range values map to `OutOfService`.
pub fn from_u32(v: u32) -> Self {
match v {
1 => Self::SeriesOnly,
2 => Self::ShuntOnly,
3 => Self::ShuntSeries,
4 => Self::SeriesPowerControl,
5 => Self::ImpedanceModulation,
_ => Self::OutOfService,
}
}
/// Returns `true` if this mode includes a shunt element at `bus_from`.
pub fn has_shunt(&self) -> bool {
matches!(
self,
Self::ShuntOnly | Self::ShuntSeries | Self::SeriesPowerControl
)
}
/// Returns `true` if this mode includes a series element between `bus_from` and `bus_to`.
pub fn has_series(&self) -> bool {
matches!(
self,
Self::SeriesOnly
| Self::ShuntSeries
| Self::SeriesPowerControl
| Self::ImpedanceModulation
)
}
/// Returns `true` if the device is in service.
pub fn in_service(&self) -> bool {
!matches!(self, Self::OutOfService)
}
}
/// FACTS device type classification.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum FactsType {
/// Static VAR Compensator (thyristor-switched capacitor/reactor).
#[default]
Svc,
/// Static Synchronous Compensator (VSC-based shunt).
Statcom,
/// Thyristor-Controlled Series Capacitor.
Tcsc,
/// Static Synchronous Series Compensator.
Sssc,
/// Unified Power Flow Controller (shunt + series VSC).
Upfc,
/// Unclassified FACTS device.
Other,
}
/// A FACTS control device (SVC, STATCOM, TCSC, UPFC).
///
/// The device is electrically represented differently depending on `mode`:
///
/// | Mode | Device | AC effect |
/// |------|--------|-----------|
/// | 0 | Out of service | No effect |
/// | 1 | TCSC (series) | Subtract `linx` from the branch reactance between `bus_from` and `bus_to` |
/// | 2 | SVC / STATCOM | Add a PV generator at `bus_from` with Q range `[−q_max, q_max]` |
/// | 3 | UPFC | Both series reactance mod and shunt reactive compensation |
/// | 4 | Series power control | Same as mode 1 with active power targeting (soft constraint) |
/// | 5 | Impedance modulation | Direct `branch.x` modification by `linx` |
///
/// The `expand_facts` function in `surge-ac` converts these records into
/// concrete Generator additions and Branch modifications before the NR solve.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FactsDevice {
/// Device name (NAME).
pub name: String,
/// Shunt connection bus number (I). SVCs/STATCOMs connect here.
#[serde(alias = "bus_i")]
pub bus_from: u32,
/// Remote/series bus number (J). Zero for shunt-only devices.
#[serde(alias = "bus_j")]
pub bus_to: u32,
/// Operating mode.
pub mode: FactsMode,
/// Desired active power flow through series element in MW (PDES).
pub p_setpoint_mw: f64,
/// Desired reactive power from shunt element in MVAr (QDES).
pub q_setpoint_mvar: f64,
/// Voltage setpoint in per-unit at `bus_from` (VSET). Used when `mode` includes a shunt.
pub voltage_setpoint_pu: f64,
/// Maximum shunt reactive injection in MVAr (SHMX). Minimum is `−q_max`.
pub q_max: f64,
/// Series reactance in per-unit on system base (LINX). Applied to the branch between
/// `bus_from` and `bus_to`. Negative values reduce impedance (TCSC boost).
pub series_reactance_pu: f64,
/// Device is in service (derived from `mode ≠ 0`).
pub in_service: bool,
// --- expanded fields ---
/// FACTS device type classification.
#[serde(default)]
pub facts_type: FactsType,
/// Rated apparent power (MVA).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub s_rated_mva: Option<f64>,
/// Minimum reactive power (MVAr). Allows asymmetric range.
#[serde(default)]
pub q_min: f64,
/// Voltage droop (pu V / pu Q). 0 = flat (STATCOM).
#[serde(default)]
pub v_droop: f64,
/// Max current (pu on s_rated). STATCOM low-V behavior.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub i_max_pu: Option<f64>,
/// Short-term overload (e.g. 1.2 = 120%).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub overload_pct: Option<f64>,
/// Overload duration (seconds).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub overload_duration_s: Option<f64>,
/// No-load loss (MW).
#[serde(default)]
pub loss_a_mw: f64,
/// Proportional loss coefficient (per-unit).
#[serde(default, alias = "loss_b")]
pub loss_b_pu: f64,
/// SVC: number of TSC banks.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tsc_steps: Option<u32>,
/// SVC: MVAr per TSC step.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tsc_mvar_per_step: Option<f64>,
/// TCSC/SSSC: min series reactance (pu, capacitive limit).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub x_min: Option<f64>,
/// TCSC/SSSC: max series reactance (pu, inductive limit).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub x_max: Option<f64>,
}
impl Default for FactsDevice {
fn default() -> Self {
Self {
name: String::new(),
bus_from: 0,
bus_to: 0,
mode: FactsMode::OutOfService,
p_setpoint_mw: 0.0,
q_setpoint_mvar: 0.0,
voltage_setpoint_pu: 1.0,
q_max: 9999.0,
series_reactance_pu: 0.0,
in_service: false,
facts_type: FactsType::Svc,
s_rated_mva: None,
q_min: 0.0,
v_droop: 0.0,
i_max_pu: None,
overload_pct: None,
overload_duration_s: None,
loss_a_mw: 0.0,
loss_b_pu: 0.0,
tsc_steps: None,
tsc_mvar_per_step: None,
x_min: None,
x_max: None,
}
}
}