Skip to main content

surge_network/network/
hvdc.rs

1// SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0
2//! Canonical HVDC domain model.
3//!
4//! This module is the only public HVDC surface for [`crate::network::Network`].
5//! Source-format specific records stay out of the canonical network model.
6
7use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10
11pub use crate::network::dc_line::{LccConverterTerminal, LccHvdcControlMode, LccHvdcLink};
12pub use crate::network::dc_network_types::{DcBranch, DcBus, DcConverterStation};
13pub use crate::network::vsc_dc_line::{
14    VscConverterAcControlMode, VscConverterTerminal, VscHvdcControlMode, VscHvdcLink,
15};
16
17/// A point-to-point HVDC link.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(tag = "technology", rename_all = "snake_case")]
20pub enum HvdcLink {
21    Lcc(LccHvdcLink),
22    Vsc(VscHvdcLink),
23}
24
25impl HvdcLink {
26    /// Stable user-facing name for the link.
27    pub fn name(&self) -> &str {
28        match self {
29            Self::Lcc(link) => &link.name,
30            Self::Vsc(link) => &link.name,
31        }
32    }
33
34    /// Whether the link is blocked / out of service.
35    pub fn is_blocked(&self) -> bool {
36        match self {
37            Self::Lcc(link) => link.mode == LccHvdcControlMode::Blocked,
38            Self::Vsc(link) => link.mode == VscHvdcControlMode::Blocked,
39        }
40    }
41
42    pub fn as_lcc(&self) -> Option<&LccHvdcLink> {
43        match self {
44            Self::Lcc(link) => Some(link),
45            Self::Vsc(_) => None,
46        }
47    }
48
49    pub fn as_lcc_mut(&mut self) -> Option<&mut LccHvdcLink> {
50        match self {
51            Self::Lcc(link) => Some(link),
52            Self::Vsc(_) => None,
53        }
54    }
55
56    pub fn as_vsc(&self) -> Option<&VscHvdcLink> {
57        match self {
58            Self::Lcc(_) => None,
59            Self::Vsc(link) => Some(link),
60        }
61    }
62
63    pub fn as_vsc_mut(&mut self) -> Option<&mut VscHvdcLink> {
64        match self {
65            Self::Lcc(_) => None,
66            Self::Vsc(link) => Some(link),
67        }
68    }
69}
70
71/// Converter role within an LCC DC grid.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
73pub enum LccDcConverterRole {
74    #[default]
75    Rectifier,
76    Inverter,
77}
78
79/// Canonical LCC converter record for explicit DC-grid topology.
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct LccDcConverter {
82    /// Stable converter identifier within the enclosing DC grid.
83    #[serde(default, skip_serializing_if = "String::is_empty")]
84    pub id: String,
85    /// DC bus number this converter connects to.
86    pub dc_bus: u32,
87    /// AC bus number this converter connects to.
88    pub ac_bus: u32,
89    /// Number of 6-pulse bridges.
90    #[serde(alias = "num_bridges")]
91    pub n_bridges: u32,
92    /// Maximum firing / extinction angle in degrees.
93    pub alpha_max_deg: f64,
94    /// Minimum firing / extinction angle in degrees.
95    pub alpha_min_deg: f64,
96    /// Minimum extinction angle in inverter mode.
97    pub gamma_min_deg: f64,
98    /// Commutating resistance per bridge in ohms.
99    pub commutation_resistance_ohm: f64,
100    /// Commutating reactance per bridge in ohms.
101    pub commutation_reactance_ohm: f64,
102    /// Converter transformer rated AC voltage on the network side.
103    pub base_voltage_kv: f64,
104    /// Transformer turns ratio.
105    pub turns_ratio: f64,
106    /// Off-nominal tap ratio.
107    pub tap_ratio: f64,
108    /// Maximum tap ratio.
109    pub tap_max: f64,
110    /// Minimum tap ratio.
111    pub tap_min: f64,
112    /// Tap step size.
113    pub tap_step: f64,
114    /// Scheduled power (MW) or current (kA) setpoint.
115    pub scheduled_setpoint: f64,
116    /// Share of total DC power assigned to this converter.
117    pub power_share_percent: f64,
118    /// Current margin percentage.
119    pub current_margin_percent: f64,
120    /// Rectifier or inverter role.
121    pub role: LccDcConverterRole,
122    /// Converter in-service flag.
123    pub in_service: bool,
124}
125
126impl Default for LccDcConverter {
127    fn default() -> Self {
128        Self {
129            id: String::new(),
130            dc_bus: 0,
131            ac_bus: 0,
132            n_bridges: 1,
133            alpha_max_deg: 90.0,
134            alpha_min_deg: 5.0,
135            gamma_min_deg: 15.0,
136            commutation_resistance_ohm: 0.0,
137            commutation_reactance_ohm: 0.0,
138            base_voltage_kv: 0.0,
139            turns_ratio: 1.0,
140            tap_ratio: 1.0,
141            tap_max: 1.1,
142            tap_min: 0.9,
143            tap_step: 0.00625,
144            scheduled_setpoint: 0.0,
145            power_share_percent: 0.0,
146            current_margin_percent: 0.0,
147            role: LccDcConverterRole::Rectifier,
148            in_service: true,
149        }
150    }
151}
152
153/// Canonical DC-grid converter.
154#[derive(Debug, Clone, Serialize, Deserialize)]
155#[serde(tag = "technology", rename_all = "snake_case")]
156pub enum DcConverter {
157    Lcc(LccDcConverter),
158    Vsc(DcConverterStation),
159}
160
161impl From<LccDcConverter> for DcConverter {
162    fn from(value: LccDcConverter) -> Self {
163        Self::Lcc(value)
164    }
165}
166
167impl From<DcConverterStation> for DcConverter {
168    fn from(value: DcConverterStation) -> Self {
169        Self::Vsc(value)
170    }
171}
172
173impl DcConverter {
174    pub fn id(&self) -> &str {
175        match self {
176            Self::Lcc(converter) => &converter.id,
177            Self::Vsc(converter) => &converter.id,
178        }
179    }
180
181    pub fn id_mut(&mut self) -> &mut String {
182        match self {
183            Self::Lcc(converter) => &mut converter.id,
184            Self::Vsc(converter) => &mut converter.id,
185        }
186    }
187
188    pub fn ac_bus(&self) -> u32 {
189        match self {
190            Self::Lcc(converter) => converter.ac_bus,
191            Self::Vsc(converter) => converter.ac_bus,
192        }
193    }
194
195    pub fn dc_bus(&self) -> u32 {
196        match self {
197            Self::Lcc(converter) => converter.dc_bus,
198            Self::Vsc(converter) => converter.dc_bus,
199        }
200    }
201
202    pub fn is_in_service(&self) -> bool {
203        match self {
204            Self::Lcc(converter) => converter.in_service,
205            Self::Vsc(converter) => converter.status,
206        }
207    }
208
209    pub fn is_lcc(&self) -> bool {
210        matches!(self, Self::Lcc(_))
211    }
212
213    pub fn as_lcc(&self) -> Option<&LccDcConverter> {
214        match self {
215            Self::Lcc(converter) => Some(converter),
216            Self::Vsc(_) => None,
217        }
218    }
219
220    pub fn as_lcc_mut(&mut self) -> Option<&mut LccDcConverter> {
221        match self {
222            Self::Lcc(converter) => Some(converter),
223            Self::Vsc(_) => None,
224        }
225    }
226
227    pub fn as_vsc(&self) -> Option<&DcConverterStation> {
228        match self {
229            Self::Lcc(_) => None,
230            Self::Vsc(converter) => Some(converter),
231        }
232    }
233
234    pub fn as_vsc_mut(&mut self) -> Option<&mut DcConverterStation> {
235        match self {
236            Self::Lcc(_) => None,
237            Self::Vsc(converter) => Some(converter),
238        }
239    }
240
241    pub fn ac_bus_mut(&mut self) -> &mut u32 {
242        match self {
243            Self::Lcc(converter) => &mut converter.ac_bus,
244            Self::Vsc(converter) => &mut converter.ac_bus,
245        }
246    }
247
248    pub fn dc_bus_mut(&mut self) -> &mut u32 {
249        match self {
250            Self::Lcc(converter) => &mut converter.dc_bus,
251            Self::Vsc(converter) => &mut converter.dc_bus,
252        }
253    }
254}
255
256/// Explicit DC-grid topology.
257#[derive(Debug, Clone, Default, Serialize, Deserialize)]
258pub struct DcGrid {
259    /// Stable grid identifier within the network.
260    pub id: u32,
261    /// Optional user-facing name when the source format provides one.
262    #[serde(default, skip_serializing_if = "Option::is_none")]
263    pub name: Option<String>,
264    /// DC buses in this grid.
265    #[serde(default, skip_serializing_if = "Vec::is_empty")]
266    pub buses: Vec<DcBus>,
267    /// DC-grid converters in this grid.
268    #[serde(default, skip_serializing_if = "Vec::is_empty")]
269    pub converters: Vec<DcConverter>,
270    /// DC branches in this grid.
271    #[serde(default, skip_serializing_if = "Vec::is_empty")]
272    pub branches: Vec<DcBranch>,
273}
274
275impl DcGrid {
276    pub fn new(id: u32, name: Option<String>) -> Self {
277        Self {
278            id,
279            name,
280            buses: Vec::new(),
281            converters: Vec::new(),
282            branches: Vec::new(),
283        }
284    }
285
286    pub fn is_empty(&self) -> bool {
287        self.buses.is_empty() && self.converters.is_empty() && self.branches.is_empty()
288    }
289
290    pub fn find_bus(&self, bus_id: u32) -> Option<&DcBus> {
291        self.buses.iter().find(|bus| bus.bus_id == bus_id)
292    }
293
294    pub fn find_bus_mut(&mut self, bus_id: u32) -> Option<&mut DcBus> {
295        self.buses.iter_mut().find(|bus| bus.bus_id == bus_id)
296    }
297
298    pub fn bus_index_map(&self) -> HashMap<u32, usize> {
299        self.buses
300            .iter()
301            .enumerate()
302            .map(|(index, bus)| (bus.bus_id, index))
303            .collect()
304    }
305
306    pub fn canonicalize_converter_ids(&mut self) {
307        for (index, converter) in self.converters.iter_mut().enumerate() {
308            let trimmed = converter.id().trim().to_string();
309            if trimmed.is_empty() {
310                *converter.id_mut() = format!("dc_grid_{}_converter_{}", self.id, index + 1);
311            } else if trimmed != converter.id() {
312                *converter.id_mut() = trimmed;
313            }
314        }
315    }
316
317    pub fn canonicalize_branch_ids(&mut self) {
318        for (index, branch) in self.branches.iter_mut().enumerate() {
319            let trimmed = branch.id.trim().to_string();
320            if trimmed.is_empty() {
321                branch.id = format!("dc_grid_{}_branch_{}", self.id, index + 1);
322            } else if trimmed != branch.id {
323                branch.id = trimmed;
324            }
325        }
326    }
327}
328
329/// Canonical HVDC namespace on [`crate::network::Network`].
330#[derive(Debug, Clone, Default, Serialize, Deserialize)]
331pub struct HvdcModel {
332    /// Point-to-point HVDC links.
333    #[serde(default, skip_serializing_if = "Vec::is_empty")]
334    pub links: Vec<HvdcLink>,
335    /// Explicit DC grids.
336    #[serde(default, skip_serializing_if = "Vec::is_empty")]
337    pub dc_grids: Vec<DcGrid>,
338}
339
340impl HvdcModel {
341    pub fn is_empty(&self) -> bool {
342        self.links.is_empty() && self.dc_grids.iter().all(DcGrid::is_empty)
343    }
344
345    pub fn has_point_to_point_links(&self) -> bool {
346        !self.links.is_empty()
347    }
348
349    pub fn has_explicit_dc_topology(&self) -> bool {
350        self.dc_grids.iter().any(|grid| !grid.is_empty())
351    }
352
353    pub fn push_link(&mut self, link: HvdcLink) {
354        self.links.push(link);
355    }
356
357    pub fn push_lcc_link(&mut self, link: LccHvdcLink) {
358        self.links.push(HvdcLink::Lcc(link));
359    }
360
361    pub fn push_vsc_link(&mut self, link: VscHvdcLink) {
362        self.links.push(HvdcLink::Vsc(link));
363    }
364
365    pub fn ensure_dc_grid(&mut self, id: u32, name: Option<String>) -> &mut DcGrid {
366        if let Some(index) = self.dc_grids.iter().position(|grid| grid.id == id) {
367            let grid = &mut self.dc_grids[index];
368            if grid.name.is_none() {
369                grid.name = name;
370            }
371            return grid;
372        }
373        self.dc_grids.push(DcGrid::new(id, name));
374        self.dc_grids.last_mut().expect("grid just inserted")
375    }
376
377    pub fn find_dc_grid(&self, id: u32) -> Option<&DcGrid> {
378        self.dc_grids.iter().find(|grid| grid.id == id)
379    }
380
381    pub fn find_dc_grid_mut(&mut self, id: u32) -> Option<&mut DcGrid> {
382        self.dc_grids.iter_mut().find(|grid| grid.id == id)
383    }
384
385    pub fn find_dc_grid_by_bus(&self, bus_id: u32) -> Option<&DcGrid> {
386        self.dc_grids
387            .iter()
388            .find(|grid| grid.buses.iter().any(|bus| bus.bus_id == bus_id))
389    }
390
391    pub fn find_dc_grid_by_bus_mut(&mut self, bus_id: u32) -> Option<&mut DcGrid> {
392        self.dc_grids
393            .iter_mut()
394            .find(|grid| grid.buses.iter().any(|bus| bus.bus_id == bus_id))
395    }
396
397    pub fn find_dc_bus(&self, bus_id: u32) -> Option<&DcBus> {
398        self.dc_grids.iter().find_map(|grid| grid.find_bus(bus_id))
399    }
400
401    pub fn find_dc_bus_mut(&mut self, bus_id: u32) -> Option<&mut DcBus> {
402        self.dc_grids
403            .iter_mut()
404            .find_map(|grid| grid.find_bus_mut(bus_id))
405    }
406
407    pub fn dc_bus_count(&self) -> usize {
408        self.dc_grids.iter().map(|grid| grid.buses.len()).sum()
409    }
410
411    pub fn dc_converter_count(&self) -> usize {
412        self.dc_grids.iter().map(|grid| grid.converters.len()).sum()
413    }
414
415    pub fn dc_branch_count(&self) -> usize {
416        self.dc_grids.iter().map(|grid| grid.branches.len()).sum()
417    }
418
419    pub fn next_dc_grid_id(&self) -> u32 {
420        self.dc_grids.iter().map(|grid| grid.id).max().unwrap_or(0) + 1
421    }
422
423    pub fn next_dc_bus_id(&self) -> u32 {
424        self.dc_grids
425            .iter()
426            .flat_map(|grid| grid.buses.iter().map(|bus| bus.bus_id))
427            .max()
428            .unwrap_or(0)
429            + 1
430    }
431
432    pub fn clear_dc_grids(&mut self) {
433        self.dc_grids.clear();
434    }
435
436    pub fn canonicalize_converter_ids(&mut self) {
437        for grid in &mut self.dc_grids {
438            grid.canonicalize_converter_ids();
439            grid.canonicalize_branch_ids();
440        }
441    }
442
443    pub fn dc_buses(&self) -> impl Iterator<Item = &DcBus> {
444        self.dc_grids.iter().flat_map(|grid| grid.buses.iter())
445    }
446
447    pub fn dc_buses_mut(&mut self) -> impl Iterator<Item = &mut DcBus> {
448        self.dc_grids
449            .iter_mut()
450            .flat_map(|grid| grid.buses.iter_mut())
451    }
452
453    pub fn dc_converters(&self) -> impl Iterator<Item = &DcConverter> {
454        self.dc_grids.iter().flat_map(|grid| grid.converters.iter())
455    }
456
457    pub fn dc_converters_mut(&mut self) -> impl Iterator<Item = &mut DcConverter> {
458        self.dc_grids
459            .iter_mut()
460            .flat_map(|grid| grid.converters.iter_mut())
461    }
462
463    pub fn dc_branches(&self) -> impl Iterator<Item = &DcBranch> {
464        self.dc_grids.iter().flat_map(|grid| grid.branches.iter())
465    }
466
467    pub fn dc_branches_mut(&mut self) -> impl Iterator<Item = &mut DcBranch> {
468        self.dc_grids
469            .iter_mut()
470            .flat_map(|grid| grid.branches.iter_mut())
471    }
472}