Skip to main content

powerio/
solver_tables.rs

1//! Normalized dense tables for solver and compiler front ends.
2//!
3//! `Network::to_normalized` keeps source bus ids because it is still a network
4//! model. Solver inputs want dense row ids, stable row order, and enough
5//! provenance to map lowered data back to the source case. This module provides
6//! that table contract without changing the lossless `Network` representation.
7
8use 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
18/// Stable pass name for the balanced normalized solver table lowering.
19pub const NORMALIZED_SOLVER_TABLES_PASS: &str = "balanced-to-normalized-solver-tables";
20
21/// A row oriented, dense indexed, per unit/radian view of a balanced network.
22///
23/// The source `Network` is first normalized with [`Network::to_normalized`], then
24/// lowered through [`IndexedNetwork`] so 3-winding transformers appear as star
25/// buses and branches. Source ids are preserved as metadata; every reference used
26/// for computation is dense and zero based.
27#[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/// Units carried by [`NormalizedSolverTables`].
48#[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/// Identity and provenance vectors that apply across the tables.
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
74#[non_exhaustive]
75pub struct SolverTableIndex {
76    /// Source bus id for each dense bus row. Synthetic 3-winding star buses also
77    /// receive a stable id in this vector, but have no source row.
78    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    /// Lower this balanced network into normalized dense solver tables.
290    ///
291    /// # Errors
292    /// Propagates [`Network::to_normalized`] errors and reports
293    /// [`Error::UnknownBus`] if the derived normalized network contains an
294    /// internal dangling bus reference.
295    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}