1use std::collections::{HashMap, HashSet};
5
6use tracing::debug;
7
8use crate::market::{
9 AmbientConditions, CombinedCyclePlant, DispatchableLoad, EmissionPolicy, MarketRules,
10 OutageEntry, PumpedHydroUnit, ReserveZone,
11};
12use crate::network::asset::AssetCatalog;
13use crate::network::boundary::BoundaryData;
14use crate::network::breaker::BreakerRating;
15use crate::network::cgmes_roundtrip::CgmesRoundtripData;
16use crate::network::grounding::GroundingEntry;
17use crate::network::impedance_correction::ImpedanceCorrectionTable;
18use crate::network::induction_machine::InductionMachine;
19use crate::network::market_data::MarketData;
20use crate::network::measurement::CimMeasurement;
21use crate::network::multi_section_line::MultiSectionLineGroup;
22use crate::network::net_ops::NetworkOperationsData;
23use crate::network::op_limits::OperationalLimits;
24use crate::network::protection::ProtectionData;
25use crate::network::scheduled_area_transfer::ScheduledAreaTransfer;
26use crate::network::switching_device_rating::SwitchingDeviceRatingSet;
27use crate::network::types::DEFAULT_BASE_MVA;
28use crate::network::voltage_droop_control::VoltageDroopControl;
29use crate::network::{
30 AreaSchedule, Branch, Bus, BusType, FactsDevice, FixedShunt, Flowgate, Generator, HvdcModel,
31 Interface, Load, NodeBreakerTopology, OltcSpec, Owner, ParSpec, PowerInjection, Region,
32 SwitchedShunt, SwitchedShuntOpf, TopologyMappingState, generator::StorageValidationError,
33};
34use serde::{Deserialize, Serialize};
35
36#[derive(Debug, thiserror::Error)]
38pub enum NetworkError {
39 #[error("network has no buses")]
41 EmptyNetwork,
42
43 #[error("base_mva must be positive and finite, got {0}")]
45 InvalidBaseMva(f64),
46
47 #[error("duplicate bus number {0}")]
49 DuplicateBusNumber(u32),
50
51 #[error("network has no slack bus")]
53 NoSlackBus,
54
55 #[error("branch ({branch_from}-{branch_to}) references missing bus {missing_bus}")]
57 InvalidBranchEndpoint {
58 branch_from: u32,
59 branch_to: u32,
60 missing_bus: u32,
61 },
62
63 #[error("branch ({0}-{0}) is a self-loop")]
65 SelfLoopBranch(u32),
66
67 #[error("generator references missing bus {0}")]
69 InvalidGeneratorBus(u32),
70
71 #[error("generator at bus {bus} references missing regulated bus {reg_bus}")]
73 InvalidGeneratorRegulatedBus { bus: u32, reg_bus: u32 },
74
75 #[error("load references missing bus {0}")]
77 InvalidLoadBus(u32),
78
79 #[error("power injection references missing bus {0}")]
81 InvalidPowerInjectionBus(u32),
82
83 #[error("fixed shunt references missing bus {0}")]
85 InvalidFixedShuntBus(u32),
86
87 #[error("dispatchable load references missing bus {0}")]
89 InvalidDispatchableLoadBus(u32),
90
91 #[error("duplicate canonical generator id `{id}`")]
93 DuplicateGeneratorId { id: String },
94
95 #[error("switched shunt `{id}` references missing host bus {bus}")]
97 InvalidSwitchedShuntBus { id: String, bus: u32 },
98
99 #[error("switched shunt `{id}` references missing regulated bus {bus}")]
101 InvalidSwitchedShuntRegulatedBus { id: String, bus: u32 },
102
103 #[error("switched shunt OPF `{id}` references missing host bus {bus}")]
105 InvalidSwitchedShuntOpfBus { id: String, bus: u32 },
106
107 #[error("generator at bus {bus} has pmin > pmax")]
109 InvalidGeneratorLimits { bus: u32 },
110
111 #[error("generator at bus {bus} has qmin > qmax")]
113 InvalidGeneratorReactiveLimits { bus: u32 },
114
115 #[error("generator at bus {bus} has invalid storage parameters: {source}")]
117 InvalidStorageParameters {
118 bus: u32,
119 #[source]
120 source: StorageValidationError,
121 },
122
123 #[error("bus {bus} field `{field}` is invalid: {value}")]
125 InvalidBusField {
126 bus: u32,
127 field: &'static str,
128 value: f64,
129 },
130
131 #[error("load at bus {bus} field `{field}` is invalid: {value}")]
133 InvalidLoadField {
134 bus: u32,
135 field: &'static str,
136 value: f64,
137 },
138
139 #[error("fixed shunt at bus {bus} field `{field}` is invalid: {value}")]
141 InvalidFixedShuntField {
142 bus: u32,
143 field: &'static str,
144 value: f64,
145 },
146
147 #[error("power injection at bus {bus} field `{field}` is invalid: {value}")]
149 InvalidPowerInjectionField {
150 bus: u32,
151 field: &'static str,
152 value: f64,
153 },
154
155 #[error("generator at bus {bus} field `{field}` is invalid: {value}")]
157 InvalidGeneratorField {
158 bus: u32,
159 field: &'static str,
160 value: f64,
161 },
162
163 #[error("branch ({from_bus}-{to_bus}) field `{field}` is invalid: {value}")]
165 InvalidBranchField {
166 from_bus: u32,
167 to_bus: u32,
168 field: &'static str,
169 value: f64,
170 },
171
172 #[error(
174 "branch ({from_bus}-{to_bus}) has invalid angle bounds (min={min_rad:?}, max={max_rad:?})"
175 )]
176 InvalidBranchAngleBounds {
177 from_bus: u32,
178 to_bus: u32,
179 min_rad: Option<f64>,
180 max_rad: Option<f64>,
181 },
182
183 #[error(
185 "connected component with buses {buses:?} has slack buses {slack_buses:?}; expected exactly one slack bus"
186 )]
187 InvalidSlackPlacement {
188 buses: Vec<u32>,
189 slack_buses: Vec<u32>,
190 },
191
192 #[error("bus {bus} is marked isolated but still has in-service connectivity")]
194 InvalidIsolatedBusConnectivity { bus: u32 },
195
196 #[error("bus {0} has non-finite voltage initial condition (vm or va is NaN/Inf)")]
198 NonFiniteBusVoltage(u32),
199
200 #[error(
202 "bus {0} has invalid voltage bounds (vmin={1}, vmax={2}): must be finite with vmin <= vmax"
203 )]
204 InvalidBusVoltageBounds(u32, f64, f64),
205
206 #[error("branch ({0}-{1}) has non-finite impedance parameter (r, x, b, or tap is NaN/Inf)")]
208 NonFiniteBranchImpedance(u32, u32),
209
210 #[error("duplicate branch key ({from_bus}-{to_bus} ckt {circuit})")]
212 DuplicateBranchKey {
213 from_bus: u32,
214 to_bus: u32,
215 circuit: String,
216 },
217
218 #[error("network has node-breaker topology but no mapped bus-branch view yet")]
220 MissingTopologyMapping,
221
222 #[error("network node-breaker topology is stale; call rebuild_topology() before solving")]
224 StaleNodeBreakerTopology,
225
226 #[error("interface `{name}` is invalid: {detail}")]
228 InvalidInterfaceDefinition { name: String, detail: String },
229
230 #[error("flowgate `{name}` is invalid: {detail}")]
232 InvalidFlowgateDefinition { name: String, detail: String },
233
234 #[error("duplicate area schedule number {0}")]
236 DuplicateAreaScheduleNumber(u32),
237
238 #[error("area {area} references invalid slack bus {slack_bus}")]
240 InvalidAreaScheduleSlackBus { area: u32, slack_bus: u32 },
241
242 #[error("area {area} field `{field}` is invalid: {value}")]
244 InvalidAreaScheduleField {
245 area: u32,
246 field: &'static str,
247 value: f64,
248 },
249
250 #[error("duplicate HVDC link name `{name}`")]
252 DuplicateHvdcLinkName { name: String },
253
254 #[error("HVDC link `{name}` references missing AC bus {bus}")]
256 InvalidHvdcLinkEndpoint { name: String, bus: u32 },
257
258 #[error("duplicate explicit DC grid id {id}")]
260 DuplicateDcGridId { id: u32 },
261
262 #[error("explicit DC grid {grid_id} has duplicate DC bus id {bus_id}")]
264 DuplicateDcBusId { grid_id: u32, bus_id: u32 },
265
266 #[error("explicit DC grid {grid_id} converter references missing AC bus {ac_bus}")]
268 InvalidDcConverterAcBus { grid_id: u32, ac_bus: u32 },
269
270 #[error("explicit DC grid {grid_id} converter references missing DC bus {dc_bus}")]
272 InvalidDcConverterDcBus { grid_id: u32, dc_bus: u32 },
273
274 #[error(
276 "explicit DC grid {grid_id} branch ({from_bus}-{to_bus}) references missing DC bus {missing_bus}"
277 )]
278 InvalidDcBranchEndpoint {
279 grid_id: u32,
280 from_bus: u32,
281 to_bus: u32,
282 missing_bus: u32,
283 },
284
285 #[error(
287 "network mixes point-to-point HVDC links with explicit DC-network topology; choose one canonical HVDC representation per solve"
288 )]
289 MixedHvdcRepresentation,
290}
291
292fn is_valid_lower_bound(value: f64) -> bool {
293 value.is_finite() || value == f64::NEG_INFINITY
294}
295
296fn is_valid_upper_bound(value: f64) -> bool {
297 value.is_finite() || value == f64::INFINITY
298}
299
300#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
308pub struct BranchEquipmentKey {
309 pub bus_a: u32,
310 pub bus_b: u32,
311 pub circuit: String,
312}
313
314impl BranchEquipmentKey {
315 pub fn new(from_bus: u32, to_bus: u32, circuit: impl Into<String>) -> Self {
316 let circuit = circuit.into();
317 if from_bus <= to_bus {
318 Self {
319 bus_a: from_bus,
320 bus_b: to_bus,
321 circuit,
322 }
323 } else {
324 Self {
325 bus_a: to_bus,
326 bus_b: from_bus,
327 circuit,
328 }
329 }
330 }
331
332 pub fn from_branch(branch: &Branch) -> Self {
333 Self::new(branch.from_bus, branch.to_bus, branch.circuit.clone())
334 }
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize)]
338struct BranchConditionalRatingEntry {
339 branch: BranchEquipmentKey,
340 ratings: Vec<ConditionalRating>,
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize)]
344struct BranchThermalRatingSnapshot {
345 branch: BranchEquipmentKey,
346 rating_a_mva: f64,
347 rating_c_mva: f64,
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize, Default)]
355pub struct BranchConditionalRatings {
356 #[serde(default, skip_serializing_if = "Vec::is_empty")]
357 entries: Vec<BranchConditionalRatingEntry>,
358 #[serde(default, skip_serializing_if = "Vec::is_empty")]
359 base_thermal_ratings: Vec<BranchThermalRatingSnapshot>,
360}
361
362impl BranchConditionalRatings {
363 pub fn is_empty(&self) -> bool {
365 self.entries.is_empty()
366 }
367
368 pub fn len(&self) -> usize {
370 self.entries.len()
371 }
372
373 pub fn iter(&self) -> impl Iterator<Item = (&BranchEquipmentKey, &Vec<ConditionalRating>)> {
375 self.entries
376 .iter()
377 .map(|entry| (&entry.branch, &entry.ratings))
378 }
379
380 pub fn values(&self) -> impl Iterator<Item = &Vec<ConditionalRating>> {
382 self.entries.iter().map(|entry| &entry.ratings)
383 }
384
385 pub fn get(&self, branch: &BranchEquipmentKey) -> Option<&[ConditionalRating]> {
387 self.entries
388 .iter()
389 .find(|entry| entry.branch == *branch)
390 .map(|entry| entry.ratings.as_slice())
391 }
392
393 pub fn get_for_branch(&self, branch: &Branch) -> Option<&[ConditionalRating]> {
395 self.get(&BranchEquipmentKey::from_branch(branch))
396 }
397
398 pub fn insert(&mut self, branch: BranchEquipmentKey, ratings: Vec<ConditionalRating>) {
400 if let Some(entry) = self.entries.iter_mut().find(|entry| entry.branch == branch) {
401 entry.ratings = ratings;
402 } else {
403 self.entries
404 .push(BranchConditionalRatingEntry { branch, ratings });
405 }
406 }
407
408 pub fn insert_for_branch(&mut self, branch: &Branch, ratings: Vec<ConditionalRating>) {
410 self.insert(BranchEquipmentKey::from_branch(branch), ratings);
411 }
412
413 pub fn clear(&mut self) {
415 self.entries.clear();
416 }
417
418 pub fn extend(&mut self, other: Self) {
420 for entry in other.entries {
421 self.insert(entry.branch, entry.ratings);
422 }
423 for snapshot in other.base_thermal_ratings {
424 if !self
425 .base_thermal_ratings
426 .iter()
427 .any(|existing| existing.branch == snapshot.branch)
428 {
429 self.base_thermal_ratings.push(snapshot);
430 }
431 }
432 }
433
434 fn apply_to(&mut self, branches: &mut [Branch], active_conditions: &[String]) {
435 if self.entries.is_empty() {
436 return;
437 }
438 let branch_positions: HashMap<BranchEquipmentKey, usize> = branches
439 .iter()
440 .enumerate()
441 .map(|(idx, branch)| (BranchEquipmentKey::from_branch(branch), idx))
442 .collect();
443 for entry in &self.entries {
445 if let Some(&br_idx) = branch_positions.get(&entry.branch)
446 && let Some(branch) = branches.get(br_idx)
447 && !self
448 .base_thermal_ratings
449 .iter()
450 .any(|snapshot| snapshot.branch == entry.branch)
451 {
452 self.base_thermal_ratings.push(BranchThermalRatingSnapshot {
453 branch: entry.branch.clone(),
454 rating_a_mva: branch.rating_a_mva,
455 rating_c_mva: branch.rating_c_mva,
456 });
457 }
458 }
459 for snapshot in &self.base_thermal_ratings {
461 if let Some(&br_idx) = branch_positions.get(&snapshot.branch)
462 && let Some(branch) = branches.get_mut(br_idx)
463 {
464 branch.rating_a_mva = snapshot.rating_a_mva;
465 branch.rating_c_mva = snapshot.rating_c_mva;
466 }
467 }
468 for entry in &self.entries {
470 let Some(&br_idx) = branch_positions.get(&entry.branch) else {
471 continue;
472 };
473 let Some(branch) = branches.get_mut(br_idx) else {
474 continue;
475 };
476 let matching: Vec<&ConditionalRating> = entry
477 .ratings
478 .iter()
479 .filter(|cr| active_conditions.iter().any(|c| c == &cr.condition_id))
480 .collect();
481 if matching.is_empty() {
482 continue;
483 }
484 if let Some(min_a) = matching
485 .iter()
486 .filter(|cr| cr.rating_a_mva > 0.0)
487 .map(|cr| cr.rating_a_mva)
488 .reduce(f64::min)
489 {
490 branch.rating_a_mva = min_a;
491 }
492 if let Some(min_c) = matching
493 .iter()
494 .filter(|cr| cr.rating_c_mva > 0.0)
495 .map(|cr| cr.rating_c_mva)
496 .reduce(f64::min)
497 {
498 branch.rating_c_mva = min_c;
499 }
500 }
501 }
502
503 fn reset_on(&mut self, branches: &mut [Branch]) {
504 let branch_positions: HashMap<BranchEquipmentKey, usize> = branches
505 .iter()
506 .enumerate()
507 .map(|(idx, branch)| (BranchEquipmentKey::from_branch(branch), idx))
508 .collect();
509 for snapshot in &self.base_thermal_ratings {
510 if let Some(&br_idx) = branch_positions.get(&snapshot.branch)
511 && let Some(branch) = branches.get_mut(br_idx)
512 {
513 branch.rating_a_mva = snapshot.rating_a_mva;
514 branch.rating_c_mva = snapshot.rating_c_mva;
515 }
516 }
517 self.base_thermal_ratings.clear();
518 }
519}
520
521#[derive(Debug, Clone, Serialize, Deserialize)]
523pub struct PhaseImpedanceEntry {
524 pub row: u8,
526 pub col: u8,
528 pub r: f64,
530 pub x: f64,
532 pub b: f64,
534}
535
536#[derive(Debug, Clone, Serialize, Deserialize)]
538pub struct MutualCoupling {
539 pub line1_id: String,
541 pub line2_id: String,
543 pub r: f64,
545 pub x: f64,
547}
548
549#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
551pub struct GeoPoint {
552 pub x: f64,
554 pub y: f64,
556}
557
558#[derive(Debug, Clone, Default, Serialize, Deserialize)]
565pub struct NetworkCimData {
566 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
568 pub per_length_phase_impedances: HashMap<String, Vec<PhaseImpedanceEntry>>,
569
570 #[serde(default, skip_serializing_if = "Vec::is_empty")]
572 pub mutual_couplings: Vec<MutualCoupling>,
573
574 #[serde(default, skip_serializing_if = "Vec::is_empty")]
577 pub grounding_impedances: Vec<GroundingEntry>,
578
579 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
581 pub geo_locations: HashMap<String, Vec<GeoPoint>>,
582
583 #[serde(default, skip_serializing_if = "Vec::is_empty")]
585 pub measurements: Vec<CimMeasurement>,
586
587 #[serde(default, skip_serializing_if = "AssetCatalog::is_empty")]
589 pub asset_catalog: AssetCatalog,
590
591 #[serde(default, skip_serializing_if = "OperationalLimits::is_empty")]
593 pub operational_limits: OperationalLimits,
594
595 #[serde(default, skip_serializing_if = "BoundaryData::is_empty")]
597 pub boundary_data: BoundaryData,
598
599 #[serde(default, skip_serializing_if = "CgmesRoundtripData::is_empty")]
603 pub cgmes_roundtrip: CgmesRoundtripData,
604
605 #[serde(default, skip_serializing_if = "ProtectionData::is_empty")]
607 pub protection_data: ProtectionData,
608
609 #[serde(default, skip_serializing_if = "MarketData::is_empty")]
611 pub market_data: MarketData,
612
613 #[serde(default, skip_serializing_if = "NetworkOperationsData::is_empty")]
615 pub network_operations: NetworkOperationsData,
616}
617
618#[derive(Debug, Clone, Default, Serialize, Deserialize)]
624pub struct NetworkMetadata {
625 #[serde(default, skip_serializing_if = "Vec::is_empty")]
627 pub regions: Vec<Region>,
628 #[serde(default, skip_serializing_if = "Vec::is_empty")]
630 pub owners: Vec<Owner>,
631 #[serde(default, skip_serializing_if = "Vec::is_empty")]
633 pub voltage_droop_controls: Vec<VoltageDroopControl>,
634 #[serde(default, skip_serializing_if = "Vec::is_empty")]
636 pub switching_device_rating_sets: Vec<SwitchingDeviceRatingSet>,
637 #[serde(default, skip_serializing_if = "Vec::is_empty")]
639 pub scheduled_area_transfers: Vec<ScheduledAreaTransfer>,
640 #[serde(default, skip_serializing_if = "Vec::is_empty")]
643 pub impedance_corrections: Vec<ImpedanceCorrectionTable>,
644 #[serde(default, skip_serializing_if = "Vec::is_empty")]
646 pub multi_section_line_groups: Vec<MultiSectionLineGroup>,
647}
648
649#[derive(Debug, Clone, Default, Serialize, Deserialize)]
652pub struct NetworkMarketData {
653 #[serde(default, skip_serializing_if = "Vec::is_empty")]
655 pub dispatchable_loads: Vec<DispatchableLoad>,
656 #[serde(default, skip_serializing_if = "Vec::is_empty")]
658 pub pumped_hydro_units: Vec<PumpedHydroUnit>,
659 #[serde(default, skip_serializing_if = "Vec::is_empty")]
661 pub combined_cycle_plants: Vec<CombinedCyclePlant>,
662 #[serde(default, skip_serializing_if = "Vec::is_empty")]
664 pub outage_schedule: Vec<OutageEntry>,
665 #[serde(default, skip_serializing_if = "Vec::is_empty")]
667 pub reserve_zones: Vec<ReserveZone>,
668 #[serde(default, skip_serializing_if = "Option::is_none")]
670 pub ambient: Option<AmbientConditions>,
671 #[serde(default, skip_serializing_if = "Option::is_none")]
673 pub emission_policy: Option<EmissionPolicy>,
674 #[serde(default, skip_serializing_if = "Option::is_none")]
676 pub market_rules: Option<MarketRules>,
677}
678
679#[derive(Debug, Clone, Default, Serialize, Deserialize)]
681pub struct NetworkControlData {
682 #[serde(default, skip_serializing_if = "Vec::is_empty")]
684 pub switched_shunts: Vec<SwitchedShunt>,
685 #[serde(default, skip_serializing_if = "Vec::is_empty")]
687 pub switched_shunts_opf: Vec<SwitchedShuntOpf>,
688 #[serde(default, skip_serializing_if = "Vec::is_empty")]
690 pub oltc_specs: Vec<OltcSpec>,
691 #[serde(default, skip_serializing_if = "Vec::is_empty")]
693 pub par_specs: Vec<ParSpec>,
694}
695
696#[derive(Debug, Clone, Serialize, Deserialize)]
707pub struct Network {
708 pub name: String,
710 pub base_mva: f64,
712 #[serde(default = "Network::default_freq_hz")]
715 pub freq_hz: f64,
716 pub buses: Vec<Bus>,
718 pub branches: Vec<Branch>,
720 pub generators: Vec<Generator>,
722 pub loads: Vec<Load>,
724 #[serde(default)]
726 pub controls: NetworkControlData,
727
728 #[serde(default, skip_serializing_if = "HvdcModel::is_empty")]
730 pub hvdc: HvdcModel,
731
732 #[serde(default, skip_serializing_if = "Vec::is_empty")]
736 pub area_schedules: Vec<AreaSchedule>,
737
738 #[serde(default, skip_serializing_if = "Vec::is_empty")]
743 pub facts_devices: Vec<FactsDevice>,
744
745 #[serde(default)]
748 pub metadata: NetworkMetadata,
749
750 #[serde(default)]
756 pub cim: NetworkCimData,
757
758 #[serde(default, skip_serializing_if = "Vec::is_empty")]
761 pub interfaces: Vec<Interface>,
762
763 #[serde(default, skip_serializing_if = "Vec::is_empty")]
769 pub flowgates: Vec<Flowgate>,
770
771 #[serde(default, skip_serializing_if = "Vec::is_empty")]
777 pub nomograms: Vec<crate::network::flowgate::OperatingNomogram>,
778
779 #[serde(default, skip_serializing_if = "Option::is_none")]
789 pub topology: Option<NodeBreakerTopology>,
790
791 #[serde(default, skip_serializing_if = "Vec::is_empty")]
796 pub induction_machines: Vec<InductionMachine>,
797
798 #[serde(default, skip_serializing_if = "BranchConditionalRatings::is_empty")]
804 pub conditional_limits: BranchConditionalRatings,
805
806 #[serde(default, skip_serializing_if = "Vec::is_empty")]
808 pub breaker_ratings: Vec<BreakerRating>,
809 #[serde(default, skip_serializing_if = "Vec::is_empty")]
811 pub fixed_shunts: Vec<FixedShunt>,
812 #[serde(default, skip_serializing_if = "Vec::is_empty")]
814 pub power_injections: Vec<PowerInjection>,
815
816 #[serde(default)]
819 pub market_data: NetworkMarketData,
820}
821
822#[derive(Debug, Clone, Serialize, Deserialize)]
828pub struct ConditionalRating {
829 pub condition_id: String,
831 pub rating_a_mva: f64,
833 pub rating_c_mva: f64,
835}
836
837pub(crate) trait HasCanonicalId {
839 fn canonical_id(&self) -> &str;
840 fn set_canonical_id(&mut self, id: String);
841 fn bus_number(&self) -> u32;
842}
843
844impl HasCanonicalId for Generator {
845 fn canonical_id(&self) -> &str {
846 &self.id
847 }
848 fn set_canonical_id(&mut self, id: String) {
849 self.id = id;
850 }
851 fn bus_number(&self) -> u32 {
852 self.bus
853 }
854}
855
856impl HasCanonicalId for Load {
857 fn canonical_id(&self) -> &str {
858 &self.id
859 }
860 fn set_canonical_id(&mut self, id: String) {
861 self.id = id;
862 }
863 fn bus_number(&self) -> u32 {
864 self.bus
865 }
866}
867
868impl HasCanonicalId for FixedShunt {
869 fn canonical_id(&self) -> &str {
870 &self.id
871 }
872 fn set_canonical_id(&mut self, id: String) {
873 self.id = id;
874 }
875 fn bus_number(&self) -> u32 {
876 self.bus
877 }
878}
879
880impl HasCanonicalId for SwitchedShunt {
881 fn canonical_id(&self) -> &str {
882 &self.id
883 }
884 fn set_canonical_id(&mut self, id: String) {
885 self.id = id;
886 }
887 fn bus_number(&self) -> u32 {
888 self.bus
889 }
890}
891
892impl HasCanonicalId for SwitchedShuntOpf {
893 fn canonical_id(&self) -> &str {
894 &self.id
895 }
896 fn set_canonical_id(&mut self, id: String) {
897 self.id = id;
898 }
899 fn bus_number(&self) -> u32 {
900 self.bus
901 }
902}
903
904fn canonicalize_ids(items: &mut [impl HasCanonicalId], prefix: &str) {
910 let mut used_ids = HashSet::new();
911 for item in items.iter_mut() {
912 let trimmed = item.canonical_id().trim().to_string();
913 if trimmed.is_empty() {
914 continue;
915 }
916 if item.canonical_id() != trimmed {
917 item.set_canonical_id(trimmed.clone());
918 }
919 used_ids.insert(trimmed);
920 }
921
922 let mut ordinal_by_bus: HashMap<u32, usize> = HashMap::new();
923 for item in items.iter_mut() {
924 let bus = item.bus_number();
925 let ordinal = ordinal_by_bus.entry(bus).or_insert(0);
926 *ordinal += 1;
927
928 if !item.canonical_id().trim().is_empty() {
929 continue;
930 }
931
932 let base = format!("{prefix}_{bus}_{ordinal}");
933 let mut candidate = base.clone();
934 let mut collision = 2usize;
935 while used_ids.contains(&candidate) {
936 candidate = format!("{base}_{collision}");
937 collision += 1;
938 }
939
940 used_ids.insert(candidate.clone());
941 item.set_canonical_id(candidate);
942 }
943}
944
945impl Default for Network {
946 fn default() -> Self {
947 Self {
948 name: String::new(),
949 base_mva: DEFAULT_BASE_MVA,
950 freq_hz: 60.0,
951 buses: Vec::new(),
952 branches: Vec::new(),
953 generators: Vec::new(),
954 loads: Vec::new(),
955 controls: NetworkControlData::default(),
956 hvdc: HvdcModel::default(),
957 area_schedules: Vec::new(),
958 facts_devices: Vec::new(),
959 metadata: NetworkMetadata::default(),
960 cim: NetworkCimData::default(),
961 interfaces: Vec::new(),
962 flowgates: Vec::new(),
963 nomograms: Vec::new(),
964 topology: None,
965 induction_machines: Vec::new(),
966 conditional_limits: BranchConditionalRatings::default(),
967 breaker_ratings: Vec::new(),
968 fixed_shunts: Vec::new(),
969 power_injections: Vec::new(),
970 market_data: NetworkMarketData::default(),
971 }
972 }
973}
974
975impl Network {
976 fn default_freq_hz() -> f64 {
978 60.0
979 }
980
981 pub fn new(name: &str) -> Self {
984 debug!(name, "creating new network");
985 Self {
986 name: name.to_string(),
987 ..Default::default()
988 }
989 }
990
991 pub fn apply_conditional_limits(&mut self, active_conditions: &[String]) {
998 self.conditional_limits
999 .apply_to(&mut self.branches, active_conditions);
1000 }
1001
1002 pub fn reset_conditional_limits(&mut self) {
1004 self.conditional_limits.reset_on(&mut self.branches);
1005 }
1006
1007 pub fn storage_generators(&self) -> impl Iterator<Item = (usize, &Generator)> {
1009 self.generators
1010 .iter()
1011 .enumerate()
1012 .filter(|(_, g)| g.is_storage())
1013 }
1014
1015 pub fn n_buses(&self) -> usize {
1017 self.buses.len()
1018 }
1019
1020 pub fn n_branches(&self) -> usize {
1022 self.branches.len()
1023 }
1024
1025 pub fn max_bus_number(&self) -> u32 {
1027 self.buses.iter().map(|b| b.number).max().unwrap_or(0)
1028 }
1029
1030 pub fn n_generators(&self) -> usize {
1032 self.generators.len()
1033 }
1034
1035 pub fn n_generators_in_service(&self) -> usize {
1037 self.generators.iter().filter(|g| g.in_service).count()
1038 }
1039
1040 pub fn validate_structure(&self) -> Result<(), NetworkError> {
1046 if self.buses.is_empty() {
1048 return Err(NetworkError::EmptyNetwork);
1049 }
1050
1051 if !self.base_mva.is_finite() || self.base_mva <= 0.0 {
1053 return Err(NetworkError::InvalidBaseMva(self.base_mva));
1054 }
1055
1056 let mut bus_numbers = std::collections::HashSet::new();
1058 for bus in &self.buses {
1059 if !bus_numbers.insert(bus.number) {
1060 return Err(NetworkError::DuplicateBusNumber(bus.number));
1061 }
1062 }
1063
1064 if let Some(topology) = &self.topology {
1068 match topology.status() {
1069 TopologyMappingState::Missing => return Err(NetworkError::MissingTopologyMapping),
1070 TopologyMappingState::Stale => return Err(NetworkError::StaleNodeBreakerTopology),
1071 TopologyMappingState::Current => {}
1072 }
1073 }
1074
1075 for branch in &self.branches {
1077 if !bus_numbers.contains(&branch.from_bus) {
1078 return Err(NetworkError::InvalidBranchEndpoint {
1079 branch_from: branch.from_bus,
1080 branch_to: branch.to_bus,
1081 missing_bus: branch.from_bus,
1082 });
1083 }
1084 if !bus_numbers.contains(&branch.to_bus) {
1085 return Err(NetworkError::InvalidBranchEndpoint {
1086 branch_from: branch.from_bus,
1087 branch_to: branch.to_bus,
1088 missing_bus: branch.to_bus,
1089 });
1090 }
1091 if branch.from_bus == branch.to_bus {
1092 return Err(NetworkError::SelfLoopBranch(branch.from_bus));
1093 }
1094 }
1095
1096 let mut branch_keys = std::collections::HashSet::new();
1098 for branch in &self.branches {
1099 let key = BranchEquipmentKey::from_branch(branch);
1100 if !branch_keys.insert(key) {
1101 return Err(NetworkError::DuplicateBranchKey {
1102 from_bus: branch.from_bus.min(branch.to_bus),
1103 to_bus: branch.from_bus.max(branch.to_bus),
1104 circuit: branch.circuit.clone(),
1105 });
1106 }
1107 }
1108
1109 self.validate_interface_definitions()?;
1110 self.validate_internal_control_indices()?;
1111 self.validate_hvdc_structure(&bus_numbers)?;
1112
1113 for load in &self.loads {
1116 if !bus_numbers.contains(&load.bus) {
1117 return Err(NetworkError::InvalidLoadBus(load.bus));
1118 }
1119 }
1120 for injection in &self.power_injections {
1121 if !bus_numbers.contains(&injection.bus) {
1122 return Err(NetworkError::InvalidPowerInjectionBus(injection.bus));
1123 }
1124 }
1125 for shunt in &self.fixed_shunts {
1126 if !bus_numbers.contains(&shunt.bus) {
1127 return Err(NetworkError::InvalidFixedShuntBus(shunt.bus));
1128 }
1129 }
1130 for resource in &self.market_data.dispatchable_loads {
1131 if !bus_numbers.contains(&resource.bus) {
1132 return Err(NetworkError::InvalidDispatchableLoadBus(resource.bus));
1133 }
1134 }
1135
1136 let mut generator_ids = std::collections::HashSet::new();
1141 for g in &self.generators {
1142 if !bus_numbers.contains(&g.bus) {
1143 return Err(NetworkError::InvalidGeneratorBus(g.bus));
1144 }
1145 if let Some(reg_bus) = g.reg_bus
1146 && !bus_numbers.contains(®_bus)
1147 {
1148 return Err(NetworkError::InvalidGeneratorRegulatedBus {
1149 bus: g.bus,
1150 reg_bus,
1151 });
1152 }
1153 let canonical_id = g.id.trim();
1154 if !canonical_id.is_empty() && !generator_ids.insert(canonical_id.to_string()) {
1155 return Err(NetworkError::DuplicateGeneratorId {
1156 id: canonical_id.to_string(),
1157 });
1158 }
1159 if g.pmin > g.pmax {
1160 return Err(NetworkError::InvalidGeneratorLimits { bus: g.bus });
1161 }
1162 }
1163
1164 for g in &self.generators {
1166 if let Some(storage) = &g.storage {
1167 storage
1168 .validate()
1169 .map_err(|source| NetworkError::InvalidStorageParameters {
1170 bus: g.bus,
1171 source,
1172 })?;
1173 }
1174 }
1175
1176 Ok(())
1177 }
1178
1179 pub fn validate_for_solve(&self) -> Result<(), NetworkError> {
1186 self.validate_structure()?;
1187 self.validate_hvdc_solve_contract()?;
1188 self.validate_area_schedules()?;
1189 self.validate_component_slack()?;
1190 self.validate_numerics_for_solve()?;
1191 Ok(())
1192 }
1193
1194 pub fn validate_for_dc_solve(&self) -> Result<(), NetworkError> {
1200 self.validate_structure()?;
1201 self.validate_hvdc_solve_contract()?;
1202 self.validate_area_schedules()?;
1203 self.validate_component_slack()?;
1204 self.validate_numerics_for_dc_solve()?;
1205 Ok(())
1206 }
1207
1208 pub fn validate(&self) -> Result<(), NetworkError> {
1212 self.validate_for_solve()
1213 }
1214
1215 fn validate_component_slack(&self) -> Result<(), NetworkError> {
1216 let bus_index = self.bus_index_map();
1217 let mut adjacency: Vec<Vec<usize>> = vec![Vec::new(); self.buses.len()];
1218 let mut electrically_active = vec![false; self.buses.len()];
1219
1220 for (idx, bus) in self.buses.iter().enumerate() {
1221 electrically_active[idx] =
1222 bus.shunt_conductance_mw != 0.0 || bus.shunt_susceptance_mvar != 0.0;
1223 }
1224 let mark_active_bus_number = |bus_number: u32, active: &mut [bool]| {
1225 if let Some(&idx) = bus_index.get(&bus_number) {
1226 active[idx] = true;
1227 }
1228 };
1229 for load in self.loads.iter().filter(|load| load.in_service) {
1230 mark_active_bus_number(load.bus, &mut electrically_active);
1231 }
1232 for generator in self
1233 .generators
1234 .iter()
1235 .filter(|generator| generator.in_service)
1236 {
1237 mark_active_bus_number(generator.bus, &mut electrically_active);
1238 }
1239 let regulating_targets: HashMap<u32, usize> = self
1240 .generators
1241 .iter()
1242 .filter(|generator| generator.can_voltage_regulate())
1243 .fold(HashMap::new(), |mut counts, generator| {
1244 let target_bus = generator.reg_bus.unwrap_or(generator.bus);
1245 *counts.entry(target_bus).or_insert(0) += 1;
1246 counts
1247 });
1248 for injection in self
1249 .power_injections
1250 .iter()
1251 .filter(|injection| injection.in_service)
1252 {
1253 mark_active_bus_number(injection.bus, &mut electrically_active);
1254 }
1255 for shunt in self.fixed_shunts.iter().filter(|shunt| shunt.in_service) {
1256 mark_active_bus_number(shunt.bus, &mut electrically_active);
1257 }
1258 for shunt in &self.controls.switched_shunts {
1259 if shunt.b_injected() != 0.0
1260 && let Some(&idx) = bus_index.get(&shunt.bus)
1261 && let Some(active) = electrically_active.get_mut(idx)
1262 {
1263 *active = true;
1264 }
1265 }
1266 for shunt in &self.controls.switched_shunts_opf {
1267 if shunt.b_init_pu != 0.0
1268 && let Some(&idx) = bus_index.get(&shunt.bus)
1269 && let Some(active) = electrically_active.get_mut(idx)
1270 {
1271 *active = true;
1272 }
1273 }
1274
1275 for branch in self.branches.iter().filter(|br| br.in_service) {
1276 let Some(&from_idx) = bus_index.get(&branch.from_bus) else {
1277 continue;
1278 };
1279 let Some(&to_idx) = bus_index.get(&branch.to_bus) else {
1280 continue;
1281 };
1282 adjacency[from_idx].push(to_idx);
1283 adjacency[to_idx].push(from_idx);
1284 }
1285
1286 let mut visited = vec![false; self.buses.len()];
1287 for start_idx in 0..self.buses.len() {
1288 if visited[start_idx] {
1289 continue;
1290 }
1291
1292 if self.buses[start_idx].bus_type == BusType::Isolated {
1293 visited[start_idx] = true;
1294 let bus_number = self.buses[start_idx].number;
1295 if !adjacency[start_idx].is_empty() || electrically_active[start_idx] {
1296 return Err(NetworkError::InvalidIsolatedBusConnectivity { bus: bus_number });
1297 }
1298 continue;
1299 }
1300
1301 let mut stack = vec![start_idx];
1302 let mut component = Vec::new();
1303 while let Some(idx) = stack.pop() {
1304 if visited[idx] {
1305 continue;
1306 }
1307 if self.buses[idx].bus_type == BusType::Isolated {
1308 return Err(NetworkError::InvalidIsolatedBusConnectivity {
1309 bus: self.buses[idx].number,
1310 });
1311 }
1312 visited[idx] = true;
1313 component.push(idx);
1314 for &next in &adjacency[idx] {
1315 if !visited[next] {
1316 stack.push(next);
1317 }
1318 }
1319 }
1320
1321 if component.is_empty() {
1322 continue;
1323 }
1324
1325 let buses: Vec<u32> = component
1326 .iter()
1327 .map(|&idx| self.buses[idx].number)
1328 .collect();
1329 let slack_buses: Vec<u32> = component
1330 .iter()
1331 .filter(|&&idx| self.buses[idx].bus_type == BusType::Slack)
1332 .map(|&idx| self.buses[idx].number)
1333 .collect();
1334 if slack_buses.len() != 1 {
1335 return Err(NetworkError::InvalidSlackPlacement { buses, slack_buses });
1336 }
1337 for &idx in &component {
1338 let bus = &self.buses[idx];
1339 let reg_count = regulating_targets.get(&bus.number).copied().unwrap_or(0);
1340 match bus.bus_type {
1341 BusType::Slack if reg_count == 0 => {
1342 return Err(NetworkError::InvalidSlackPlacement { buses, slack_buses });
1343 }
1344 BusType::PV if reg_count == 0 => {
1345 return Err(NetworkError::InvalidGeneratorField {
1346 bus: bus.number,
1347 field: "voltage_regulated",
1348 value: 0.0,
1349 });
1350 }
1351 _ => {}
1352 }
1353 }
1354 }
1355
1356 Ok(())
1357 }
1358
1359 fn validate_area_schedules(&self) -> Result<(), NetworkError> {
1360 let bus_index = self.bus_index_map();
1361 let mut seen_areas = HashSet::new();
1362
1363 for area in &self.area_schedules {
1364 if !seen_areas.insert(area.number) {
1365 return Err(NetworkError::DuplicateAreaScheduleNumber(area.number));
1366 }
1367 if area.slack_bus == 0 || !bus_index.contains_key(&area.slack_bus) {
1368 return Err(NetworkError::InvalidAreaScheduleSlackBus {
1369 area: area.number,
1370 slack_bus: area.slack_bus,
1371 });
1372 }
1373 if !area.p_desired_mw.is_finite() {
1374 return Err(NetworkError::InvalidAreaScheduleField {
1375 area: area.number,
1376 field: "p_desired_mw",
1377 value: area.p_desired_mw,
1378 });
1379 }
1380 if !area.p_tolerance_mw.is_finite() || area.p_tolerance_mw < 0.0 {
1381 return Err(NetworkError::InvalidAreaScheduleField {
1382 area: area.number,
1383 field: "p_tolerance_mw",
1384 value: area.p_tolerance_mw,
1385 });
1386 }
1387 }
1388
1389 Ok(())
1390 }
1391
1392 fn validate_numerics_for_solve(&self) -> Result<(), NetworkError> {
1393 for bus in &self.buses {
1394 if !bus.shunt_conductance_mw.is_finite() {
1395 return Err(NetworkError::InvalidBusField {
1396 bus: bus.number,
1397 field: "shunt_conductance_mw",
1398 value: bus.shunt_conductance_mw,
1399 });
1400 }
1401 if !bus.shunt_susceptance_mvar.is_finite() {
1402 return Err(NetworkError::InvalidBusField {
1403 bus: bus.number,
1404 field: "shunt_susceptance_mvar",
1405 value: bus.shunt_susceptance_mvar,
1406 });
1407 }
1408 if !bus.voltage_magnitude_pu.is_finite() || !bus.voltage_angle_rad.is_finite() {
1409 return Err(NetworkError::NonFiniteBusVoltage(bus.number));
1410 }
1411 if !bus.voltage_min_pu.is_finite()
1412 || !bus.voltage_max_pu.is_finite()
1413 || bus.voltage_min_pu > bus.voltage_max_pu
1414 {
1415 return Err(NetworkError::InvalidBusVoltageBounds(
1416 bus.number,
1417 bus.voltage_min_pu,
1418 bus.voltage_max_pu,
1419 ));
1420 }
1421 }
1422
1423 for load in &self.loads {
1424 for (field, value) in [
1425 ("active_power_demand_mw", load.active_power_demand_mw),
1426 (
1427 "reactive_power_demand_mvar",
1428 load.reactive_power_demand_mvar,
1429 ),
1430 ("zip_p_impedance_frac", load.zip_p_impedance_frac),
1431 ("zip_p_current_frac", load.zip_p_current_frac),
1432 ("zip_p_power_frac", load.zip_p_power_frac),
1433 ("zip_q_impedance_frac", load.zip_q_impedance_frac),
1434 ("zip_q_current_frac", load.zip_q_current_frac),
1435 ("zip_q_power_frac", load.zip_q_power_frac),
1436 (
1437 "freq_sensitivity_p_pct_per_hz",
1438 load.freq_sensitivity_p_pct_per_hz,
1439 ),
1440 (
1441 "freq_sensitivity_q_pct_per_hz",
1442 load.freq_sensitivity_q_pct_per_hz,
1443 ),
1444 ("frac_motor_a", load.frac_motor_a),
1445 ("frac_motor_b", load.frac_motor_b),
1446 ("frac_motor_c", load.frac_motor_c),
1447 ("frac_motor_d", load.frac_motor_d),
1448 ("frac_electronic", load.frac_electronic),
1449 ("frac_static", load.frac_static),
1450 ] {
1451 if !value.is_finite() || (field.ends_with("_frac") && !(0.0..=1.0).contains(&value))
1452 {
1453 return Err(NetworkError::InvalidLoadField {
1454 bus: load.bus,
1455 field,
1456 value,
1457 });
1458 }
1459 }
1460 }
1461
1462 for injection in &self.power_injections {
1463 for (field, value) in [
1464 (
1465 "active_power_injection_mw",
1466 injection.active_power_injection_mw,
1467 ),
1468 (
1469 "reactive_power_injection_mvar",
1470 injection.reactive_power_injection_mvar,
1471 ),
1472 ] {
1473 if !value.is_finite() {
1474 return Err(NetworkError::InvalidPowerInjectionField {
1475 bus: injection.bus,
1476 field,
1477 value,
1478 });
1479 }
1480 }
1481 }
1482
1483 for shunt in &self.fixed_shunts {
1484 for (field, value) in [("g_mw", shunt.g_mw), ("b_mvar", shunt.b_mvar)] {
1485 if !value.is_finite() {
1486 return Err(NetworkError::InvalidFixedShuntField {
1487 bus: shunt.bus,
1488 field,
1489 value,
1490 });
1491 }
1492 }
1493 if let Some(rated_kv) = shunt.rated_kv {
1494 if !rated_kv.is_finite() {
1495 return Err(NetworkError::InvalidFixedShuntField {
1496 bus: shunt.bus,
1497 field: "rated_kv",
1498 value: rated_kv,
1499 });
1500 }
1501 }
1502 if let Some(rated_mvar) = shunt.rated_mvar {
1503 if !rated_mvar.is_finite() {
1504 return Err(NetworkError::InvalidFixedShuntField {
1505 bus: shunt.bus,
1506 field: "rated_mvar",
1507 value: rated_mvar,
1508 });
1509 }
1510 }
1511 }
1512
1513 for g in &self.generators {
1514 for (field, value) in [
1515 ("p", g.p),
1516 ("q", g.q),
1517 ("voltage_setpoint_pu", g.voltage_setpoint_pu),
1518 ("machine_base_mva", g.machine_base_mva),
1519 ] {
1520 if !value.is_finite() {
1521 return Err(NetworkError::InvalidGeneratorField {
1522 bus: g.bus,
1523 field,
1524 value,
1525 });
1526 }
1527 }
1528 if g.machine_base_mva <= 0.0 {
1529 return Err(NetworkError::InvalidGeneratorField {
1530 bus: g.bus,
1531 field: "machine_base_mva",
1532 value: g.machine_base_mva,
1533 });
1534 }
1535 if g.voltage_setpoint_pu <= 0.0 {
1536 return Err(NetworkError::InvalidGeneratorField {
1537 bus: g.bus,
1538 field: "voltage_setpoint_pu",
1539 value: g.voltage_setpoint_pu,
1540 });
1541 }
1542 for (field, value) in [("pmin", g.pmin), ("qmin", g.qmin)] {
1543 if !is_valid_lower_bound(value) {
1544 return Err(NetworkError::InvalidGeneratorField {
1545 bus: g.bus,
1546 field,
1547 value,
1548 });
1549 }
1550 }
1551 for (field, value) in [("pmax", g.pmax), ("qmax", g.qmax)] {
1552 if !is_valid_upper_bound(value) {
1553 return Err(NetworkError::InvalidGeneratorField {
1554 bus: g.bus,
1555 field,
1556 value,
1557 });
1558 }
1559 }
1560 if g.pmin > g.pmax {
1561 return Err(NetworkError::InvalidGeneratorLimits { bus: g.bus });
1562 }
1563 if g.qmin > g.qmax {
1564 return Err(NetworkError::InvalidGeneratorReactiveLimits { bus: g.bus });
1565 }
1566 if let Some(apf) = g.agc_participation_factor {
1567 if !apf.is_finite() || apf < 0.0 {
1568 return Err(NetworkError::InvalidGeneratorField {
1569 bus: g.bus,
1570 field: "agc_participation_factor",
1571 value: apf,
1572 });
1573 }
1574 }
1575 if let Some(forced_outage_rate) = g.forced_outage_rate {
1576 if !forced_outage_rate.is_finite() || !(0.0..=1.0).contains(&forced_outage_rate) {
1577 return Err(NetworkError::InvalidGeneratorField {
1578 bus: g.bus,
1579 field: "forced_outage_rate",
1580 value: forced_outage_rate,
1581 });
1582 }
1583 }
1584 }
1585
1586 for branch in &self.branches {
1587 if branch.tap < 0.0 {
1588 return Err(NetworkError::InvalidBranchField {
1589 from_bus: branch.from_bus,
1590 to_bus: branch.to_bus,
1591 field: "tap",
1592 value: branch.tap,
1593 });
1594 }
1595 for (field, value) in [
1596 ("r", branch.r),
1597 ("x", branch.x),
1598 ("b", branch.b),
1599 ("g_pi", branch.g_pi),
1600 ("tap", branch.tap),
1601 ("phase_shift_rad", branch.phase_shift_rad),
1602 ("g_mag", branch.g_mag),
1603 ("b_mag", branch.b_mag),
1604 ] {
1605 if !value.is_finite() {
1606 return Err(NetworkError::InvalidBranchField {
1607 from_bus: branch.from_bus,
1608 to_bus: branch.to_bus,
1609 field,
1610 value,
1611 });
1612 }
1613 }
1614 for (field, value) in [
1615 ("rating_a_mva", branch.rating_a_mva),
1616 ("rating_b_mva", branch.rating_b_mva),
1617 ("rating_c_mva", branch.rating_c_mva),
1618 ] {
1619 if !is_valid_upper_bound(value) {
1620 return Err(NetworkError::InvalidBranchField {
1621 from_bus: branch.from_bus,
1622 to_bus: branch.to_bus,
1623 field,
1624 value,
1625 });
1626 }
1627 }
1628 if let (Some(min), Some(max)) = (branch.angle_diff_min_rad, branch.angle_diff_max_rad) {
1629 if !is_valid_lower_bound(min) || !is_valid_upper_bound(max) || min > max {
1630 return Err(NetworkError::InvalidBranchAngleBounds {
1631 from_bus: branch.from_bus,
1632 to_bus: branch.to_bus,
1633 min_rad: branch.angle_diff_min_rad,
1634 max_rad: branch.angle_diff_max_rad,
1635 });
1636 }
1637 }
1638 if let Some(min) = branch.angle_diff_min_rad {
1639 if !is_valid_lower_bound(min) {
1640 return Err(NetworkError::InvalidBranchField {
1641 from_bus: branch.from_bus,
1642 to_bus: branch.to_bus,
1643 field: "angle_diff_min_rad",
1644 value: min,
1645 });
1646 }
1647 }
1648 if let Some(max) = branch.angle_diff_max_rad {
1649 if !is_valid_upper_bound(max) {
1650 return Err(NetworkError::InvalidBranchField {
1651 from_bus: branch.from_bus,
1652 to_bus: branch.to_bus,
1653 field: "angle_diff_max_rad",
1654 value: max,
1655 });
1656 }
1657 }
1658 }
1659
1660 Ok(())
1661 }
1662
1663 fn validate_numerics_for_dc_solve(&self) -> Result<(), NetworkError> {
1664 for bus in &self.buses {
1665 for (field, value) in [
1666 ("shunt_conductance_mw", bus.shunt_conductance_mw),
1667 ("shunt_susceptance_mvar", bus.shunt_susceptance_mvar),
1668 ] {
1669 if !value.is_finite() {
1670 return Err(NetworkError::InvalidBusField {
1671 bus: bus.number,
1672 field,
1673 value,
1674 });
1675 }
1676 }
1677 }
1678
1679 for load in &self.loads {
1680 for (field, value) in [
1681 ("active_power_demand_mw", load.active_power_demand_mw),
1682 (
1683 "reactive_power_demand_mvar",
1684 load.reactive_power_demand_mvar,
1685 ),
1686 ] {
1687 if !value.is_finite() {
1688 return Err(NetworkError::InvalidLoadField {
1689 bus: load.bus,
1690 field,
1691 value,
1692 });
1693 }
1694 }
1695 }
1696
1697 for injection in &self.power_injections {
1698 for (field, value) in [
1699 (
1700 "active_power_injection_mw",
1701 injection.active_power_injection_mw,
1702 ),
1703 (
1704 "reactive_power_injection_mvar",
1705 injection.reactive_power_injection_mvar,
1706 ),
1707 ] {
1708 if !value.is_finite() {
1709 return Err(NetworkError::InvalidPowerInjectionField {
1710 bus: injection.bus,
1711 field,
1712 value,
1713 });
1714 }
1715 }
1716 }
1717
1718 for g in &self.generators {
1719 if !g.p.is_finite() {
1720 return Err(NetworkError::InvalidGeneratorField {
1721 bus: g.bus,
1722 field: "p",
1723 value: g.p,
1724 });
1725 }
1726 for (field, value) in [("pmin", g.pmin)] {
1727 if !is_valid_lower_bound(value) {
1728 return Err(NetworkError::InvalidGeneratorField {
1729 bus: g.bus,
1730 field,
1731 value,
1732 });
1733 }
1734 }
1735 for (field, value) in [("pmax", g.pmax)] {
1736 if !is_valid_upper_bound(value) {
1737 return Err(NetworkError::InvalidGeneratorField {
1738 bus: g.bus,
1739 field,
1740 value,
1741 });
1742 }
1743 }
1744 if g.pmin > g.pmax {
1745 return Err(NetworkError::InvalidGeneratorLimits { bus: g.bus });
1746 }
1747 if let Some(apf) = g.agc_participation_factor {
1748 if !apf.is_finite() || apf < 0.0 {
1749 return Err(NetworkError::InvalidGeneratorField {
1750 bus: g.bus,
1751 field: "agc_participation_factor",
1752 value: apf,
1753 });
1754 }
1755 }
1756 }
1757
1758 for branch in &self.branches {
1759 if branch.tap < 0.0 {
1760 return Err(NetworkError::InvalidBranchField {
1761 from_bus: branch.from_bus,
1762 to_bus: branch.to_bus,
1763 field: "tap",
1764 value: branch.tap,
1765 });
1766 }
1767 for (field, value) in [
1768 ("x", branch.x),
1769 ("tap", branch.tap),
1770 ("phase_shift_rad", branch.phase_shift_rad),
1771 ] {
1772 if !value.is_finite() {
1773 return Err(NetworkError::InvalidBranchField {
1774 from_bus: branch.from_bus,
1775 to_bus: branch.to_bus,
1776 field,
1777 value,
1778 });
1779 }
1780 }
1781 for (field, value) in [
1782 ("rating_a_mva", branch.rating_a_mva),
1783 ("rating_b_mva", branch.rating_b_mva),
1784 ("rating_c_mva", branch.rating_c_mva),
1785 ] {
1786 if !is_valid_upper_bound(value) {
1787 return Err(NetworkError::InvalidBranchField {
1788 from_bus: branch.from_bus,
1789 to_bus: branch.to_bus,
1790 field,
1791 value,
1792 });
1793 }
1794 }
1795 }
1796
1797 Ok(())
1798 }
1799
1800 pub fn canonicalize_branch_circuit_ids(&mut self) {
1806 let mut used = HashSet::new();
1807 let mut next_suffix_by_key: HashMap<(u32, u32, String), usize> = HashMap::new();
1808
1809 for branch in &mut self.branches {
1810 let base_circuit = {
1811 let trimmed = branch.circuit.trim();
1812 if trimmed.is_empty() {
1813 "1".to_string()
1814 } else {
1815 trimmed.to_string()
1816 }
1817 };
1818 let base_key =
1819 BranchEquipmentKey::new(branch.from_bus, branch.to_bus, base_circuit.clone());
1820 let counter_key = (base_key.bus_a, base_key.bus_b, base_circuit.clone());
1821
1822 if used.insert(base_key) {
1823 branch.circuit = base_circuit;
1824 next_suffix_by_key.entry(counter_key).or_insert(2);
1825 continue;
1826 }
1827
1828 let next_suffix = next_suffix_by_key.entry(counter_key).or_insert(2);
1829 loop {
1830 let candidate = format!("{base_circuit}#{}", *next_suffix);
1831 *next_suffix += 1;
1832 let candidate_key =
1833 BranchEquipmentKey::new(branch.from_bus, branch.to_bus, candidate.clone());
1834 if used.insert(candidate_key) {
1835 branch.circuit = candidate;
1836 break;
1837 }
1838 }
1839 }
1840 }
1841
1842 pub fn canonicalize_generator_ids(&mut self) {
1843 canonicalize_ids(&mut self.generators, "gen");
1844 }
1845
1846 pub fn canonicalize_load_ids(&mut self) {
1851 canonicalize_ids(&mut self.loads, "load");
1852 }
1853
1854 pub fn canonicalize_shunt_ids(&mut self) {
1859 canonicalize_ids(&mut self.fixed_shunts, "shunt");
1860 }
1861
1862 pub fn canonicalize_switched_shunt_ids(&mut self) {
1864 canonicalize_ids(&mut self.controls.switched_shunts, "switched_shunt");
1865 canonicalize_ids(&mut self.controls.switched_shunts_opf, "switched_shunt_opf");
1866 }
1867
1868 pub fn canonicalize_hvdc_converter_ids(&mut self) {
1870 self.hvdc.canonicalize_converter_ids();
1871 }
1872
1873 pub fn canonicalize_dispatchable_load_ids(&mut self) {
1879 struct DlWrapper<'a> {
1880 inner: &'a mut DispatchableLoad,
1881 }
1882 impl HasCanonicalId for DlWrapper<'_> {
1883 fn canonical_id(&self) -> &str {
1884 &self.inner.resource_id
1885 }
1886 fn set_canonical_id(&mut self, id: String) {
1887 self.inner.resource_id = id;
1888 }
1889 fn bus_number(&self) -> u32 {
1890 self.inner.bus
1891 }
1892 }
1893
1894 let mut wrappers: Vec<DlWrapper> = self
1895 .market_data
1896 .dispatchable_loads
1897 .iter_mut()
1898 .map(|dl| DlWrapper { inner: dl })
1899 .collect();
1900 canonicalize_ids(&mut wrappers, "dispatchable_load");
1901 }
1902
1903 pub fn canonicalize_runtime_identities(&mut self) {
1910 self.canonicalize_runtime_bus_types();
1911 self.canonicalize_branch_circuit_ids();
1912 self.canonicalize_generator_ids();
1913 self.canonicalize_switched_shunt_ids();
1914 self.canonicalize_hvdc_converter_ids();
1915 }
1916
1917 fn canonicalize_runtime_bus_types(&mut self) {
1918 let regulating_targets: HashSet<u32> = self
1919 .generators
1920 .iter()
1921 .filter(|generator| generator.can_voltage_regulate())
1922 .map(|generator| generator.reg_bus.unwrap_or(generator.bus))
1923 .collect();
1924
1925 for bus in &mut self.buses {
1926 if bus.bus_type == BusType::PV && !regulating_targets.contains(&bus.number) {
1927 bus.bus_type = BusType::PQ;
1928 }
1929 }
1930 }
1931
1932 pub fn bus_index_map(&self) -> HashMap<u32, usize> {
1934 self.buses
1935 .iter()
1936 .enumerate()
1937 .map(|(i, b)| (b.number, i))
1938 .collect()
1939 }
1940
1941 pub fn gen_index_by_id(&self) -> HashMap<String, usize> {
1945 let mut map = HashMap::new();
1946 for (i, g) in self.generators.iter().enumerate() {
1947 map.entry(g.id.trim().to_string()).or_insert(i);
1948 }
1949 map
1950 }
1951
1952 pub fn find_gen_index_by_id(&self, id: &str) -> Option<usize> {
1954 let canonical = id.trim();
1955 self.generators
1956 .iter()
1957 .position(|g| g.id.trim() == canonical)
1958 }
1959
1960 pub fn gen_index_map(&self) -> HashMap<(u32, Option<String>), usize> {
1966 let mut map = HashMap::new();
1967 for (i, g) in self.generators.iter().enumerate() {
1968 map.entry((g.bus, g.machine_id.clone())).or_insert(i);
1969 }
1970 map
1971 }
1972
1973 pub fn find_gen_index(&self, bus: u32, machine_id: Option<&str>) -> Option<usize> {
1979 match machine_id {
1980 Some(mid) => self
1981 .generators
1982 .iter()
1983 .position(|g| g.bus == bus && g.machine_id.as_deref().unwrap_or("1") == mid),
1984 None => self.generators.iter().position(|g| g.bus == bus),
1985 }
1986 }
1987
1988 pub fn branch_index_map(&self) -> HashMap<(u32, u32, String), usize> {
1993 let mut map = HashMap::new();
1994 for (i, b) in self.branches.iter().enumerate() {
1995 map.entry((b.from_bus, b.to_bus, b.circuit.clone()))
1996 .or_insert(i);
1997 }
1998 map
1999 }
2000
2001 pub fn find_branch_index(&self, from_bus: u32, to_bus: u32, circuit: &str) -> Option<usize> {
2005 self.branches.iter().position(|branch| {
2006 let same_direction = branch.from_bus == from_bus && branch.to_bus == to_bus;
2007 let reverse_direction = branch.from_bus == to_bus && branch.to_bus == from_bus;
2008 (same_direction || reverse_direction) && branch.circuit == circuit
2009 })
2010 }
2011
2012 pub fn load_index_by_id(&self) -> HashMap<String, usize> {
2016 let mut map = HashMap::new();
2017 for (i, load) in self.loads.iter().enumerate() {
2018 map.entry(load.id.clone()).or_insert(i);
2019 }
2020 map
2021 }
2022
2023 pub fn find_load_index_by_id(&self, id: &str) -> Option<usize> {
2025 self.loads.iter().position(|load| load.id == id)
2026 }
2027
2028 pub fn find_load_index(&self, bus: u32, id: Option<&str>) -> Option<usize> {
2032 match id {
2033 Some(load_id) => self
2034 .loads
2035 .iter()
2036 .position(|load| load.bus == bus && load.id == load_id),
2037 None => self.loads.iter().position(|load| load.bus == bus),
2038 }
2039 }
2040
2041 pub fn shunt_index_by_id(&self) -> HashMap<String, usize> {
2045 let mut map = HashMap::new();
2046 for (i, shunt) in self.fixed_shunts.iter().enumerate() {
2047 map.entry(shunt.id.clone()).or_insert(i);
2048 }
2049 map
2050 }
2051
2052 pub fn find_shunt_index_by_id(&self, id: &str) -> Option<usize> {
2054 self.fixed_shunts.iter().position(|shunt| shunt.id == id)
2055 }
2056
2057 pub fn find_shunt_index(&self, bus: u32, id: Option<&str>) -> Option<usize> {
2061 match id {
2062 Some(shunt_id) => self
2063 .fixed_shunts
2064 .iter()
2065 .position(|shunt| shunt.bus == bus && shunt.id == shunt_id),
2066 None => self.fixed_shunts.iter().position(|shunt| shunt.bus == bus),
2067 }
2068 }
2069
2070 pub fn find_hvdc_link_index_by_name(&self, name: &str) -> Option<usize> {
2072 self.hvdc.links.iter().position(|link| link.name() == name)
2073 }
2074
2075 fn validate_interface_definitions(&self) -> Result<(), NetworkError> {
2076 let existing_branches: HashSet<_> = self
2077 .branches
2078 .iter()
2079 .map(crate::network::BranchRef::from)
2080 .collect();
2081 for interface in &self.interfaces {
2082 if interface.members.is_empty() {
2083 return Err(NetworkError::InvalidInterfaceDefinition {
2084 name: interface.name.clone(),
2085 detail: "interface has no weighted branch members".to_string(),
2086 });
2087 }
2088 for member in &interface.members {
2089 if !member.coefficient.is_finite() {
2090 return Err(NetworkError::InvalidInterfaceDefinition {
2091 name: interface.name.clone(),
2092 detail: format!(
2093 "interface member ({}, {}, {}) has non-finite coefficient {}",
2094 member.branch.from_bus,
2095 member.branch.to_bus,
2096 member.branch.circuit,
2097 member.coefficient
2098 ),
2099 });
2100 }
2101 if !existing_branches.contains(&member.branch) {
2102 return Err(NetworkError::InvalidInterfaceDefinition {
2103 name: interface.name.clone(),
2104 detail: format!(
2105 "interface references missing branch ({}, {}, {})",
2106 member.branch.from_bus, member.branch.to_bus, member.branch.circuit
2107 ),
2108 });
2109 }
2110 }
2111 }
2112
2113 for flowgate in &self.flowgates {
2114 if flowgate.monitored.is_empty() && flowgate.contingency_branch.is_none() {
2115 return Err(NetworkError::InvalidFlowgateDefinition {
2116 name: flowgate.name.clone(),
2117 detail: "flowgate has neither monitored members nor a contingency branch"
2118 .to_string(),
2119 });
2120 }
2121 for member in &flowgate.monitored {
2122 if !member.coefficient.is_finite() {
2123 return Err(NetworkError::InvalidFlowgateDefinition {
2124 name: flowgate.name.clone(),
2125 detail: format!(
2126 "flowgate monitored branch ({}, {}, {}) has non-finite coefficient {}",
2127 member.branch.from_bus,
2128 member.branch.to_bus,
2129 member.branch.circuit,
2130 member.coefficient
2131 ),
2132 });
2133 }
2134 if !existing_branches.contains(&member.branch) {
2135 return Err(NetworkError::InvalidFlowgateDefinition {
2136 name: flowgate.name.clone(),
2137 detail: format!(
2138 "flowgate references missing monitored branch ({}, {}, {})",
2139 member.branch.from_bus, member.branch.to_bus, member.branch.circuit
2140 ),
2141 });
2142 }
2143 }
2144 if let Some(branch) = &flowgate.contingency_branch
2145 && !existing_branches.contains(branch)
2146 {
2147 return Err(NetworkError::InvalidFlowgateDefinition {
2148 name: flowgate.name.clone(),
2149 detail: format!(
2150 "flowgate references missing contingency branch ({}, {}, {})",
2151 branch.from_bus, branch.to_bus, branch.circuit
2152 ),
2153 });
2154 }
2155 }
2156
2157 Ok(())
2158 }
2159
2160 fn validate_internal_control_indices(&self) -> Result<(), NetworkError> {
2161 let bus_numbers: HashSet<u32> = self.buses.iter().map(|bus| bus.number).collect();
2162
2163 for shunt in &self.controls.switched_shunts {
2164 if !bus_numbers.contains(&shunt.bus) {
2165 return Err(NetworkError::InvalidSwitchedShuntBus {
2166 id: shunt.id.clone(),
2167 bus: shunt.bus,
2168 });
2169 }
2170 if !bus_numbers.contains(&shunt.bus_regulated) {
2171 return Err(NetworkError::InvalidSwitchedShuntRegulatedBus {
2172 id: shunt.id.clone(),
2173 bus: shunt.bus_regulated,
2174 });
2175 }
2176 }
2177
2178 for shunt in &self.controls.switched_shunts_opf {
2179 if !bus_numbers.contains(&shunt.bus) {
2180 return Err(NetworkError::InvalidSwitchedShuntOpfBus {
2181 id: shunt.id.clone(),
2182 bus: shunt.bus,
2183 });
2184 }
2185 }
2186
2187 Ok(())
2188 }
2189
2190 fn validate_hvdc_structure(&self, bus_numbers: &HashSet<u32>) -> Result<(), NetworkError> {
2191 let mut link_names = HashSet::new();
2192 for link in &self.hvdc.links {
2193 let name = link.name().trim().to_string();
2194 if !name.is_empty() && !link_names.insert(name.clone()) {
2195 return Err(NetworkError::DuplicateHvdcLinkName { name });
2196 }
2197
2198 match link {
2199 crate::network::HvdcLink::Lcc(link) => {
2200 for terminal in [&link.rectifier, &link.inverter] {
2201 if !bus_numbers.contains(&terminal.bus) {
2202 return Err(NetworkError::InvalidHvdcLinkEndpoint {
2203 name: link.name.clone(),
2204 bus: terminal.bus,
2205 });
2206 }
2207 }
2208 }
2209 crate::network::HvdcLink::Vsc(link) => {
2210 for terminal in [&link.converter1, &link.converter2] {
2211 if !bus_numbers.contains(&terminal.bus) {
2212 return Err(NetworkError::InvalidHvdcLinkEndpoint {
2213 name: link.name.clone(),
2214 bus: terminal.bus,
2215 });
2216 }
2217 }
2218 }
2219 }
2220 }
2221
2222 let mut grid_ids = HashSet::new();
2223 for grid in self.hvdc.dc_grids.iter().filter(|grid| !grid.is_empty()) {
2224 if !grid_ids.insert(grid.id) {
2225 return Err(NetworkError::DuplicateDcGridId { id: grid.id });
2226 }
2227
2228 let mut dc_bus_ids = HashSet::new();
2229 for bus in &grid.buses {
2230 if !dc_bus_ids.insert(bus.bus_id) {
2231 return Err(NetworkError::DuplicateDcBusId {
2232 grid_id: grid.id,
2233 bus_id: bus.bus_id,
2234 });
2235 }
2236 }
2237
2238 for converter in &grid.converters {
2239 if !bus_numbers.contains(&converter.ac_bus()) {
2240 return Err(NetworkError::InvalidDcConverterAcBus {
2241 grid_id: grid.id,
2242 ac_bus: converter.ac_bus(),
2243 });
2244 }
2245 if !dc_bus_ids.contains(&converter.dc_bus()) {
2246 return Err(NetworkError::InvalidDcConverterDcBus {
2247 grid_id: grid.id,
2248 dc_bus: converter.dc_bus(),
2249 });
2250 }
2251 }
2252
2253 for branch in &grid.branches {
2254 if !dc_bus_ids.contains(&branch.from_bus) {
2255 return Err(NetworkError::InvalidDcBranchEndpoint {
2256 grid_id: grid.id,
2257 from_bus: branch.from_bus,
2258 to_bus: branch.to_bus,
2259 missing_bus: branch.from_bus,
2260 });
2261 }
2262 if !dc_bus_ids.contains(&branch.to_bus) {
2263 return Err(NetworkError::InvalidDcBranchEndpoint {
2264 grid_id: grid.id,
2265 from_bus: branch.from_bus,
2266 to_bus: branch.to_bus,
2267 missing_bus: branch.to_bus,
2268 });
2269 }
2270 }
2271 }
2272
2273 Ok(())
2274 }
2275
2276 fn validate_hvdc_solve_contract(&self) -> Result<(), NetworkError> {
2277 if self.hvdc.has_point_to_point_links() && self.hvdc.has_explicit_dc_topology() {
2278 return Err(NetworkError::MixedHvdcRepresentation);
2279 }
2280 Ok(())
2281 }
2282
2283 pub fn find_pumped_hydro_index_by_name(&self, name: &str) -> Option<usize> {
2285 self.market_data
2286 .pumped_hydro_units
2287 .iter()
2288 .position(|unit| unit.name == name)
2289 }
2290
2291 pub fn find_dispatchable_load_index(
2293 &self,
2294 resource_id: &str,
2295 bus: Option<u32>,
2296 ) -> Option<usize> {
2297 self.market_data
2298 .dispatchable_loads
2299 .iter()
2300 .enumerate()
2301 .find_map(|(index, resource)| {
2302 if resource.resource_id != resource_id {
2303 return None;
2304 }
2305 if bus.is_none_or(|value| resource.bus == value) {
2306 Some(index)
2307 } else {
2308 None
2309 }
2310 })
2311 }
2312
2313 pub fn find_combined_cycle_index_by_name(&self, name: &str) -> Option<usize> {
2315 self.market_data
2316 .combined_cycle_plants
2317 .iter()
2318 .position(|plant| plant.name == name)
2319 }
2320
2321 pub fn slack_bus_index(&self) -> Option<usize> {
2329 self.buses.iter().position(|b| b.bus_type == BusType::Slack)
2330 }
2331
2332 pub fn slack_buses(&self) -> Vec<&Bus> {
2338 self.buses
2339 .iter()
2340 .filter(|b| b.bus_type == BusType::Slack)
2341 .collect()
2342 }
2343
2344 pub fn agc_participation_by_bus(&self) -> Vec<(usize, f64)> {
2351 let bus_map = self.bus_index_map();
2352 let mut by_bus = vec![0.0f64; self.n_buses()];
2353 for g in self.generators.iter().filter(|g| g.in_service) {
2354 if let Some(apf) = g.agc_participation_factor {
2355 if apf > 0.0 && apf.is_finite() {
2356 if let Some(&idx) = bus_map.get(&g.bus) {
2357 by_bus[idx] += apf;
2358 }
2359 }
2360 }
2361 }
2362 by_bus
2363 .into_iter()
2364 .enumerate()
2365 .filter(|&(_, w)| w > 0.0)
2366 .collect()
2367 }
2368
2369 pub fn bus_load_p_mw(&self) -> Vec<f64> {
2372 self.bus_load_p_mw_with_map(&self.bus_index_map())
2373 }
2374
2375 pub fn bus_load_q_mvar(&self) -> Vec<f64> {
2378 self.bus_load_q_mvar_with_map(&self.bus_index_map())
2379 }
2380
2381 pub fn bus_load_p_mw_with_map(&self, bus_map: &HashMap<u32, usize>) -> Vec<f64> {
2383 let mut demand = vec![0.0; self.buses.len()];
2384 for load in &self.loads {
2385 if load.in_service {
2386 if let Some(&idx) = bus_map.get(&load.bus) {
2387 demand[idx] += load.active_power_demand_mw;
2388 }
2389 }
2390 }
2391 for injection in &self.power_injections {
2392 if injection.in_service {
2393 if let Some(&idx) = bus_map.get(&injection.bus) {
2394 demand[idx] -= injection.active_power_injection_mw;
2395 }
2396 }
2397 }
2398 demand
2399 }
2400
2401 pub fn bus_load_q_mvar_with_map(&self, bus_map: &HashMap<u32, usize>) -> Vec<f64> {
2403 let mut demand = vec![0.0; self.buses.len()];
2404 for load in &self.loads {
2405 if load.in_service {
2406 if let Some(&idx) = bus_map.get(&load.bus) {
2407 demand[idx] += load.reactive_power_demand_mvar;
2408 }
2409 }
2410 }
2411 for injection in &self.power_injections {
2412 if injection.in_service {
2413 if let Some(&idx) = bus_map.get(&injection.bus) {
2414 demand[idx] -= injection.reactive_power_injection_mvar;
2415 }
2416 }
2417 }
2418 demand
2419 }
2420
2421 pub fn bus_p_injection_pu(&self) -> Vec<f64> {
2428 self.bus_p_injection_pu_with_map(&self.bus_index_map())
2429 }
2430
2431 pub fn bus_p_injection_pu_with_map(&self, bus_map: &HashMap<u32, usize>) -> Vec<f64> {
2433 let n = self.buses.len();
2434 let demand = self.bus_load_p_mw_with_map(bus_map);
2435 let mut p_inj = vec![0.0; n];
2436
2437 for (i, d) in demand.iter().enumerate() {
2438 p_inj[i] -= d / self.base_mva;
2439 }
2440
2441 for g in &self.generators {
2442 if g.in_service
2443 && let Some(&idx) = bus_map.get(&g.bus)
2444 {
2445 p_inj[idx] += g.p / self.base_mva;
2446 }
2447 }
2448
2449 p_inj
2450 }
2451
2452 pub fn bus_q_injection_pu(&self) -> Vec<f64> {
2458 self.bus_q_injection_pu_with_map(&self.bus_index_map())
2459 }
2460
2461 pub fn bus_q_injection_pu_with_map(&self, bus_map: &HashMap<u32, usize>) -> Vec<f64> {
2463 let n = self.buses.len();
2464 let demand = self.bus_load_q_mvar_with_map(bus_map);
2465 let mut q_inj = vec![0.0; n];
2466
2467 for (i, d) in demand.iter().enumerate() {
2468 q_inj[i] -= d / self.base_mva;
2469 }
2470
2471 for g in &self.generators {
2472 if g.in_service
2473 && let Some(&idx) = bus_map.get(&g.bus)
2474 {
2475 q_inj[idx] += g.q / self.base_mva;
2476 }
2477 }
2478
2479 q_inj
2480 }
2481
2482 pub fn total_generation_mw(&self) -> f64 {
2484 self.generators
2485 .iter()
2486 .filter(|g| g.in_service)
2487 .map(|g| g.p)
2488 .sum()
2489 }
2490
2491 pub fn total_load_mw(&self) -> f64 {
2493 self.loads
2494 .iter()
2495 .filter(|l| l.in_service)
2496 .map(|l| l.active_power_demand_mw)
2497 .sum()
2498 }
2499
2500 pub fn rebuild_bus_state_from_explicit_equipment(&mut self) {
2510 self.rebuild_bus_state_from_explicit_equipment_with_map(&self.bus_index_map());
2511 }
2512
2513 pub fn rebuild_bus_state_from_explicit_equipment_with_map(
2515 &mut self,
2516 bus_map: &HashMap<u32, usize>,
2517 ) {
2518 for bus in &mut self.buses {
2519 bus.shunt_conductance_mw = 0.0;
2520 bus.shunt_susceptance_mvar = 0.0;
2521 }
2522
2523 for shunt in &self.fixed_shunts {
2524 if shunt.in_service
2525 && let Some(&idx) = bus_map.get(&shunt.bus)
2526 {
2527 self.buses[idx].shunt_conductance_mw += shunt.g_mw;
2528 self.buses[idx].shunt_susceptance_mvar += shunt.b_mvar;
2529 }
2530 }
2531 }
2532
2533 pub fn scale_loads(&mut self, factor: f64, area: Option<u32>) {
2538 self.scale_loads_with_map(factor, area, &self.bus_index_map());
2539 }
2540
2541 pub fn scale_loads_with_map(
2543 &mut self,
2544 factor: f64,
2545 area: Option<u32>,
2546 bus_map: &HashMap<u32, usize>,
2547 ) {
2548 let bus_area: Vec<u32> = self.buses.iter().map(|b| b.area).collect();
2549 for load in &mut self.loads {
2550 if let Some(a) = area {
2551 if let Some(&idx) = bus_map.get(&load.bus) {
2552 if bus_area.get(idx).copied() != Some(a) {
2553 continue;
2554 }
2555 }
2556 }
2557 load.active_power_demand_mw *= factor;
2558 load.reactive_power_demand_mvar *= factor;
2559 }
2560 }
2561
2562 pub fn scale_generation(&mut self, factor: f64, area: Option<u32>) {
2568 self.scale_generation_with_map(factor, area, &self.bus_index_map());
2569 }
2570
2571 pub fn scale_generation_with_map(
2573 &mut self,
2574 factor: f64,
2575 area: Option<u32>,
2576 bus_map: &HashMap<u32, usize>,
2577 ) {
2578 let bus_area: Vec<u32> = self.buses.iter().map(|b| b.area).collect();
2579 for g in &mut self.generators {
2580 if !g.in_service {
2581 continue;
2582 }
2583 if let Some(a) = area {
2584 if let Some(&idx) = bus_map.get(&g.bus) {
2585 if bus_area.get(idx).copied() != Some(a) {
2586 continue;
2587 }
2588 }
2589 }
2590 g.p = (g.p * factor).clamp(g.pmin, g.pmax);
2591 }
2592 }
2593
2594 pub fn set_branch_sequence(
2598 &mut self,
2599 from_bus: u32,
2600 to_bus: u32,
2601 circuit: &str,
2602 r0: f64,
2603 x0: f64,
2604 b0: f64,
2605 ) -> bool {
2606 use crate::network::ZeroSeqData;
2607 for br in &mut self.branches {
2608 let matched = (br.from_bus == from_bus && br.to_bus == to_bus && br.circuit == circuit)
2609 || (br.from_bus == to_bus && br.to_bus == from_bus && br.circuit == circuit);
2610 if matched {
2611 let zs = br.zero_seq.get_or_insert_with(ZeroSeqData::default);
2612 zs.r0 = r0;
2613 zs.x0 = x0;
2614 zs.b0 = b0;
2615 return true;
2616 }
2617 }
2618 false
2619 }
2620
2621 pub fn get_branch_sequence(
2626 &self,
2627 from_bus: u32,
2628 to_bus: u32,
2629 circuit: &str,
2630 ) -> Option<(f64, f64, f64)> {
2631 for br in &self.branches {
2632 let matched = (br.from_bus == from_bus && br.to_bus == to_bus && br.circuit == circuit)
2633 || (br.from_bus == to_bus && br.to_bus == from_bus && br.circuit == circuit);
2634 if matched {
2635 return br.zero_seq.as_ref().map(|zs| (zs.r0, zs.x0, zs.b0));
2636 }
2637 }
2638 None
2639 }
2640}
2641
2642#[cfg(test)]
2643mod tests {
2644 use super::*;
2645 use crate::network::{
2646 Branch, Bus, BusType, FixedShunt, Generator, GeneratorRef, Load, StorageParams,
2647 };
2648
2649 fn make_3bus_network() -> Network {
2655 let mut net = Network::new("test-3bus");
2656
2657 let bus1 = Bus::new(1, BusType::Slack, 138.0);
2658 let bus2 = Bus::new(2, BusType::PQ, 138.0);
2659 let bus3 = Bus::new(3, BusType::PV, 138.0);
2660
2661 net.buses.push(bus1);
2662 net.buses.push(bus2);
2663 net.buses.push(bus3);
2664
2665 net.loads.push(Load::new(2, 50.0, 0.0));
2667 net.loads.push(Load::new(3, 30.0, 0.0));
2668
2669 net.branches.push(Branch::new_line(1, 2, 0.01, 0.1, 0.02));
2670 net.branches.push(Branch::new_line(2, 3, 0.02, 0.2, 0.04));
2671
2672 net.generators.push(Generator::new(1, 90.0, 1.0));
2673 net.generators.push(Generator::new(3, 20.0, 1.0));
2674 net.canonicalize_generator_ids();
2675
2676 net
2677 }
2678
2679 #[test]
2684 fn test_n_buses() {
2685 let net = make_3bus_network();
2686 assert_eq!(net.n_buses(), 3);
2687 }
2688
2689 #[test]
2690 fn test_n_branches() {
2691 let net = make_3bus_network();
2692 assert_eq!(net.n_branches(), 2);
2693 }
2694
2695 #[test]
2696 fn test_n_generators() {
2697 let net = make_3bus_network();
2698 assert_eq!(net.n_generators(), 2);
2699 }
2700
2701 #[test]
2702 fn test_n_generators_in_service_excludes_out_of_service() {
2703 let mut net = make_3bus_network();
2704 net.generators[1].in_service = false;
2706 assert_eq!(net.n_generators(), 2, "n_generators returns total count");
2707 assert_eq!(
2708 net.n_generators_in_service(),
2709 1,
2710 "n_generators_in_service should only count in-service generators"
2711 );
2712 }
2713
2714 #[test]
2719 fn test_bus_index_map_correctness() {
2720 let net = make_3bus_network();
2721 let map = net.bus_index_map();
2722 assert_eq!(map.len(), 3);
2723 assert_eq!(map[&1], 0);
2724 assert_eq!(map[&2], 1);
2725 assert_eq!(map[&3], 2);
2726 }
2727
2728 #[test]
2729 fn test_bus_index_map_non_contiguous_bus_numbers() {
2730 let mut net = Network::new("non-contiguous");
2731 net.buses.push(Bus::new(10, BusType::Slack, 138.0));
2732 net.buses.push(Bus::new(50, BusType::PQ, 138.0));
2733 net.buses.push(Bus::new(99, BusType::PQ, 138.0));
2734
2735 let map = net.bus_index_map();
2736 assert_eq!(map.len(), 3);
2737 assert_eq!(map[&10], 0);
2738 assert_eq!(map[&50], 1);
2739 assert_eq!(map[&99], 2);
2740 }
2741
2742 #[test]
2747 fn test_slack_bus_index() {
2748 let net = make_3bus_network();
2749 assert_eq!(
2750 net.slack_bus_index(),
2751 Some(0),
2752 "Bus 1 is Slack and at index 0"
2753 );
2754 }
2755
2756 #[test]
2757 fn test_slack_bus_index_no_slack() {
2758 let mut net = Network::new("no-slack");
2759 net.buses.push(Bus::new(1, BusType::PQ, 138.0));
2760 net.buses.push(Bus::new(2, BusType::PV, 138.0));
2761 assert_eq!(
2762 net.slack_bus_index(),
2763 None,
2764 "No slack bus should return None"
2765 );
2766 }
2767
2768 #[test]
2769 fn test_slack_bus_index_slack_not_first() {
2770 let mut net = Network::new("slack-last");
2771 net.buses.push(Bus::new(1, BusType::PQ, 138.0));
2772 net.buses.push(Bus::new(2, BusType::PQ, 138.0));
2773 net.buses.push(Bus::new(3, BusType::Slack, 138.0));
2774 assert_eq!(
2775 net.slack_bus_index(),
2776 Some(2),
2777 "Slack bus at the end should return index 2"
2778 );
2779 }
2780
2781 #[test]
2786 fn test_total_generation_mw() {
2787 let net = make_3bus_network();
2788 assert!(
2790 (net.total_generation_mw() - 110.0).abs() < 1e-10,
2791 "total generation should be 110 MW; got {}",
2792 net.total_generation_mw()
2793 );
2794 }
2795
2796 #[test]
2797 fn test_total_generation_mw_excludes_offline() {
2798 let mut net = make_3bus_network();
2799 net.generators[0].in_service = false;
2800 assert!(
2801 (net.total_generation_mw() - 20.0).abs() < 1e-10,
2802 "offline gen should not count; got {}",
2803 net.total_generation_mw()
2804 );
2805 }
2806
2807 #[test]
2808 fn test_total_load_mw() {
2809 let net = make_3bus_network();
2810 assert!(
2812 (net.total_load_mw() - 80.0).abs() < 1e-10,
2813 "total load should be 80 MW; got {}",
2814 net.total_load_mw()
2815 );
2816 }
2817
2818 #[test]
2823 fn test_empty_network() {
2824 let net = Network::new("empty");
2825 assert_eq!(net.n_buses(), 0);
2826 assert_eq!(net.n_branches(), 0);
2827 assert_eq!(net.n_generators(), 0);
2828 assert_eq!(net.slack_bus_index(), None);
2829 assert!((net.total_generation_mw()).abs() < 1e-10);
2830 assert!((net.total_load_mw()).abs() < 1e-10);
2831 assert!(net.bus_index_map().is_empty());
2832 }
2833
2834 #[test]
2835 fn test_empty_network_bus_p_injection() {
2836 let net = Network::new("empty");
2837 let p_inj = net.bus_p_injection_pu();
2838 assert!(
2839 p_inj.is_empty(),
2840 "empty network should have empty injection vector"
2841 );
2842 }
2843
2844 #[test]
2845 fn test_validate_rejects_invalid_storage_parameters() {
2846 let mut net = make_3bus_network();
2847 net.generators[0].storage = Some(StorageParams {
2848 charge_efficiency: 1.2,
2849 ..StorageParams::with_energy_capacity_mwh(50.0)
2850 });
2851
2852 let err = net.validate().unwrap_err();
2853 assert!(matches!(
2854 err,
2855 NetworkError::InvalidStorageParameters { bus: 1, .. }
2856 ));
2857 }
2858
2859 #[test]
2860 fn test_validate_rejects_load_on_missing_bus() {
2861 let mut net = make_3bus_network();
2862 net.loads.push(Load::new(99, 10.0, 2.0));
2863
2864 let err = net.validate().unwrap_err();
2865 assert!(matches!(err, NetworkError::InvalidLoadBus(99)));
2866 }
2867
2868 #[test]
2869 fn test_validate_rejects_power_injection_on_missing_bus() {
2870 let mut net = make_3bus_network();
2871 net.power_injections.push(PowerInjection {
2872 bus: 99,
2873 id: "inj_missing".into(),
2874 kind: crate::network::power_injection::PowerInjectionKind::Other,
2875 active_power_injection_mw: 5.0,
2876 reactive_power_injection_mvar: 1.0,
2877 in_service: true,
2878 });
2879
2880 let err = net.validate().unwrap_err();
2881 assert!(matches!(err, NetworkError::InvalidPowerInjectionBus(99)));
2882 }
2883
2884 #[test]
2885 fn test_validate_rejects_fixed_shunt_on_missing_bus() {
2886 let mut net = make_3bus_network();
2887 net.fixed_shunts.push(FixedShunt {
2888 bus: 99,
2889 id: "sh_missing".into(),
2890 shunt_type: crate::network::ShuntType::Capacitor,
2891 g_mw: 0.0,
2892 b_mvar: 1.0,
2893 in_service: true,
2894 rated_kv: None,
2895 rated_mvar: None,
2896 });
2897
2898 let err = net.validate().unwrap_err();
2899 assert!(matches!(err, NetworkError::InvalidFixedShuntBus(99)));
2900 }
2901
2902 #[test]
2903 fn test_validate_rejects_dispatchable_load_on_missing_bus() {
2904 let mut net = make_3bus_network();
2905 net.market_data
2906 .dispatchable_loads
2907 .push(DispatchableLoad::curtailable(
2908 99,
2909 10.0,
2910 2.0,
2911 0.0,
2912 100.0,
2913 net.base_mva,
2914 ));
2915
2916 let err = net.validate().unwrap_err();
2917 assert!(matches!(err, NetworkError::InvalidDispatchableLoadBus(99)));
2918 }
2919
2920 #[test]
2925 fn test_bus_p_injection_pu() {
2926 let net = make_3bus_network();
2927 let p_inj = net.bus_p_injection_pu();
2928 assert_eq!(p_inj.len(), 3);
2932 assert!(
2933 (p_inj[0] - 0.9).abs() < 1e-10,
2934 "bus 1 p_inj: expected 0.9, got {}",
2935 p_inj[0]
2936 );
2937 assert!(
2938 (p_inj[1] - (-0.5)).abs() < 1e-10,
2939 "bus 2 p_inj: expected -0.5, got {}",
2940 p_inj[1]
2941 );
2942 assert!(
2943 (p_inj[2] - (-0.1)).abs() < 1e-10,
2944 "bus 3 p_inj: expected -0.1, got {}",
2945 p_inj[2]
2946 );
2947 }
2948
2949 #[test]
2954 fn test_network_new_defaults() {
2955 let net = Network::new("test-defaults");
2956 assert_eq!(net.name, "test-defaults");
2957 assert!(
2958 (net.base_mva - 100.0).abs() < 1e-10,
2959 "default base_mva should be 100"
2960 );
2961 assert!(net.buses.is_empty());
2962 assert!(net.branches.is_empty());
2963 assert!(net.generators.is_empty());
2964 assert!(net.loads.is_empty());
2965 }
2966
2967 #[test]
2972 fn test_scale_loads_all() {
2973 let mut net = make_3bus_network();
2974 net.loads.push(Load::new(2, 10.0, 5.0));
2976 net.loads.push(Load::new(3, 20.0, 10.0));
2977
2978 let old_total: f64 = net
2979 .loads
2980 .iter()
2981 .map(|l| l.active_power_demand_mw)
2982 .sum::<f64>();
2983
2984 net.scale_loads(1.5, None);
2985
2986 let new_total: f64 = net
2987 .loads
2988 .iter()
2989 .map(|l| l.active_power_demand_mw)
2990 .sum::<f64>();
2991
2992 assert!(
2993 (new_total - old_total * 1.5).abs() < 1e-10,
2994 "total Pd should scale by 1.5: old={} new={} expected={}",
2995 old_total,
2996 new_total,
2997 old_total * 1.5
2998 );
2999 }
3000
3001 #[test]
3002 fn test_scale_generation_clamped() {
3003 let mut net = make_3bus_network();
3004 net.scale_generation(10.0, None);
3009
3010 for g in &net.generators {
3011 assert!(
3012 g.p <= g.pmax,
3013 "Pg={} should be <= Pmax={} after 10x scaling",
3014 g.p,
3015 g.pmax
3016 );
3017 }
3018 }
3019
3020 #[test]
3025 fn test_set_branch_sequence_roundtrip() {
3026 let mut net = make_3bus_network();
3027 let (from, to, ckt) = {
3029 let br = &net.branches[0];
3030 (br.from_bus, br.to_bus, br.circuit.clone())
3031 };
3032 assert!(net.get_branch_sequence(from, to, &ckt).is_none());
3033
3034 assert!(net.set_branch_sequence(from, to, &ckt, 0.15, 0.45, 0.02));
3035 let (r0, x0, b0) = net.get_branch_sequence(from, to, &ckt).unwrap();
3036 assert!((r0 - 0.15).abs() < 1e-10);
3037 assert!((x0 - 0.45).abs() < 1e-10);
3038 assert!((b0 - 0.02).abs() < 1e-10);
3039
3040 let (r0r, x0r, b0r) = net.get_branch_sequence(to, from, &ckt).unwrap();
3042 assert!((r0r - 0.15).abs() < 1e-10);
3043 assert!((x0r - 0.45).abs() < 1e-10);
3044 assert!((b0r - 0.02).abs() < 1e-10);
3045 }
3046
3047 #[test]
3048 fn test_set_branch_sequence_not_found() {
3049 let mut net = make_3bus_network();
3050 assert!(!net.set_branch_sequence(99, 100, "1", 0.1, 0.2, 0.0));
3051 }
3052
3053 #[test]
3058 fn test_negative_tap_ratio_rejected_by_validate_for_solve() {
3059 let mut net = make_3bus_network();
3060 net.branches[0].tap = -0.95;
3061 let err = net.validate_for_solve().unwrap_err();
3062 assert!(matches!(
3063 err,
3064 NetworkError::InvalidBranchField { field: "tap", .. }
3065 ));
3066 }
3067
3068 #[test]
3069 fn validate_for_solve_rejects_missing_slack_in_island() {
3070 let mut net = Network::new("component-slack");
3071 net.buses = vec![
3072 Bus::new(1, BusType::Slack, 138.0),
3073 Bus::new(2, BusType::PQ, 138.0),
3074 Bus::new(3, BusType::PQ, 138.0),
3075 ];
3076 net.branches.push(Branch::new_line(1, 2, 0.01, 0.1, 0.02));
3077 net.generators.push(Generator::new(1, 50.0, 1.0));
3078 net.canonicalize_generator_ids();
3079
3080 let err = net.validate_for_solve().unwrap_err();
3081 assert!(matches!(
3082 err,
3083 NetworkError::InvalidSlackPlacement { buses, slack_buses }
3084 if buses.contains(&3) && slack_buses.is_empty()
3085 ));
3086 }
3087
3088 #[test]
3089 fn validate_for_solve_rejects_isolated_bus_with_active_equipment() {
3090 let mut net = Network::new("isolated-bus");
3091 net.buses = vec![Bus::new(1, BusType::Isolated, 138.0)];
3092 net.loads.push(Load::new(1, 5.0, 1.0));
3093
3094 let err = net.validate_for_solve().unwrap_err();
3095 assert!(matches!(
3096 err,
3097 NetworkError::InvalidIsolatedBusConnectivity { bus: 1 }
3098 ));
3099 }
3100
3101 #[test]
3102 fn validate_for_solve_rejects_isolated_bus_with_bus_shunt() {
3103 let mut net = Network::new("isolated-bus-shunt");
3104 let mut bus = Bus::new(1, BusType::Isolated, 138.0);
3105 bus.shunt_susceptance_mvar = 25.0;
3106 net.buses = vec![bus];
3107
3108 let err = net.validate_for_solve().unwrap_err();
3109 assert!(matches!(
3110 err,
3111 NetworkError::InvalidIsolatedBusConnectivity { bus: 1 }
3112 ));
3113 }
3114
3115 #[test]
3116 fn validate_for_solve_rejects_duplicate_area_schedule_numbers() {
3117 let mut net = make_3bus_network();
3118 net.area_schedules.push(AreaSchedule {
3119 number: 1,
3120 slack_bus: 1,
3121 p_desired_mw: 10.0,
3122 p_tolerance_mw: 5.0,
3123 name: "A".to_string(),
3124 });
3125 net.area_schedules.push(AreaSchedule {
3126 number: 1,
3127 slack_bus: 1,
3128 p_desired_mw: 20.0,
3129 p_tolerance_mw: 5.0,
3130 name: "B".to_string(),
3131 });
3132
3133 let err = net.validate_for_solve().unwrap_err();
3134 assert!(matches!(err, NetworkError::DuplicateAreaScheduleNumber(1)));
3135 }
3136
3137 #[test]
3138 fn validate_for_solve_rejects_invalid_area_schedule_slack_bus() {
3139 let mut net = make_3bus_network();
3140 net.area_schedules.push(AreaSchedule {
3141 number: 7,
3142 slack_bus: 99,
3143 p_desired_mw: 10.0,
3144 p_tolerance_mw: 5.0,
3145 name: "bad".to_string(),
3146 });
3147
3148 let err = net.validate_for_solve().unwrap_err();
3149 assert!(matches!(
3150 err,
3151 NetworkError::InvalidAreaScheduleSlackBus {
3152 area: 7,
3153 slack_bus: 99
3154 }
3155 ));
3156 }
3157
3158 #[test]
3159 fn validate_for_solve_allows_explicitly_unbounded_generator_limits() {
3160 let mut net = make_3bus_network();
3161 net.generators[0].qmin = f64::NEG_INFINITY;
3162 net.generators[0].qmax = f64::INFINITY;
3163 net.generators[0].pmax = f64::INFINITY;
3164
3165 net.validate_for_solve()
3166 .expect("unbounded OPF-style generator limits should be allowed");
3167 }
3168
3169 #[test]
3170 fn validate_for_solve_allows_explicitly_unbounded_branch_limits() {
3171 let mut net = make_3bus_network();
3172 net.branches[0].rating_a_mva = f64::INFINITY;
3173 net.branches[0].angle_diff_min_rad = Some(f64::NEG_INFINITY);
3174 net.branches[0].angle_diff_max_rad = Some(f64::INFINITY);
3175
3176 net.validate_for_solve()
3177 .expect("unbounded thermal and angle limits should be allowed");
3178 }
3179
3180 #[test]
3181 fn validate_for_solve_rejects_wrong_sided_infinite_limits() {
3182 let mut net = make_3bus_network();
3183 net.generators[0].qmax = f64::NEG_INFINITY;
3184 let err = net.validate_for_solve().unwrap_err();
3185 assert!(matches!(
3186 err,
3187 NetworkError::InvalidGeneratorField {
3188 field: "qmax",
3189 value,
3190 ..
3191 } if value == f64::NEG_INFINITY
3192 ));
3193 }
3194
3195 #[test]
3196 fn validate_for_solve_allows_raw_agc_weights_above_one() {
3197 let mut net = make_3bus_network();
3198 net.generators[0].agc_participation_factor = Some(2.0);
3199
3200 net.validate_for_solve()
3201 .expect("AGC participation factors are raw weights and may exceed 1.0");
3202 }
3203
3204 #[test]
3205 fn validate_accepts_network_after_generator_ids_are_canonicalized() {
3206 let mut net = make_3bus_network();
3207 net.generators[0].id.clear();
3208
3209 net.canonicalize_generator_ids();
3210 net.validate()
3211 .expect("validate should succeed after canonicalization");
3212 assert!(
3213 !net.generators[0].id.is_empty(),
3214 "canonicalization should have auto-assigned a canonical id"
3215 );
3216 assert!(
3217 net.generators[0].id.starts_with("gen_"),
3218 "canonical id should follow gen_{{bus}}_{{ordinal}} format, got: {}",
3219 net.generators[0].id
3220 );
3221 }
3222
3223 #[test]
3224 fn canonicalize_runtime_identities_demotes_pv_bus_without_active_regulator() {
3225 let mut net = Network::new("runtime-bus-type-normalization");
3226 net.buses.push(Bus::new(1, BusType::Slack, 230.0));
3227 net.buses.push(Bus::new(2, BusType::PV, 230.0));
3228 net.branches.push(Branch::new_line(1, 2, 0.01, 0.1, 0.0));
3229
3230 let mut slack_gen = Generator::new(1, 50.0, 1.0);
3231 slack_gen.id = "g1".into();
3232 net.generators.push(slack_gen);
3233
3234 let mut offline_pv_gen = Generator::new(2, 10.0, 1.02);
3235 offline_pv_gen.id = "g2".into();
3236 offline_pv_gen.in_service = false;
3237 net.generators.push(offline_pv_gen);
3238
3239 net.canonicalize_runtime_identities();
3240
3241 assert_eq!(net.buses[1].bus_type, BusType::PQ);
3242 net.validate()
3243 .expect("runtime canonicalization should produce a solve-ready network");
3244 }
3245
3246 #[test]
3247 fn validate_rejects_duplicate_generator_id() {
3248 let mut net = make_3bus_network();
3249 let duplicate_id = net.generators[0].id.clone();
3250 net.generators[1].id = duplicate_id.clone();
3251
3252 let err = net.validate().unwrap_err();
3253 assert!(matches!(
3254 err,
3255 NetworkError::DuplicateGeneratorId { id } if id == duplicate_id
3256 ));
3257 }
3258
3259 #[test]
3260 fn canonicalize_generator_ids_fills_missing_ids_deterministically() {
3261 let mut net = Network::new("canonicalize-generator-ids");
3262 net.generators
3263 .push(Generator::with_id(" explicit-a ", 10, 0.0, 1.0));
3264 net.generators.push(Generator::new(10, 0.0, 1.0));
3265 net.generators.push(Generator::new(10, 0.0, 1.0));
3266 net.generators
3267 .push(Generator::with_id("gen_10_2", 10, 0.0, 1.0));
3268 net.generators.push(Generator::new(20, 0.0, 1.0));
3269
3270 let mut clone = net.clone();
3271 net.canonicalize_generator_ids();
3272 clone.canonicalize_generator_ids();
3273
3274 let ids: Vec<&str> = net
3275 .generators
3276 .iter()
3277 .map(|generator| generator.id.as_str())
3278 .collect();
3279 assert_eq!(
3280 ids,
3281 vec![
3282 "explicit-a",
3283 "gen_10_2_2",
3284 "gen_10_3",
3285 "gen_10_2",
3286 "gen_20_1"
3287 ]
3288 );
3289 assert_eq!(
3290 ids,
3291 clone
3292 .generators
3293 .iter()
3294 .map(|generator| generator.id.as_str())
3295 .collect::<Vec<_>>()
3296 );
3297 let unique_ids: HashSet<&str> = ids.iter().copied().collect();
3298 assert_eq!(unique_ids.len(), ids.len());
3299 }
3300
3301 #[test]
3302 fn canonicalize_branch_circuit_ids_disambiguates_reverse_duplicates() {
3303 let mut net = Network::new("canonicalize-branch-circuits");
3304 net.buses.push(Bus::new(1, BusType::Slack, 230.0));
3305 net.buses.push(Bus::new(2, BusType::PQ, 230.0));
3306 net.branches.push(Branch::new_line(1, 2, 0.0, 0.1, 0.0));
3307 net.branches.push(Branch::new_line(2, 1, 0.0, 0.1, 0.0));
3308 net.branches.push(Branch::new_line(1, 2, 0.0, 0.1, 0.0));
3309
3310 net.canonicalize_branch_circuit_ids();
3311
3312 let circuits: Vec<&str> = net
3313 .branches
3314 .iter()
3315 .map(|branch| branch.circuit.as_str())
3316 .collect();
3317 assert_eq!(circuits, vec!["1", "1#2", "1#3"]);
3318 net.validate_structure()
3319 .expect("canonicalized branch circuits should be structurally valid");
3320 }
3321
3322 #[test]
3323 fn canonicalize_load_and_shunt_ids_fills_missing_ids_deterministically() {
3324 let mut net = Network::new("canonicalize-load-shunt-ids");
3325 net.loads.push(Load {
3326 bus: 3,
3327 id: String::new(),
3328 ..Default::default()
3329 });
3330 net.loads.push(Load {
3331 bus: 3,
3332 id: " load_existing ".into(),
3333 ..Default::default()
3334 });
3335 net.fixed_shunts.push(FixedShunt {
3336 bus: 7,
3337 id: String::new(),
3338 shunt_type: crate::network::ShuntType::Capacitor,
3339 g_mw: 0.0,
3340 b_mvar: 0.0,
3341 in_service: true,
3342 rated_kv: None,
3343 rated_mvar: None,
3344 });
3345 net.fixed_shunts.push(FixedShunt {
3346 bus: 7,
3347 id: " sh_existing ".into(),
3348 shunt_type: crate::network::ShuntType::Reactor,
3349 g_mw: 0.0,
3350 b_mvar: 0.0,
3351 in_service: true,
3352 rated_kv: None,
3353 rated_mvar: None,
3354 });
3355
3356 net.canonicalize_load_ids();
3357 net.canonicalize_shunt_ids();
3358
3359 assert_eq!(net.loads[0].id, "load_3_1");
3360 assert_eq!(net.loads[1].id, "load_existing");
3361 assert_eq!(net.fixed_shunts[0].id, "shunt_7_1");
3362 assert_eq!(net.fixed_shunts[1].id, "sh_existing");
3363 }
3364
3365 #[test]
3366 fn canonicalize_dispatchable_load_ids_fills_missing_ids_deterministically() {
3367 let mut net = make_3bus_network();
3368 net.market_data
3369 .dispatchable_loads
3370 .push(DispatchableLoad::curtailable(
3371 2, 150.0, 30.0, 50.0, 40.0, 100.0,
3372 ));
3373 net.market_data
3374 .dispatchable_loads
3375 .push(DispatchableLoad::curtailable(
3376 2, 100.0, 20.0, 0.0, 50.0, 100.0,
3377 ));
3378 net.market_data.dispatchable_loads[1].resource_id = " dr_existing ".into();
3379
3380 net.canonicalize_dispatchable_load_ids();
3381
3382 assert_eq!(
3383 net.market_data.dispatchable_loads[0].resource_id,
3384 "dispatchable_load_2_1"
3385 );
3386 assert_eq!(
3387 net.market_data.dispatchable_loads[1].resource_id,
3388 "dr_existing"
3389 );
3390 }
3391
3392 #[test]
3393 fn find_stable_asset_indices_by_identity() {
3394 let mut net = make_3bus_network();
3395 net.loads.push(Load {
3396 bus: 2,
3397 id: "load_2_a".into(),
3398 ..Default::default()
3399 });
3400 net.fixed_shunts.push(FixedShunt {
3401 bus: 2,
3402 id: "shunt_2_a".into(),
3403 shunt_type: crate::network::ShuntType::Capacitor,
3404 g_mw: 1.0,
3405 b_mvar: 2.0,
3406 in_service: true,
3407 rated_kv: None,
3408 rated_mvar: None,
3409 });
3410 net.hvdc
3411 .links
3412 .push(crate::network::HvdcLink::Vsc(crate::network::VscHvdcLink {
3413 name: "HVDC_A".into(),
3414 converter1: crate::network::VscConverterTerminal {
3415 bus: 1,
3416 ..Default::default()
3417 },
3418 converter2: crate::network::VscConverterTerminal {
3419 bus: 3,
3420 ..Default::default()
3421 },
3422 ..Default::default()
3423 }));
3424 net.market_data
3425 .pumped_hydro_units
3426 .push(PumpedHydroUnit::new(
3427 "PH_A".into(),
3428 GeneratorRef {
3429 bus: 1,
3430 id: "GEN_A".into(),
3431 },
3432 100.0,
3433 ));
3434 net.market_data
3435 .dispatchable_loads
3436 .push(DispatchableLoad::curtailable(
3437 2, 20.0, 5.0, 0.0, 100.0, 100.0,
3438 ));
3439 net.market_data.dispatchable_loads[0].resource_id = "dr_a".into();
3440 net.market_data
3441 .combined_cycle_plants
3442 .push(CombinedCyclePlant {
3443 id: String::new(),
3444 name: "CC_A".into(),
3445 configs: Vec::new(),
3446 transitions: Vec::new(),
3447 active_config: None,
3448 hours_in_config: 0.0,
3449 duct_firing_capable: false,
3450 });
3451
3452 assert_eq!(net.find_branch_index(1, 2, "1"), Some(0));
3453 assert_eq!(net.find_branch_index(2, 1, "1"), Some(0));
3454 assert_eq!(net.find_load_index_by_id("load_2_a"), Some(2));
3455 assert_eq!(net.find_load_index(2, Some("load_2_a")), Some(2));
3456 assert_eq!(net.find_shunt_index_by_id("shunt_2_a"), Some(0));
3457 assert_eq!(net.find_shunt_index(2, Some("shunt_2_a")), Some(0));
3458 assert_eq!(net.find_hvdc_link_index_by_name("HVDC_A"), Some(0));
3459 assert_eq!(net.find_pumped_hydro_index_by_name("PH_A"), Some(0));
3460 assert_eq!(net.find_dispatchable_load_index("dr_a", Some(2)), Some(0));
3461 assert_eq!(net.find_combined_cycle_index_by_name("CC_A"), Some(0));
3462 }
3463
3464 #[test]
3465 fn conditional_limits_apply_and_reset() {
3466 let mut net = Network::new("test");
3467 let mut br0 = Branch::new_line(1, 2, 0.01, 0.1, 0.0);
3469 br0.rating_a_mva = 100.0;
3470 br0.rating_c_mva = 150.0;
3471 let mut br1 = Branch::new_line(2, 3, 0.01, 0.1, 0.0);
3472 br1.rating_a_mva = 200.0;
3473 br1.rating_c_mva = 250.0;
3474 net.branches.push(br0);
3475 net.branches.push(br1);
3476
3477 net.conditional_limits.insert_for_branch(
3479 &net.branches[0],
3480 vec![
3481 ConditionalRating {
3482 condition_id: "summer".to_string(),
3483 rating_a_mva: 80.0,
3484 rating_c_mva: 0.0,
3485 },
3486 ConditionalRating {
3487 condition_id: "winter".to_string(),
3488 rating_a_mva: 120.0,
3489 rating_c_mva: 0.0,
3490 },
3491 ],
3492 );
3493
3494 net.apply_conditional_limits(&["summer".to_string()]);
3496 assert!(
3497 (net.branches[0].rating_a_mva - 80.0).abs() < 1e-6,
3498 "Branch 0 rate_a should be 80 after summer, got {}",
3499 net.branches[0].rating_a_mva
3500 );
3501 assert!(
3502 (net.branches[1].rating_a_mva - 200.0).abs() < 1e-6,
3503 "Branch 1 should be unchanged"
3504 );
3505
3506 net.apply_conditional_limits(&["winter".to_string()]);
3508 assert!(
3509 (net.branches[0].rating_a_mva - 120.0).abs() < 1e-6,
3510 "Branch 0 rate_a should be 120 after winter, got {}",
3511 net.branches[0].rating_a_mva
3512 );
3513
3514 net.reset_conditional_limits();
3516 assert!(
3517 (net.branches[0].rating_a_mva - 100.0).abs() < 1e-6,
3518 "Branch 0 rate_a should be 100 after reset, got {}",
3519 net.branches[0].rating_a_mva
3520 );
3521 }
3522
3523 #[test]
3524 fn conditional_limits_clear_preserves_reset_state() {
3525 let mut net = Network::new("test");
3526 let mut branch = Branch::new_line(1, 2, 0.01, 0.1, 0.0);
3527 branch.rating_a_mva = 100.0;
3528 branch.rating_c_mva = 150.0;
3529 net.branches.push(branch);
3530 net.conditional_limits.insert_for_branch(
3531 &net.branches[0],
3532 vec![ConditionalRating {
3533 condition_id: "summer".to_string(),
3534 rating_a_mva: 80.0,
3535 rating_c_mva: 120.0,
3536 }],
3537 );
3538
3539 net.apply_conditional_limits(&["summer".to_string()]);
3540 assert!((net.branches[0].rating_a_mva - 80.0).abs() < 1e-6);
3541
3542 net.conditional_limits.clear();
3543 net.reset_conditional_limits();
3544
3545 assert!((net.branches[0].rating_a_mva - 100.0).abs() < 1e-6);
3546 assert!((net.branches[0].rating_c_mva - 150.0).abs() < 1e-6);
3547 }
3548
3549 #[test]
3550 fn conditional_limits_empty_conditions_reset_to_base() {
3551 let mut net = Network::new("conditional-empty");
3552 let mut branch = Branch::new_line(1, 2, 0.01, 0.1, 0.0);
3553 branch.rating_a_mva = 100.0;
3554 branch.rating_c_mva = 150.0;
3555 net.branches.push(branch);
3556 net.conditional_limits.insert_for_branch(
3557 &net.branches[0],
3558 vec![ConditionalRating {
3559 condition_id: "summer".to_string(),
3560 rating_a_mva: 80.0,
3561 rating_c_mva: 120.0,
3562 }],
3563 );
3564
3565 net.apply_conditional_limits(&["summer".to_string()]);
3566 assert!((net.branches[0].rating_a_mva - 80.0).abs() < 1e-6);
3567
3568 net.apply_conditional_limits(&[]);
3569 assert!((net.branches[0].rating_a_mva - 100.0).abs() < 1e-6);
3570 assert!((net.branches[0].rating_c_mva - 150.0).abs() < 1e-6);
3571 }
3572
3573 #[test]
3574 fn conditional_limits_serde_roundtrip_preserves_reset_state() {
3575 let mut net = Network::new("test");
3576 let mut branch = Branch::new_line(1, 2, 0.01, 0.1, 0.0);
3577 branch.rating_a_mva = 100.0;
3578 branch.rating_c_mva = 150.0;
3579 net.branches.push(branch);
3580 net.conditional_limits.insert_for_branch(
3581 &net.branches[0],
3582 vec![ConditionalRating {
3583 condition_id: "summer".to_string(),
3584 rating_a_mva: 80.0,
3585 rating_c_mva: 120.0,
3586 }],
3587 );
3588
3589 net.apply_conditional_limits(&["summer".to_string()]);
3590 let json = serde_json::to_string(&net).unwrap();
3591 let mut roundtripped: Network = serde_json::from_str(&json).unwrap();
3592
3593 roundtripped.reset_conditional_limits();
3594
3595 assert!((roundtripped.branches[0].rating_a_mva - 100.0).abs() < 1e-6);
3596 assert!((roundtripped.branches[0].rating_c_mva - 150.0).abs() < 1e-6);
3597 }
3598
3599 #[test]
3600 fn validate_structure_rejects_reverse_direction_duplicate_branch() {
3601 let mut net = Network::new("dup-branch");
3602 net.buses.push(Bus::new(1, BusType::Slack, 230.0));
3603 net.buses.push(Bus::new(2, BusType::PQ, 230.0));
3604 net.branches.push(Branch::new_line(1, 2, 0.01, 0.1, 0.0));
3605 let mut reverse = Branch::new_line(2, 1, 0.01, 0.1, 0.0);
3606 reverse.circuit = "1".to_string();
3607 net.branches.push(reverse);
3608
3609 let err = net.validate_structure().unwrap_err();
3610 assert!(matches!(
3611 err,
3612 NetworkError::DuplicateBranchKey {
3613 from_bus: 1,
3614 to_bus: 2,
3615 ..
3616 }
3617 ));
3618 }
3619
3620 #[test]
3621 fn generator_lookup_uses_trimmed_canonical_id() {
3622 let mut net = Network::new("trimmed-generator-id");
3623 net.buses.push(Bus::new(1, BusType::Slack, 230.0));
3624 let mut generator = Generator::new(1, 50.0, 1.0);
3625 generator.id = " GEN_A ".to_string();
3626 net.generators.push(generator);
3627
3628 assert_eq!(net.find_gen_index_by_id("GEN_A"), Some(0));
3629 assert_eq!(net.find_gen_index_by_id(" GEN_A "), Some(0));
3630 assert_eq!(net.gen_index_by_id().get("GEN_A"), Some(&0));
3631 }
3632
3633 #[test]
3634 fn validate_for_solve_rejects_invalid_switched_shunt_indices() {
3635 let mut net = Network::new("switched-shunt-index");
3636 net.buses.push(Bus::new(1, BusType::Slack, 230.0));
3637 net.controls.switched_shunts.push(SwitchedShunt {
3638 id: "ssh_1".into(),
3639 bus: 1,
3640 bus_regulated: 0,
3641 b_step: 0.01,
3642 n_steps_cap: 1,
3643 n_steps_react: 0,
3644 v_target: 1.0,
3645 v_band: 0.02,
3646 n_active_steps: 0,
3647 });
3648
3649 let err = net.validate_for_solve().unwrap_err();
3650 assert!(matches!(
3651 err,
3652 NetworkError::InvalidSwitchedShuntRegulatedBus { id, bus }
3653 if id == "ssh_1" && bus == 0
3654 ));
3655 }
3656
3657 #[test]
3658 fn validate_for_solve_rejects_slack_without_regulating_generator() {
3659 let mut net = Network::new("slack-no-reg");
3660 net.buses.push(Bus::new(1, BusType::Slack, 230.0));
3661 let mut generator = Generator::new(1, 50.0, 1.0);
3662 generator.voltage_regulated = false;
3663 net.generators.push(generator);
3664
3665 let err = net.validate_for_solve().unwrap_err();
3666 assert!(matches!(
3667 err,
3668 NetworkError::InvalidSlackPlacement { slack_buses, .. } if slack_buses == vec![1]
3669 ));
3670 }
3671
3672 #[test]
3673 fn validate_for_solve_rejects_pv_without_regulating_generator() {
3674 let mut net = Network::new("pv-no-reg");
3675 net.buses.push(Bus::new(1, BusType::Slack, 230.0));
3676 net.buses.push(Bus::new(2, BusType::PV, 230.0));
3677 net.branches.push(Branch::new_line(1, 2, 0.01, 0.1, 0.0));
3678 net.generators.push(Generator::new(1, 60.0, 1.0));
3679 let mut generator = Generator::new(2, 40.0, 1.0);
3680 generator.voltage_regulated = false;
3681 net.generators.push(generator);
3682
3683 let err = net.validate_for_solve().unwrap_err();
3684 assert!(matches!(
3685 err,
3686 NetworkError::InvalidGeneratorField {
3687 bus,
3688 field: "voltage_regulated",
3689 value
3690 } if bus == 2 && value == 0.0
3691 ));
3692 }
3693
3694 #[test]
3695 fn validate_for_solve_accepts_remote_regulated_pv_bus() {
3696 let mut net = Network::new("pv-remote-reg");
3697 net.buses.push(Bus::new(1, BusType::Slack, 230.0));
3698 net.buses.push(Bus::new(2, BusType::PV, 230.0));
3699 net.branches.push(Branch::new_line(1, 2, 0.01, 0.1, 0.0));
3700
3701 let mut slack_generator = Generator::new(1, 60.0, 1.0);
3702 slack_generator.reg_bus = Some(1);
3703 net.generators.push(slack_generator);
3704
3705 let mut remote_generator = Generator::new(1, 20.0, 1.0);
3706 remote_generator.reg_bus = Some(2);
3707 net.generators.push(remote_generator);
3708
3709 assert!(net.validate_for_solve().is_ok());
3710 }
3711
3712 #[test]
3713 fn validate_structure_rejects_missing_remote_regulated_bus() {
3714 let mut net = Network::new("missing-reg-bus");
3715 net.buses.push(Bus::new(1, BusType::Slack, 230.0));
3716 let mut generator = Generator::new(1, 50.0, 1.0);
3717 generator.reg_bus = Some(2);
3718 net.generators.push(generator);
3719
3720 let err = net.validate_structure().unwrap_err();
3721 assert!(matches!(
3722 err,
3723 NetworkError::InvalidGeneratorRegulatedBus { bus, reg_bus }
3724 if bus == 1 && reg_bus == 2
3725 ));
3726 }
3727
3728 #[test]
3729 fn validate_structure_rejects_empty_interface_members() {
3730 let mut net = Network::new("interface-mismatch");
3731 net.buses.push(Bus::new(1, BusType::Slack, 230.0));
3732 net.buses.push(Bus::new(2, BusType::PQ, 230.0));
3733 net.interfaces.push(Interface {
3734 name: "IF_A".to_string(),
3735 members: vec![],
3736 limit_forward_mw: 100.0,
3737 limit_reverse_mw: 100.0,
3738 in_service: true,
3739 limit_forward_mw_schedule: vec![],
3740 limit_reverse_mw_schedule: vec![],
3741 });
3742
3743 let err = net.validate_structure().unwrap_err();
3744 assert!(matches!(
3745 err,
3746 NetworkError::InvalidInterfaceDefinition { name, .. } if name == "IF_A"
3747 ));
3748 }
3749
3750 #[test]
3751 fn validate_structure_rejects_missing_interface_branch_reference() {
3752 let mut net = Network::new("interface-missing-branch");
3753 net.buses.push(Bus::new(1, BusType::Slack, 230.0));
3754 net.buses.push(Bus::new(2, BusType::PQ, 230.0));
3755 net.interfaces.push(Interface {
3756 name: "IF_A".to_string(),
3757 members: vec![crate::network::WeightedBranchRef::new(1, 2, "1", 1.0)],
3758 limit_forward_mw: 100.0,
3759 limit_reverse_mw: 100.0,
3760 in_service: true,
3761 limit_forward_mw_schedule: vec![],
3762 limit_reverse_mw_schedule: vec![],
3763 });
3764
3765 let err = net.validate_structure().unwrap_err();
3766 assert!(matches!(
3767 err,
3768 NetworkError::InvalidInterfaceDefinition { name, .. } if name == "IF_A"
3769 ));
3770 }
3771
3772 #[test]
3773 fn validate_structure_rejects_non_finite_flowgate_coefficients() {
3774 let mut net = Network::new("flowgate-nan");
3775 net.buses.push(Bus::new(1, BusType::Slack, 230.0));
3776 net.buses.push(Bus::new(2, BusType::PQ, 230.0));
3777 net.branches.push(Branch::new_line(1, 2, 0.01, 0.1, 0.0));
3778 net.flowgates.push(Flowgate {
3779 name: "FG_A".to_string(),
3780 monitored: vec![crate::network::WeightedBranchRef::new(1, 2, "1", f64::NAN)],
3781 contingency_branch: None,
3782 limit_mw: 100.0,
3783 limit_reverse_mw: 100.0,
3784 in_service: true,
3785 limit_mw_schedule: vec![],
3786 limit_reverse_mw_schedule: vec![],
3787 hvdc_coefficients: vec![],
3788 hvdc_band_coefficients: vec![],
3789 limit_mw_active_period: None,
3790 });
3791
3792 let err = net.validate_structure().unwrap_err();
3793 assert!(matches!(
3794 err,
3795 NetworkError::InvalidFlowgateDefinition { name, .. } if name == "FG_A"
3796 ));
3797 }
3798
3799 #[test]
3800 fn validate_structure_rejects_missing_flowgate_contingency_branch() {
3801 let mut net = Network::new("flowgate-missing-ctg");
3802 net.buses.push(Bus::new(1, BusType::Slack, 230.0));
3803 net.buses.push(Bus::new(2, BusType::PQ, 230.0));
3804 net.branches.push(Branch::new_line(1, 2, 0.01, 0.1, 0.0));
3805 net.flowgates.push(Flowgate {
3806 name: "FG_A".to_string(),
3807 monitored: vec![crate::network::WeightedBranchRef::new(1, 2, "1", 1.0)],
3808 contingency_branch: Some(crate::network::BranchRef::new(2, 1, "1")),
3809 limit_mw: 100.0,
3810 limit_reverse_mw: 100.0,
3811 in_service: true,
3812 limit_mw_schedule: vec![],
3813 limit_reverse_mw_schedule: vec![],
3814 hvdc_coefficients: vec![],
3815 hvdc_band_coefficients: vec![],
3816 limit_mw_active_period: None,
3817 });
3818
3819 let err = net.validate_structure().unwrap_err();
3820 assert!(matches!(
3821 err,
3822 NetworkError::InvalidFlowgateDefinition { name, .. } if name == "FG_A"
3823 ));
3824 }
3825
3826 #[test]
3827 fn validate_structure_rejects_duplicate_hvdc_link_names() {
3828 let mut net = Network::new("dup-hvdc-name");
3829 net.buses.push(Bus::new(1, BusType::Slack, 230.0));
3830 net.buses.push(Bus::new(2, BusType::PQ, 230.0));
3831
3832 let mut link_a = crate::network::LccHvdcLink {
3833 name: "HVDC_A".to_string(),
3834 ..Default::default()
3835 };
3836 link_a.rectifier.bus = 1;
3837 link_a.inverter.bus = 2;
3838 let mut link_b = crate::network::LccHvdcLink {
3839 name: "HVDC_A".to_string(),
3840 ..Default::default()
3841 };
3842 link_b.rectifier.bus = 1;
3843 link_b.inverter.bus = 2;
3844 net.hvdc.push_lcc_link(link_a);
3845 net.hvdc.push_lcc_link(link_b);
3846
3847 let err = net.validate_structure().unwrap_err();
3848 assert!(matches!(
3849 err,
3850 NetworkError::DuplicateHvdcLinkName { name } if name == "HVDC_A"
3851 ));
3852 }
3853
3854 #[test]
3855 fn validate_for_solve_rejects_mixed_hvdc_representations() {
3856 let mut net = Network::new("mixed-hvdc");
3857 net.buses.push(Bus::new(1, BusType::Slack, 230.0));
3858 net.buses.push(Bus::new(2, BusType::PQ, 230.0));
3859
3860 let mut link = crate::network::LccHvdcLink {
3861 name: "HVDC_A".to_string(),
3862 ..Default::default()
3863 };
3864 link.rectifier.bus = 1;
3865 link.inverter.bus = 2;
3866 net.hvdc.push_lcc_link(link);
3867
3868 let grid = net.hvdc.ensure_dc_grid(1, Some("grid".to_string()));
3869 grid.buses.push(crate::network::DcBus {
3870 bus_id: 101,
3871 p_dc_mw: 0.0,
3872 v_dc_pu: 1.0,
3873 base_kv_dc: 320.0,
3874 v_dc_max: 1.1,
3875 v_dc_min: 0.9,
3876 cost: 0.0,
3877 g_shunt_siemens: 0.0,
3878 r_ground_ohm: 0.0,
3879 });
3880 grid.converters.push(crate::network::DcConverter::Vsc(
3881 crate::network::DcConverterStation {
3882 id: String::new(),
3883 dc_bus: 101,
3884 ac_bus: 1,
3885 control_type_dc: 2,
3886 control_type_ac: 1,
3887 active_power_mw: 0.0,
3888 reactive_power_mvar: 0.0,
3889 is_lcc: false,
3890 voltage_setpoint_pu: 1.0,
3891 transformer_r_pu: 0.0,
3892 transformer_x_pu: 0.0,
3893 transformer: false,
3894 tap_ratio: 1.0,
3895 filter_susceptance_pu: 0.0,
3896 filter: false,
3897 reactor_r_pu: 0.0,
3898 reactor_x_pu: 0.0,
3899 reactor: false,
3900 base_kv_ac: 230.0,
3901 voltage_max_pu: 1.1,
3902 voltage_min_pu: 0.9,
3903 current_max_pu: 2.0,
3904 status: true,
3905 loss_constant_mw: 0.0,
3906 loss_linear: 0.0,
3907 loss_quadratic_rectifier: 0.0,
3908 loss_quadratic_inverter: 0.0,
3909 droop: 0.0,
3910 power_dc_setpoint_mw: 0.0,
3911 voltage_dc_setpoint_pu: 1.0,
3912 active_power_ac_max_mw: 10.0,
3913 active_power_ac_min_mw: -10.0,
3914 reactive_power_ac_max_mvar: 10.0,
3915 reactive_power_ac_min_mvar: -10.0,
3916 },
3917 ));
3918
3919 let err = net.validate_for_solve().unwrap_err();
3920 assert!(matches!(err, NetworkError::MixedHvdcRepresentation));
3921 }
3922}