Skip to main content

powerio/
solver_tables.rs

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