1use std::collections::{HashMap, HashSet};
9
10use serde::{Deserialize, Serialize};
11
12use crate::network::{
13 BranchCurrentRatings, BranchRatingSet, BusId, BusType, GenCaps, GenCost, Hvdc,
14 LoadVoltageModel, Network,
15};
16use crate::{Error, IndexedNetwork, Result};
17
18pub const NORMALIZED_SOLVER_TABLES_PASS: &str = "balanced-to-normalized-solver-tables";
20
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28#[non_exhaustive]
29pub struct NormalizedSolverTables {
30 pub pass: String,
31 pub network_name: String,
32 pub base_mva: f64,
33 pub base_frequency: f64,
34 pub units: SolverTableUnits,
35 pub index: SolverTableIndex,
36 pub buses: Vec<SolverBusRow>,
37 pub loads: Vec<SolverLoadRow>,
38 pub shunts: Vec<SolverShuntRow>,
39 pub branches: Vec<SolverBranchRow>,
40 pub switches: Vec<SolverSwitchRow>,
41 pub arcs: Vec<SolverArcRow>,
42 pub generators: Vec<SolverGeneratorRow>,
43 pub storage: Vec<SolverStorageRow>,
44 pub hvdc: Vec<SolverHvdcRow>,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49#[non_exhaustive]
50pub struct SolverTableUnits {
51 pub power: String,
52 pub voltage: String,
53 pub angle: String,
54 pub impedance: String,
55 pub admittance: String,
56 pub dense_index_base: String,
57}
58
59impl Default for SolverTableUnits {
60 fn default() -> Self {
61 Self {
62 power: "per_unit".to_string(),
63 voltage: "per_unit".to_string(),
64 angle: "radian".to_string(),
65 impedance: "per_unit".to_string(),
66 admittance: "per_unit".to_string(),
67 dense_index_base: "zero".to_string(),
68 }
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
74#[non_exhaustive]
75pub struct SolverTableIndex {
76 pub bus_ids: Vec<BusId>,
79 pub reference_bus_indices: Vec<usize>,
80 pub component_labels: Vec<usize>,
81 pub branch_from_arc_indices: Vec<usize>,
82 pub branch_to_arc_indices: Vec<usize>,
83 pub bus_source_rows: Vec<Option<usize>>,
84 pub load_source_rows: Vec<Option<usize>>,
85 pub shunt_source_rows: Vec<Option<usize>>,
86 pub branch_source_rows: Vec<Option<usize>>,
87 pub switch_source_rows: Vec<Option<usize>>,
88 pub generator_source_rows: Vec<Option<usize>>,
89 pub storage_source_rows: Vec<Option<usize>>,
90 pub hvdc_source_rows: Vec<Option<usize>>,
91}
92
93#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
94#[non_exhaustive]
95pub struct SolverBusRow {
96 pub index: usize,
97 pub bus_id: BusId,
98 pub source_row: Option<usize>,
99 pub kind: BusType,
100 pub vm: f64,
101 pub va: f64,
102 pub base_kv: f64,
103 pub vmax: f64,
104 pub vmin: f64,
105 pub evhi: Option<f64>,
106 pub evlo: Option<f64>,
107 pub area: usize,
108 pub zone: usize,
109 pub pd: f64,
110 pub qd: f64,
111 pub gs: f64,
112 pub bs: f64,
113}
114
115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
116#[non_exhaustive]
117pub struct SolverLoadRow {
118 pub index: usize,
119 pub source_row: Option<usize>,
120 pub bus_index: usize,
121 pub p: f64,
122 pub q: f64,
123 pub voltage_model: Option<LoadVoltageModel>,
124}
125
126#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
127#[non_exhaustive]
128pub struct SolverShuntRow {
129 pub index: usize,
130 pub source_row: Option<usize>,
131 pub bus_index: usize,
132 pub g: f64,
133 pub b: f64,
134}
135
136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
137#[non_exhaustive]
138pub struct SolverBranchRow {
139 pub index: usize,
140 pub source_row: Option<usize>,
141 pub from_bus_index: usize,
142 pub to_bus_index: usize,
143 pub r: f64,
144 pub x: f64,
145 pub b: f64,
146 pub g_fr: f64,
147 pub b_fr: f64,
148 pub g_to: f64,
149 pub b_to: f64,
150 pub rate_a: f64,
151 pub rate_b: f64,
152 pub rate_c: f64,
153 pub rating_sets: Vec<BranchRatingSet>,
154 pub current_ratings: Option<BranchCurrentRatings>,
155 pub tap: f64,
156 pub shift: f64,
157 pub angmin: f64,
158 pub angmax: f64,
159}
160
161#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
162#[non_exhaustive]
163pub struct SolverSwitchRow {
164 pub index: usize,
165 pub source_row: Option<usize>,
166 pub from_bus_index: usize,
167 pub to_bus_index: usize,
168 pub closed: bool,
169 pub thermal_rating: Option<f64>,
170 pub current_rating: Option<f64>,
171 pub pf: Option<f64>,
172 pub qf: Option<f64>,
173 pub pt: Option<f64>,
174 pub qt: Option<f64>,
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
178#[serde(rename_all = "snake_case")]
179pub enum SolverArcTerminal {
180 From,
181 To,
182}
183
184#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
185#[non_exhaustive]
186pub struct SolverArcRow {
187 pub index: usize,
188 pub branch_index: usize,
189 pub terminal: SolverArcTerminal,
190 pub from_bus_index: usize,
191 pub to_bus_index: usize,
192 pub tap: f64,
193 pub shift: f64,
194 pub g_shunt: f64,
195 pub b_shunt: f64,
196 pub rate_a: f64,
197}
198
199#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
200#[non_exhaustive]
201pub struct SolverGeneratorRow {
202 pub index: usize,
203 pub source_row: Option<usize>,
204 pub bus_index: usize,
205 pub pg: f64,
206 pub qg: f64,
207 pub pmax: f64,
208 pub pmin: f64,
209 pub qmax: f64,
210 pub qmin: f64,
211 pub vg: f64,
212 pub mbase: f64,
213 pub cost: Option<SolverCostRow>,
214 pub caps: GenCaps,
215 pub regulated_bus_index: Option<usize>,
216}
217
218#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
219#[non_exhaustive]
220pub struct SolverStorageRow {
221 pub index: usize,
222 pub source_row: Option<usize>,
223 pub bus_index: usize,
224 pub ps: f64,
225 pub qs: f64,
226 pub energy: f64,
227 pub energy_rating: f64,
228 pub charge_rating: f64,
229 pub discharge_rating: f64,
230 pub charge_efficiency: f64,
231 pub discharge_efficiency: f64,
232 pub thermal_rating: f64,
233 pub current_rating: Option<f64>,
234 pub qmin: f64,
235 pub qmax: f64,
236 pub r: f64,
237 pub x: f64,
238 pub p_loss: f64,
239 pub q_loss: f64,
240}
241
242#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
243#[non_exhaustive]
244pub struct SolverHvdcRow {
245 pub index: usize,
246 pub source_row: Option<usize>,
247 pub from_bus_index: usize,
248 pub to_bus_index: usize,
249 pub pf: f64,
250 pub pt: f64,
251 pub qf: f64,
252 pub qt: f64,
253 pub vf: f64,
254 pub vt: f64,
255 pub pmin: f64,
256 pub pmax: f64,
257 pub qminf: f64,
258 pub qmaxf: f64,
259 pub qmint: f64,
260 pub qmaxt: f64,
261 pub loss0: f64,
262 pub loss1: f64,
263 pub cost: Option<SolverCostRow>,
264}
265
266#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
267#[non_exhaustive]
268pub struct SolverCostRow {
269 pub model: u8,
270 pub startup: f64,
271 pub shutdown: f64,
272 pub ncost: usize,
273 pub coeffs: Vec<f64>,
274}
275
276impl From<&GenCost> for SolverCostRow {
277 fn from(cost: &GenCost) -> Self {
278 Self {
279 model: cost.model,
280 startup: cost.startup,
281 shutdown: cost.shutdown,
282 ncost: cost.ncost,
283 coeffs: cost.coeffs.clone(),
284 }
285 }
286}
287
288impl Network {
289 pub fn to_normalized_solver_tables(&self) -> Result<NormalizedSolverTables> {
296 NormalizedSolverTables::from_network(self)
297 }
298}
299
300impl NormalizedSolverTables {
301 pub fn from_network(source: &Network) -> Result<Self> {
302 let normalized = normalized_for_solver(source)?;
303 let view = IndexedNetwork::new(&normalized);
304 let net = view.network();
305 let provenance = SourceRows::new(source, net);
306
307 let branch_arcs = branch_and_arc_rows(&view, &provenance)?;
308 let buses = bus_rows(&view, &provenance);
309 let loads = load_rows(&view, &provenance)?;
310 let shunts = shunt_rows(&view, &provenance)?;
311 let switches = switch_rows(&view, &provenance)?;
312 let generators = generator_rows(&view, &provenance)?;
313 let storage = storage_rows(&view, &provenance)?;
314 let hvdc = hvdc_rows(&view, &provenance)?;
315
316 Ok(Self {
317 pass: NORMALIZED_SOLVER_TABLES_PASS.to_string(),
318 network_name: net.name.clone(),
319 base_mva: net.base_mva,
320 base_frequency: net.base_frequency,
321 units: SolverTableUnits::default(),
322 index: SolverTableIndex {
323 bus_ids: net.buses.iter().map(|b| b.id).collect(),
324 reference_bus_indices: view.reference_bus_indices(),
325 component_labels: view.connected_component_labels(),
326 branch_from_arc_indices: branch_arcs.branch_from_arc_indices,
327 branch_to_arc_indices: branch_arcs.branch_to_arc_indices,
328 bus_source_rows: provenance.bus,
329 load_source_rows: provenance.load,
330 shunt_source_rows: provenance.shunt,
331 branch_source_rows: provenance.branch,
332 switch_source_rows: provenance.switch,
333 generator_source_rows: provenance.generator,
334 storage_source_rows: provenance.storage,
335 hvdc_source_rows: provenance.hvdc,
336 },
337 buses,
338 loads,
339 shunts,
340 branches: branch_arcs.branches,
341 switches,
342 arcs: branch_arcs.arcs,
343 generators,
344 storage,
345 hvdc,
346 })
347 }
348}
349
350fn normalized_for_solver(source: &Network) -> Result<Network> {
351 if source.is_normalized() {
352 Ok(source.clone())
353 } else {
354 source.to_normalized()
355 }
356}
357
358fn bus_rows(view: &IndexedNetwork<'_>, provenance: &SourceRows) -> Vec<SolverBusRow> {
359 view.network()
360 .buses
361 .iter()
362 .enumerate()
363 .map(|(i, bus)| SolverBusRow {
364 index: i,
365 bus_id: bus.id,
366 source_row: provenance.bus[i],
367 kind: bus.kind,
368 vm: bus.vm,
369 va: bus.va,
370 base_kv: bus.base_kv,
371 vmax: bus.vmax,
372 vmin: bus.vmin,
373 evhi: bus.evhi,
374 evlo: bus.evlo,
375 area: bus.area,
376 zone: bus.zone,
377 pd: view.pd()[i],
378 qd: view.qd()[i],
379 gs: view.gs()[i],
380 bs: view.bs()[i],
381 })
382 .collect()
383}
384
385fn load_rows(view: &IndexedNetwork<'_>, provenance: &SourceRows) -> Result<Vec<SolverLoadRow>> {
386 view.network()
387 .loads
388 .iter()
389 .enumerate()
390 .map(|(i, load)| {
391 Ok(SolverLoadRow {
392 index: i,
393 source_row: provenance.load[i],
394 bus_index: dense_bus(view, load.bus, i)?,
395 p: load.p,
396 q: load.q,
397 voltage_model: load.voltage_model.clone(),
398 })
399 })
400 .collect()
401}
402
403fn shunt_rows(view: &IndexedNetwork<'_>, provenance: &SourceRows) -> Result<Vec<SolverShuntRow>> {
404 view.network()
405 .shunts
406 .iter()
407 .enumerate()
408 .map(|(i, shunt)| {
409 Ok(SolverShuntRow {
410 index: i,
411 source_row: provenance.shunt[i],
412 bus_index: dense_bus(view, shunt.bus, i)?,
413 g: shunt.g,
414 b: shunt.b,
415 })
416 })
417 .collect()
418}
419
420struct BranchArcRows {
421 branches: Vec<SolverBranchRow>,
422 arcs: Vec<SolverArcRow>,
423 branch_from_arc_indices: Vec<usize>,
424 branch_to_arc_indices: Vec<usize>,
425}
426
427fn branch_and_arc_rows(
428 view: &IndexedNetwork<'_>,
429 provenance: &SourceRows,
430) -> Result<BranchArcRows> {
431 let net = view.network();
432 let mut branch_from_arc_indices = Vec::with_capacity(net.branches.len());
433 let mut branch_to_arc_indices = Vec::with_capacity(net.branches.len());
434 let mut arcs = Vec::with_capacity(net.branches.len() * 2);
435 let branches = net
436 .branches
437 .iter()
438 .enumerate()
439 .map(|(i, branch)| {
440 let from_bus_index = dense_bus(view, branch.from, i)?;
441 let to_bus_index = dense_bus(view, branch.to, i)?;
442 let charging = branch.terminal_charging();
443 let from_arc = arcs.len();
444 arcs.push(SolverArcRow {
445 index: from_arc,
446 branch_index: i,
447 terminal: SolverArcTerminal::From,
448 from_bus_index,
449 to_bus_index,
450 tap: branch.tap,
451 shift: branch.shift,
452 g_shunt: charging.g_fr,
453 b_shunt: charging.b_fr,
454 rate_a: branch.rate_a,
455 });
456 let to_arc = arcs.len();
457 arcs.push(SolverArcRow {
458 index: to_arc,
459 branch_index: i,
460 terminal: SolverArcTerminal::To,
461 from_bus_index: to_bus_index,
462 to_bus_index: from_bus_index,
463 tap: 1.0,
464 shift: 0.0,
465 g_shunt: charging.g_to,
466 b_shunt: charging.b_to,
467 rate_a: branch.rate_a,
468 });
469 branch_from_arc_indices.push(from_arc);
470 branch_to_arc_indices.push(to_arc);
471
472 Ok(SolverBranchRow {
473 index: i,
474 source_row: provenance.branch[i],
475 from_bus_index,
476 to_bus_index,
477 r: branch.r,
478 x: branch.x,
479 b: branch.b,
480 g_fr: charging.g_fr,
481 b_fr: charging.b_fr,
482 g_to: charging.g_to,
483 b_to: charging.b_to,
484 rate_a: branch.rate_a,
485 rate_b: branch.rate_b,
486 rate_c: branch.rate_c,
487 rating_sets: branch.rating_sets.clone(),
488 current_ratings: branch.current_ratings,
489 tap: branch.tap,
490 shift: branch.shift,
491 angmin: branch.angmin,
492 angmax: branch.angmax,
493 })
494 })
495 .collect::<Result<Vec<_>>>()?;
496
497 Ok(BranchArcRows {
498 branches,
499 arcs,
500 branch_from_arc_indices,
501 branch_to_arc_indices,
502 })
503}
504
505fn switch_rows(view: &IndexedNetwork<'_>, provenance: &SourceRows) -> Result<Vec<SolverSwitchRow>> {
506 view.network()
507 .switches
508 .iter()
509 .enumerate()
510 .map(|(i, switch)| {
511 Ok(SolverSwitchRow {
512 index: i,
513 source_row: provenance.switch[i],
514 from_bus_index: dense_bus(view, switch.from, i)?,
515 to_bus_index: dense_bus(view, switch.to, i)?,
516 closed: switch.closed,
517 thermal_rating: switch.thermal_rating,
518 current_rating: switch.current_rating,
519 pf: switch.pf,
520 qf: switch.qf,
521 pt: switch.pt,
522 qt: switch.qt,
523 })
524 })
525 .collect()
526}
527
528fn generator_rows(
529 view: &IndexedNetwork<'_>,
530 provenance: &SourceRows,
531) -> Result<Vec<SolverGeneratorRow>> {
532 view.network()
533 .generators
534 .iter()
535 .enumerate()
536 .map(|(i, generator)| {
537 Ok(SolverGeneratorRow {
538 index: i,
539 source_row: provenance.generator[i],
540 bus_index: dense_bus(view, generator.bus, i)?,
541 pg: generator.pg,
542 qg: generator.qg,
543 pmax: generator.pmax,
544 pmin: generator.pmin,
545 qmax: generator.qmax,
546 qmin: generator.qmin,
547 vg: generator.vg,
548 mbase: generator.mbase,
549 cost: generator.cost.as_ref().map(SolverCostRow::from),
550 caps: generator.caps,
551 regulated_bus_index: generator
552 .regulated_bus
553 .map(|bus| dense_bus(view, bus, i))
554 .transpose()?,
555 })
556 })
557 .collect()
558}
559
560fn storage_rows(
561 view: &IndexedNetwork<'_>,
562 provenance: &SourceRows,
563) -> Result<Vec<SolverStorageRow>> {
564 let base_mva = view.network().base_mva;
565 view.network()
566 .storage
567 .iter()
568 .enumerate()
569 .map(|(i, storage)| {
570 Ok(SolverStorageRow {
571 index: i,
572 source_row: provenance.storage[i],
573 bus_index: dense_bus(view, storage.bus, i)?,
574 ps: storage.ps / base_mva,
575 qs: storage.qs / base_mva,
576 energy: storage.energy,
577 energy_rating: storage.energy_rating,
578 charge_rating: storage.charge_rating,
579 discharge_rating: storage.discharge_rating,
580 charge_efficiency: storage.charge_efficiency,
581 discharge_efficiency: storage.discharge_efficiency,
582 thermal_rating: storage.thermal_rating,
583 current_rating: storage.current_rating,
584 qmin: storage.qmin,
585 qmax: storage.qmax,
586 r: storage.r,
587 x: storage.x,
588 p_loss: storage.p_loss,
589 q_loss: storage.q_loss,
590 })
591 })
592 .collect()
593}
594
595fn hvdc_rows(view: &IndexedNetwork<'_>, provenance: &SourceRows) -> Result<Vec<SolverHvdcRow>> {
596 view.network()
597 .hvdc
598 .iter()
599 .enumerate()
600 .map(|(i, hvdc)| hvdc_row(view, provenance, i, hvdc))
601 .collect()
602}
603
604fn hvdc_row(
605 view: &IndexedNetwork<'_>,
606 provenance: &SourceRows,
607 i: usize,
608 hvdc: &Hvdc,
609) -> Result<SolverHvdcRow> {
610 let base_mva = view.network().base_mva;
611 Ok(SolverHvdcRow {
612 index: i,
613 source_row: provenance.hvdc[i],
614 from_bus_index: dense_bus(view, hvdc.from, i)?,
615 to_bus_index: dense_bus(view, hvdc.to, i)?,
616 pf: hvdc.pf,
617 pt: hvdc.pt,
618 qf: hvdc.qf,
619 qt: hvdc.qt,
620 vf: hvdc.vf,
621 vt: hvdc.vt,
622 pmin: hvdc.pmin / base_mva,
623 pmax: hvdc.pmax / base_mva,
624 qminf: hvdc.qminf,
625 qmaxf: hvdc.qmaxf,
626 qmint: hvdc.qmint,
627 qmaxt: hvdc.qmaxt,
628 loss0: hvdc.loss0,
629 loss1: hvdc.loss1,
630 cost: hvdc.cost.as_ref().map(SolverCostRow::from),
631 })
632}
633
634fn dense_bus(view: &IndexedNetwork<'_>, bus_id: BusId, element_index: usize) -> Result<usize> {
635 view.bus_index(bus_id).ok_or(Error::UnknownBus {
636 bus_id,
637 element_index,
638 })
639}
640
641#[derive(Debug)]
642struct SourceRows {
643 bus: Vec<Option<usize>>,
644 load: Vec<Option<usize>>,
645 shunt: Vec<Option<usize>>,
646 branch: Vec<Option<usize>>,
647 switch: Vec<Option<usize>>,
648 generator: Vec<Option<usize>>,
649 storage: Vec<Option<usize>>,
650 hvdc: Vec<Option<usize>>,
651}
652
653impl SourceRows {
654 fn new(source: &Network, lowered: &Network) -> Self {
655 let kept_buses: HashSet<BusId> = source
656 .buses
657 .iter()
658 .filter(|b| b.kind != BusType::Isolated)
659 .map(|b| b.id)
660 .collect();
661 let bus_source: HashMap<BusId, usize> = source
662 .buses
663 .iter()
664 .enumerate()
665 .filter(|(_, b)| b.kind != BusType::Isolated)
666 .map(|(i, b)| (b.id, i))
667 .collect();
668 let bus = lowered
669 .buses
670 .iter()
671 .map(|b| bus_source.get(&b.id).copied())
672 .collect();
673
674 Self {
675 bus,
676 load: resize_sources(
677 lowered.loads.len(),
678 source.loads.iter().enumerate().filter_map(|(i, load)| {
679 (load.in_service && kept_buses.contains(&load.bus)).then_some(i)
680 }),
681 ),
682 shunt: resize_sources(
683 lowered.shunts.len(),
684 source.shunts.iter().enumerate().filter_map(|(i, shunt)| {
685 (shunt.in_service && kept_buses.contains(&shunt.bus)).then_some(i)
686 }),
687 ),
688 branch: resize_sources(
689 lowered.branches.len(),
690 source
691 .branches
692 .iter()
693 .enumerate()
694 .filter_map(|(i, branch)| {
695 (branch.in_service
696 && kept_buses.contains(&branch.from)
697 && kept_buses.contains(&branch.to))
698 .then_some(i)
699 }),
700 ),
701 switch: resize_sources(
702 lowered.switches.len(),
703 source
704 .switches
705 .iter()
706 .enumerate()
707 .filter_map(|(i, switch)| {
708 (kept_buses.contains(&switch.from) && kept_buses.contains(&switch.to))
709 .then_some(i)
710 }),
711 ),
712 generator: resize_sources(
713 lowered.generators.len(),
714 source
715 .generators
716 .iter()
717 .enumerate()
718 .filter_map(|(i, generator)| {
719 (generator.in_service && kept_buses.contains(&generator.bus)).then_some(i)
720 }),
721 ),
722 storage: resize_sources(
723 lowered.storage.len(),
724 source
725 .storage
726 .iter()
727 .enumerate()
728 .filter_map(|(i, storage)| {
729 (storage.in_service && kept_buses.contains(&storage.bus)).then_some(i)
730 }),
731 ),
732 hvdc: resize_sources(
733 lowered.hvdc.len(),
734 source.hvdc.iter().enumerate().filter_map(|(i, hvdc)| {
735 (hvdc.in_service
736 && kept_buses.contains(&hvdc.from)
737 && kept_buses.contains(&hvdc.to))
738 .then_some(i)
739 }),
740 ),
741 }
742 }
743}
744
745fn resize_sources(len: usize, rows: impl Iterator<Item = usize>) -> Vec<Option<usize>> {
746 let mut out: Vec<Option<usize>> = rows.map(Some).collect();
747 out.resize(len, None);
748 out.truncate(len);
749 out
750}
751
752#[cfg(test)]
753mod tests {
754 use super::*;
755 use crate::network::{Branch, Bus, Extras, Generator, Hvdc, Load, SourceFormat, Storage};
756 use crate::parse_file;
757
758 fn approx(a: f64, b: f64) -> bool {
759 (a - b).abs() < 1e-12
760 }
761
762 fn bus(id: usize, kind: BusType) -> Bus {
763 Bus {
764 id: BusId(id),
765 kind,
766 vm: 1.0,
767 va: 0.0,
768 base_kv: 230.0,
769 vmax: 1.1,
770 vmin: 0.9,
771 evhi: None,
772 evlo: None,
773 area: 1,
774 zone: 1,
775 name: None,
776 uid: None,
777 extras: Extras::new(),
778 }
779 }
780
781 fn branch(from: usize, to: usize, in_service: bool) -> Branch {
782 Branch {
783 from: BusId(from),
784 to: BusId(to),
785 r: 0.01,
786 x: 0.1,
787 b: 0.02,
788 charging: None,
789 rate_a: 100.0,
790 rate_b: 110.0,
791 rate_c: 120.0,
792 rating_sets: Vec::new(),
793 current_ratings: None,
794 tap: 0.0,
795 shift: 30.0,
796 in_service,
797 angmin: -360.0,
798 angmax: 360.0,
799 control: None,
800 solution: None,
801 uid: None,
802 extras: Extras::new(),
803 }
804 }
805
806 fn generator(bus: usize, in_service: bool) -> Generator {
807 Generator {
808 bus: BusId(bus),
809 pg: 50.0,
810 qg: 5.0,
811 pmax: 80.0,
812 pmin: 0.0,
813 qmax: 40.0,
814 qmin: -40.0,
815 vg: 1.0,
816 mbase: 100.0,
817 in_service,
818 cost: None,
819 caps: [None; crate::network::GEN_EXTRA_KEYS.len()],
820 regulated_bus: None,
821 uid: None,
822 }
823 }
824
825 #[test]
826 fn solver_tables_are_dense_normalized_and_traceable() {
827 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/case14.m");
828 let net = parse_file(path, None).unwrap().network;
829
830 let tables = net.to_normalized_solver_tables().unwrap();
831
832 assert_eq!(tables.pass, NORMALIZED_SOLVER_TABLES_PASS);
833 assert_eq!(tables.units.power, "per_unit");
834 assert_eq!(tables.units.angle, "radian");
835 assert_eq!(tables.buses.len(), 14);
836 assert_eq!(tables.branches.len(), 20);
837 assert_eq!(tables.arcs.len(), 40);
838 assert_eq!(tables.index.reference_bus_indices, vec![0]);
839 assert_eq!(tables.index.branch_from_arc_indices[0], 0);
840 assert_eq!(tables.index.branch_to_arc_indices[0], 1);
841 assert_eq!(tables.arcs[0].terminal, SolverArcTerminal::From);
842 assert_eq!(tables.arcs[1].terminal, SolverArcTerminal::To);
843 assert!(tables.index.bus_source_rows.iter().all(Option::is_some));
844 assert!(tables.index.branch_source_rows.iter().all(Option::is_some));
845
846 let bus_2 = &tables.buses[1];
847 assert_eq!(bus_2.bus_id, BusId(2));
848 assert!(approx(bus_2.pd, 21.7 / 100.0));
849 assert!(approx(bus_2.qd, 12.7 / 100.0));
850 }
851
852 #[test]
853 fn solver_tables_filter_out_of_service_rows_and_keep_source_rows() {
854 let mut net = Network::in_memory(
855 "filtered",
856 100.0,
857 vec![
858 bus(1, BusType::Ref),
859 bus(2, BusType::Pq),
860 bus(3, BusType::Isolated),
861 ],
862 vec![branch(1, 2, true), branch(1, 3, true), branch(1, 2, false)],
863 );
864 net.loads.push(Load {
865 bus: BusId(2),
866 p: 10.0,
867 q: 5.0,
868 voltage_model: None,
869 in_service: true,
870 uid: None,
871 extras: Extras::new(),
872 });
873 net.loads.push(Load {
874 bus: BusId(3),
875 p: 99.0,
876 q: 99.0,
877 voltage_model: None,
878 in_service: true,
879 uid: None,
880 extras: Extras::new(),
881 });
882 net.generators.push(generator(1, true));
883 net.generators.push(generator(2, false));
884 net.source_format = SourceFormat::Matpower;
885
886 let tables = net.to_normalized_solver_tables().unwrap();
887
888 assert_eq!(tables.index.bus_ids, vec![BusId(1), BusId(2)]);
889 assert_eq!(tables.branches.len(), 1);
890 assert_eq!(tables.loads.len(), 1);
891 assert_eq!(tables.generators.len(), 1);
892 assert_eq!(tables.index.branch_source_rows, vec![Some(0)]);
893 assert_eq!(tables.index.load_source_rows, vec![Some(0)]);
894 assert_eq!(tables.index.generator_source_rows, vec![Some(0)]);
895 assert!(approx(tables.loads[0].p, 0.1));
896 assert!(approx(tables.branches[0].rate_a, 1.0));
897 assert!(approx(tables.branches[0].tap, 1.0));
898 assert!(approx(tables.branches[0].shift, 30.0_f64.to_radians()));
899 }
900
901 #[test]
902 fn solver_tables_do_not_scale_an_already_normalized_network_twice() {
903 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/case14.m");
904 let net = parse_file(path, None).unwrap().network;
905 let normalized = net.to_normalized().unwrap();
906
907 let tables = normalized.to_normalized_solver_tables().unwrap();
908
909 let bus_2 = &tables.buses[1];
910 assert!(approx(bus_2.pd, 21.7 / 100.0));
911 assert!(approx(bus_2.qd, 12.7 / 100.0));
912 }
913
914 #[test]
915 fn solver_tables_scale_storage_and_hvdc_power_fields_to_per_unit() {
916 let mut net = Network::in_memory(
917 "storage-hvdc",
918 100.0,
919 vec![bus(1, BusType::Ref), bus(2, BusType::Pq)],
920 Vec::new(),
921 );
922 net.generators.push(generator(1, true));
923 net.storage.push(Storage {
924 bus: BusId(2),
925 ps: 30.0,
926 qs: -10.0,
927 energy: 50.0,
928 energy_rating: 100.0,
929 charge_rating: 20.0,
930 discharge_rating: 25.0,
931 charge_efficiency: 0.9,
932 discharge_efficiency: 0.85,
933 thermal_rating: 40.0,
934 current_rating: None,
935 qmin: -15.0,
936 qmax: 15.0,
937 r: 0.01,
938 x: 0.02,
939 p_loss: 2.0,
940 q_loss: 1.0,
941 in_service: true,
942 uid: None,
943 extras: Extras::new(),
944 });
945 net.hvdc.push(Hvdc {
946 from: BusId(1),
947 to: BusId(2),
948 in_service: true,
949 pf: 20.0,
950 pt: -19.0,
951 qf: 5.0,
952 qt: -4.0,
953 vf: 1.0,
954 vt: 1.0,
955 pmin: -40.0,
956 pmax: 75.0,
957 qminf: -25.0,
958 qmaxf: 30.0,
959 qmint: -20.0,
960 qmaxt: 22.0,
961 loss0: 1.5,
962 loss1: 0.02,
963 cost: None,
964 uid: None,
965 extras: Extras::new(),
966 });
967
968 let tables = net.to_normalized_solver_tables().unwrap();
969
970 let storage = &tables.storage[0];
971 assert!(approx(storage.ps, 0.3));
972 assert!(approx(storage.qs, -0.1));
973 assert!(approx(storage.energy, 0.5));
974 assert!(approx(storage.thermal_rating, 0.4));
975 assert!(approx(storage.p_loss, 0.02));
976
977 let hvdc = &tables.hvdc[0];
978 assert!(approx(hvdc.pf, 0.2));
979 assert!(approx(hvdc.pt, -0.19));
980 assert!(approx(hvdc.pmin, -0.4));
981 assert!(approx(hvdc.pmax, 0.75));
982 assert!(approx(hvdc.qminf, -0.25));
983 assert!(approx(hvdc.loss0, 0.015));
984 }
985}