1use serde::{Deserialize, Serialize};
7
8use powerio_prob::{AcBusSpecification, AcOpfInstance, AcPfInstance, ReferenceBuses};
9use powerio_tx::{BalancedNetwork, BusId, IndexedNetwork};
10
11use crate::dcopf::{Units, limits, nodal};
12use crate::{AnalysisBranchSource, Error, PiecewiseLinearCost, PreparedObjective, Result};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[non_exhaustive]
19pub struct AcOpfAssemblyOptions {
20 pub units: Units,
22 pub skip_zero_impedance: bool,
27 pub synthesize_unrated_limits: bool,
32 pub correct_angle_difference_bounds: bool,
35}
36
37impl Default for AcOpfAssemblyOptions {
38 fn default() -> Self {
39 Self {
40 units: Units::default(),
41 skip_zero_impedance: false,
42 synthesize_unrated_limits: false,
43 correct_angle_difference_bounds: true,
44 }
45 }
46}
47
48impl AcOpfAssemblyOptions {
49 #[must_use]
50 pub const fn with_units(mut self, units: Units) -> Self {
51 self.units = units;
52 self
53 }
54
55 #[must_use]
56 pub const fn with_skip_zero_impedance(mut self, skip: bool) -> Self {
57 self.skip_zero_impedance = skip;
58 self
59 }
60
61 #[must_use]
62 pub const fn with_synthesize_unrated_limits(mut self, synthesize: bool) -> Self {
63 self.synthesize_unrated_limits = synthesize;
64 self
65 }
66
67 #[must_use]
68 pub const fn with_correct_angle_difference_bounds(mut self, correct: bool) -> Self {
69 self.correct_angle_difference_bounds = correct;
70 self
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76#[non_exhaustive]
77pub struct AcPfAssemblyOptions {
78 pub units: Units,
80 pub skip_zero_impedance: bool,
82 pub correct_angle_difference_bounds: bool,
85}
86
87impl Default for AcPfAssemblyOptions {
88 fn default() -> Self {
89 Self {
90 units: Units::default(),
91 skip_zero_impedance: false,
92 correct_angle_difference_bounds: true,
93 }
94 }
95}
96
97impl AcPfAssemblyOptions {
98 #[must_use]
99 pub const fn with_units(mut self, units: Units) -> Self {
100 self.units = units;
101 self
102 }
103
104 #[must_use]
105 pub const fn with_skip_zero_impedance(mut self, skip: bool) -> Self {
106 self.skip_zero_impedance = skip;
107 self
108 }
109
110 #[must_use]
111 pub const fn with_correct_angle_difference_bounds(mut self, correct: bool) -> Self {
112 self.correct_angle_difference_bounds = correct;
113 self
114 }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
124#[serde(rename_all = "snake_case", tag = "kind")]
125#[non_exhaustive]
126pub enum PreparedAcBusSpecification {
127 Pq { p: f64, q: f64 },
128 Pv { p: f64, vm: f64 },
129 Reference { vm: f64, va: f64 },
130 Isolated,
131}
132
133#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
135#[non_exhaustive]
136pub struct AcPfBusData {
137 pub q_d: Vec<f64>,
140 pub g_s: Vec<f64>,
142 pub b_s: Vec<f64>,
144 pub initial_vm: Vec<f64>,
146 pub initial_va: Vec<f64>,
148}
149
150#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
152#[non_exhaustive]
153pub struct AcPfGeneratorData {
154 pub identities: Vec<String>,
155 pub bus_of_gen: Vec<usize>,
157 pub analysis_rows: Vec<usize>,
159 pub source_rows: Vec<Option<usize>>,
161 pub qg: Vec<f64>,
163 pub qmax: Vec<f64>,
164 pub qmin: Vec<f64>,
165}
166
167#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
169#[non_exhaustive]
170pub struct AcPfPreparation {
171 pub name: String,
172 pub n_buses: usize,
173 pub n_source_generators: usize,
174 pub n_source_branches: usize,
175 pub base_mva: f64,
176 pub units: Units,
177 pub skip_zero_impedance: bool,
178 pub correct_angle_difference_bounds: bool,
180 pub bus_ids: Vec<BusId>,
182 pub bus_analysis_rows: Vec<usize>,
184 pub bus_source_rows: Vec<Option<usize>>,
187 pub specifications: Vec<PreparedAcBusSpecification>,
191 pub reference_buses: ReferenceBuses,
192 pub buses: AcPfBusData,
193 pub generators: AcPfGeneratorData,
194 pub branches: AcBranchData,
195}
196
197impl AcPfPreparation {
198 #[must_use]
199 pub fn n_generators(&self) -> usize {
200 self.generators.qg.len()
201 }
202
203 #[must_use]
204 pub fn n_branches(&self) -> usize {
205 self.branches.g.len()
206 }
207}
208
209#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
211#[non_exhaustive]
212pub struct AcBusData {
213 pub p_d: Vec<f64>,
215 pub q_d: Vec<f64>,
217 pub g_s: Vec<f64>,
220 pub b_s: Vec<f64>,
223 pub vm_min: Vec<f64>,
225 pub vm_max: Vec<f64>,
227 pub initial_vm: Vec<f64>,
230 pub initial_va: Vec<f64>,
233 pub voltage_bound_active: Vec<bool>,
235}
236
237#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
239#[non_exhaustive]
240pub struct AcBranchData {
241 pub identities: Vec<String>,
243 pub from_bus: Vec<usize>,
244 pub to_bus: Vec<usize>,
245 pub g: Vec<f64>,
247 pub b: Vec<f64>,
249 pub g_fr: Vec<f64>,
251 pub b_fr: Vec<f64>,
253 pub g_to: Vec<f64>,
255 pub b_to: Vec<f64>,
257 pub tap: Vec<f64>,
260 pub shift: Vec<f64>,
262 pub s_max: Vec<f64>,
264 pub angle_min: Vec<f64>,
266 pub angle_max: Vec<f64>,
267 pub analysis_rows: Vec<usize>,
269 pub analysis_sources: Vec<AnalysisBranchSource>,
272 pub skipped_zero_impedance: Vec<usize>,
274 pub thermal_limit_active: Vec<bool>,
276 pub angle_bound_active: Vec<bool>,
278}
279
280#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
282#[non_exhaustive]
283pub struct AcGeneratorData {
284 pub identities: Vec<String>,
286 pub bus_of_gen: Vec<usize>,
288 pub analysis_rows: Vec<usize>,
290 pub source_rows: Vec<Option<usize>>,
292 pub q: Vec<f64>,
294 pub c: Vec<f64>,
296 pub c0: Vec<f64>,
299 pub piecewise_linear: Vec<Option<PiecewiseLinearCost>>,
303 pub pmax: Vec<f64>,
304 pub pmin: Vec<f64>,
305 pub qmax: Vec<f64>,
306 pub qmin: Vec<f64>,
307 pub pg: Vec<f64>,
309 pub qg: Vec<f64>,
311 pub vg: Vec<f64>,
313 pub capability_active: Vec<bool>,
315}
316
317#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
324#[non_exhaustive]
325pub struct AcStorageData {
326 pub identities: Vec<String>,
328 pub bus_of_storage: Vec<usize>,
330 pub source_rows: Vec<usize>,
332 pub p: Vec<f64>,
333 pub q: Vec<f64>,
334 pub energy: Vec<f64>,
335 pub energy_rating: Vec<f64>,
336 pub charge_rating: Vec<f64>,
337 pub discharge_rating: Vec<f64>,
338 pub charge_efficiency: Vec<f64>,
339 pub discharge_efficiency: Vec<f64>,
340 pub s_max: Vec<f64>,
341 pub qmin: Vec<f64>,
342 pub qmax: Vec<f64>,
343 pub r: Vec<f64>,
344 pub x: Vec<f64>,
345 pub p_loss: Vec<f64>,
346 pub q_loss: Vec<f64>,
347 pub in_service: Vec<bool>,
348}
349
350#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
353#[non_exhaustive]
354pub struct NodalAcGeneratorData {
355 pub q: Vec<f64>,
356 pub c: Vec<f64>,
357 pub c0: Vec<f64>,
358 pub pmax: Vec<f64>,
359 pub pmin: Vec<f64>,
360 pub qmax: Vec<f64>,
361 pub qmin: Vec<f64>,
362 pub has_gen: Vec<bool>,
367}
368
369#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
380#[non_exhaustive]
381pub struct AcOpfPreparation {
382 pub name: String,
383 pub n_buses: usize,
384 pub n_source_generators: usize,
385 pub n_source_branches: usize,
386 pub base_mva: f64,
387 pub units: Units,
388 pub objective: PreparedObjective,
390 pub skip_zero_impedance: bool,
391 pub synthesize_unrated_limits: bool,
393 pub correct_angle_difference_bounds: bool,
395 pub bus_ids: Vec<BusId>,
397 pub bus_analysis_rows: Vec<usize>,
399 pub bus_source_rows: Vec<Option<usize>>,
402 pub reference_buses: ReferenceBuses,
403 pub buses: AcBusData,
404 pub generators: AcGeneratorData,
405 pub storage: AcStorageData,
406 pub branches: AcBranchData,
407}
408
409impl AcOpfPreparation {
410 #[must_use]
411 pub fn n_generators(&self) -> usize {
412 self.generators.q.len()
413 }
414
415 #[must_use]
416 pub fn n_branches(&self) -> usize {
417 self.branches.g.len()
418 }
419
420 #[must_use]
421 pub fn n_storage(&self) -> usize {
422 self.storage.identities.len()
423 }
424
425 pub fn calc_nodal_generator_data(&self) -> Result<NodalAcGeneratorData> {
435 let n = self.n_buses;
436 let generators = &self.generators;
437 if let Some(gen_index) = generators.piecewise_linear.iter().position(Option::is_some) {
438 return Err(Error::PiecewiseNodalCost { gen_index });
439 }
440 let bus_of_gen = &generators.bus_of_gen;
441 let costs =
442 nodal::combine_costs(n, bus_of_gen, &generators.q, &generators.c, &generators.c0);
443 Ok(NodalAcGeneratorData {
444 q: costs.q,
445 c: costs.c,
446 c0: costs.c0,
447 pmax: nodal::sum_by_bus(n, bus_of_gen, &generators.pmax),
448 pmin: nodal::sum_by_bus(n, bus_of_gen, &generators.pmin),
449 qmax: nodal::sum_by_bus(n, bus_of_gen, &generators.qmax),
450 qmin: nodal::sum_by_bus(n, bus_of_gen, &generators.qmin),
451 has_gen: nodal::buses_with_generators(n, bus_of_gen),
452 })
453 }
454
455 #[must_use]
462 pub fn calc_vm_setpoints(&self) -> Vec<f64> {
463 let mut vm: Vec<f64> = self
464 .buses
465 .initial_vm
466 .iter()
467 .map(|&value| if value > 0.0 { value } else { 1.0 })
468 .collect();
469 for generator in 0..self.n_generators() {
470 let vg = self.generators.vg[generator];
471 if vg > 0.0 {
472 vm[self.generators.bus_of_gen[generator]] = vg;
473 }
474 }
475 vm
476 }
477}
478
479pub fn build_ac_opf_preparation(
488 instance: &AcOpfInstance,
489 options: &AcOpfAssemblyOptions,
490) -> Result<AcOpfPreparation> {
491 let view = IndexedNetwork::new(instance.network());
492 let objective = crate::opf::compile_objective(instance.objective())?;
493 let mut preparation = preparation_from_view(&view, *options, objective)?;
494 apply_instance_semantics(&mut preparation, instance.network(), instance.constraints())?;
495 if let Some(point) = instance.initial_point() {
496 let (power_scale, _) = options.units.power_scales(preparation.base_mva);
497 for (dense, bus) in preparation.bus_ids.iter().copied().enumerate() {
498 if let Some(value) = point.bus_voltage_magnitude(bus) {
499 preparation.buses.initial_vm[dense] = value;
500 }
501 if let Some(value) = point.bus_voltage_angle(bus) {
502 preparation.buses.initial_va[dense] = value;
503 }
504 }
505 for (generator, identity) in preparation.generators.identities.iter().enumerate() {
506 if let Some(value) = point.generator_active_power(identity) {
507 preparation.generators.pg[generator] = value * power_scale;
508 }
509 if let Some(value) = point.generator_reactive_power(identity) {
510 preparation.generators.qg[generator] = value * power_scale;
511 }
512 if let Some(value) = point.generator_voltage_setpoint(identity) {
513 preparation.generators.vg[generator] = value;
514 }
515 }
516 }
517 Ok(preparation)
518}
519
520pub fn build_ac_pf_preparation(
529 instance: &AcPfInstance,
530 options: &AcPfAssemblyOptions,
531) -> Result<AcPfPreparation> {
532 let source = instance.network();
533 let mut analysis_network = source.clone();
534 for (bus, specification) in analysis_network
535 .buses_mut()
536 .iter_mut()
537 .zip(instance.specifications())
538 {
539 bus.kind = match specification {
540 AcBusSpecification::Pq { .. } => powerio_tx::BusType::Pq,
541 AcBusSpecification::Pv { .. } => powerio_tx::BusType::Pv,
542 AcBusSpecification::Reference { .. } => powerio_tx::BusType::Ref,
543 AcBusSpecification::Isolated => powerio_tx::BusType::Isolated,
544 _ => return Err(Error::UnsupportedAcPfSpecification),
545 };
546 }
547
548 let view = IndexedNetwork::new(&analysis_network);
549 let mut common = preparation_from_view(
550 &view,
551 AcOpfAssemblyOptions {
552 units: options.units,
553 skip_zero_impedance: options.skip_zero_impedance,
554 synthesize_unrated_limits: false,
555 correct_angle_difference_bounds: options.correct_angle_difference_bounds,
556 },
557 PreparedObjective::Feasibility,
558 )?;
559 apply_source_mappings(&mut common, source);
560
561 let (power_scale, _) = options.units.power_scales(view.per_unit_base());
562 let specifications = common
563 .bus_source_rows
564 .iter()
565 .map(|source_row| match source_row {
566 Some(row) => prepare_bus_specification(
567 instance.specifications()[*row],
568 power_scale,
569 source.is_normalized(),
570 ),
571 None => Ok(PreparedAcBusSpecification::Pq { p: 0.0, q: 0.0 }),
572 })
573 .collect::<Result<Vec<_>>>()?;
574
575 let mut initial_vm = common.buses.initial_vm.clone();
576 let mut initial_va = common
577 .bus_analysis_rows
578 .iter()
579 .map(|&row| view.to_radians(view.network().buses()[row].va))
580 .collect::<Vec<_>>();
581 if let Some(point) = instance.initial_point() {
582 for (dense, bus) in common.bus_ids.iter().copied().enumerate() {
583 if common.bus_source_rows[dense].is_none() {
584 continue;
585 }
586 if let Some(value) = point.bus_voltage_magnitude(bus) {
587 initial_vm[dense] = value;
588 }
589 if let Some(value) = point.bus_voltage_angle(bus) {
590 initial_va[dense] = value;
591 }
592 }
593 }
594
595 Ok(AcPfPreparation {
596 name: common.name,
597 n_buses: common.n_buses,
598 n_source_generators: common.n_source_generators,
599 n_source_branches: common.n_source_branches,
600 base_mva: common.base_mva,
601 units: common.units,
602 skip_zero_impedance: common.skip_zero_impedance,
603 correct_angle_difference_bounds: common.correct_angle_difference_bounds,
604 bus_ids: common.bus_ids,
605 bus_analysis_rows: common.bus_analysis_rows,
606 bus_source_rows: common.bus_source_rows,
607 specifications,
608 reference_buses: common.reference_buses,
609 buses: AcPfBusData {
610 q_d: common.buses.q_d,
611 g_s: common.buses.g_s,
612 b_s: common.buses.b_s,
613 initial_vm,
614 initial_va,
615 },
616 generators: AcPfGeneratorData {
617 identities: common.generators.identities,
618 bus_of_gen: common.generators.bus_of_gen,
619 analysis_rows: common.generators.analysis_rows,
620 source_rows: common.generators.source_rows,
621 qg: common.generators.qg,
622 qmax: common.generators.qmax,
623 qmin: common.generators.qmin,
624 },
625 branches: common.branches,
626 })
627}
628
629fn prepare_bus_specification(
630 specification: AcBusSpecification,
631 power_scale: f64,
632 source_is_normalized: bool,
633) -> Result<PreparedAcBusSpecification> {
634 Ok(match specification {
635 AcBusSpecification::Pq { p, q } => PreparedAcBusSpecification::Pq {
636 p: p * power_scale,
637 q: q * power_scale,
638 },
639 AcBusSpecification::Pv { p, vm } => PreparedAcBusSpecification::Pv {
640 p: p * power_scale,
641 vm,
642 },
643 AcBusSpecification::Reference { vm, va } => PreparedAcBusSpecification::Reference {
644 vm,
645 va: if source_is_normalized {
646 va
647 } else {
648 va.to_radians()
649 },
650 },
651 AcBusSpecification::Isolated => PreparedAcBusSpecification::Isolated,
652 _ => return Err(Error::UnsupportedAcPfSpecification),
653 })
654}
655
656#[allow(clippy::too_many_lines)]
658fn preparation_from_view(
659 case: &IndexedNetwork,
660 options: AcOpfAssemblyOptions,
661 objective: PreparedObjective,
662) -> Result<AcOpfPreparation> {
663 case.network().check_base_mva()?;
664
665 let active_buses = crate::opf::active_bus_index(case)?;
666 let n_buses = active_buses.analysis_rows.len();
667 let base = case.per_unit_base();
668 let (p_scale, y_scale) = options.units.power_scales(base);
669 let thermal = limits::ThermalLimits {
670 synthesize_unrated: options.synthesize_unrated_limits,
671 power_scale: p_scale,
672 admittance_scale: y_scale,
673 };
674 let (q_scale, c_scale) = options.units.cost_scales(base);
675
676 let mut bus_of_gen = Vec::new();
677 let mut generator_identities = Vec::new();
678 let mut generator_rows = Vec::new();
679 let mut cost_q = Vec::new();
680 let mut cost_c = Vec::new();
681 let mut cost_c0 = Vec::new();
682 let mut piecewise_linear = Vec::new();
683 let mut pmax = Vec::new();
684 let mut pmin = Vec::new();
685 let mut qmax = Vec::new();
686 let mut qmin = Vec::new();
687 let mut pg = Vec::new();
688 let mut qg = Vec::new();
689 let mut vg = Vec::new();
690
691 for (source_row, generator) in case.in_service_gens() {
692 let analysis_bus = case
693 .bus_index(generator.bus)
694 .ok_or(powerio_tx::Error::UnknownBus {
695 bus_id: generator.bus,
696 element_index: source_row,
697 })?;
698 let Some(bus) = active_buses.dense_by_analysis[analysis_bus] else {
699 continue;
700 };
701 let terms = match objective {
702 PreparedObjective::Feasibility => nodal::GeneratorCostTerms {
703 q: 0.0,
704 c: 0.0,
705 c0: 0.0,
706 piecewise_linear: None,
707 },
708 PreparedObjective::NetworkGeneratorCost => {
709 let cost = generator
710 .cost
711 .as_ref()
712 .ok_or(powerio_tx::Error::MissingGenCost {
713 gen_index: source_row,
714 })?;
715 nodal::generator_cost_terms(cost, source_row, p_scale)?
716 }
717 };
718 generator_identities.push(crate::opf::row_identity(
719 generator.uid.as_deref(),
720 "generators",
721 source_row,
722 ));
723 bus_of_gen.push(bus);
724 generator_rows.push(source_row);
725 cost_q.push(terms.q * q_scale);
726 cost_c.push(terms.c * c_scale);
727 cost_c0.push(terms.c0);
728 piecewise_linear.push(terms.piecewise_linear);
729 pmax.push(generator.pmax * p_scale);
730 pmin.push(generator.pmin * p_scale);
731 qmax.push(generator.qmax * p_scale);
732 qmin.push(generator.qmin * p_scale);
733 pg.push(generator.pg * p_scale);
734 qg.push(generator.qg * p_scale);
735 vg.push(generator.vg);
736 }
737
738 let mut storage_identities = Vec::new();
739 let mut bus_of_storage = Vec::new();
740 let mut storage_source_rows = Vec::new();
741 let mut storage_p = Vec::new();
742 let mut storage_q = Vec::new();
743 let mut storage_energy = Vec::new();
744 let mut storage_energy_rating = Vec::new();
745 let mut storage_charge_rating = Vec::new();
746 let mut storage_discharge_rating = Vec::new();
747 let mut storage_charge_efficiency = Vec::new();
748 let mut storage_discharge_efficiency = Vec::new();
749 let mut storage_s_max = Vec::new();
750 let mut storage_qmin = Vec::new();
751 let mut storage_qmax = Vec::new();
752 let mut storage_r = Vec::new();
753 let mut storage_x = Vec::new();
754 let mut storage_p_loss = Vec::new();
755 let mut storage_q_loss = Vec::new();
756 let mut storage_in_service = Vec::new();
757 for (source_row, storage) in case.network().storage().iter().enumerate() {
758 if !storage.in_service {
759 continue;
760 }
761 let analysis_bus = case
762 .bus_index(storage.bus)
763 .ok_or(powerio_tx::Error::UnknownBus {
764 bus_id: storage.bus,
765 element_index: source_row,
766 })?;
767 let Some(bus) = active_buses.dense_by_analysis[analysis_bus] else {
768 continue;
769 };
770 storage_identities.push(crate::opf::row_identity(
771 storage.uid.as_deref(),
772 "storage",
773 source_row,
774 ));
775 bus_of_storage.push(bus);
776 storage_source_rows.push(source_row);
777 storage_p.push(storage.ps * p_scale);
778 storage_q.push(storage.qs * p_scale);
779 storage_energy.push(storage.energy * p_scale);
780 storage_energy_rating.push(storage.energy_rating * p_scale);
781 storage_charge_rating.push(storage.charge_rating * p_scale);
782 storage_discharge_rating.push(storage.discharge_rating * p_scale);
783 storage_charge_efficiency.push(storage.charge_efficiency);
784 storage_discharge_efficiency.push(storage.discharge_efficiency);
785 storage_s_max.push(storage.thermal_rating * p_scale);
786 storage_qmin.push(storage.qmin * p_scale);
787 storage_qmax.push(storage.qmax * p_scale);
788 storage_r.push(storage.r);
789 storage_x.push(storage.x);
790 storage_p_loss.push(storage.p_loss * p_scale);
791 storage_q_loss.push(storage.q_loss * p_scale);
792 storage_in_service.push(storage.in_service);
793 }
794 let mut g_s: Vec<f64> = active_buses
795 .analysis_rows
796 .iter()
797 .map(|&row| case.gs()[row] * p_scale)
798 .collect();
799 let mut b_s: Vec<f64> = active_buses
800 .analysis_rows
801 .iter()
802 .map(|&row| case.bs()[row] * p_scale)
803 .collect();
804
805 let mut from_bus = Vec::new();
806 let mut branch_identities = Vec::new();
807 let mut to_bus = Vec::new();
808 let mut g = Vec::new();
809 let mut b = Vec::new();
810 let mut g_fr = Vec::new();
811 let mut b_fr = Vec::new();
812 let mut g_to = Vec::new();
813 let mut b_to = Vec::new();
814 let mut tap = Vec::new();
815 let mut shift = Vec::new();
816 let mut s_max = Vec::new();
817 let mut angle_min = Vec::new();
818 let mut angle_max = Vec::new();
819 let mut branch_rows = Vec::new();
820 let mut skipped_zero_impedance = Vec::new();
821 let network = case.network();
824
825 for (source_row, branch) in case.in_service_branches() {
826 let from_analysis = case
827 .bus_index(branch.from)
828 .ok_or(powerio_tx::Error::UnknownBus {
829 bus_id: branch.from,
830 element_index: source_row,
831 })?;
832 let to_analysis = case
833 .bus_index(branch.to)
834 .ok_or(powerio_tx::Error::UnknownBus {
835 bus_id: branch.to,
836 element_index: source_row,
837 })?;
838 let (Some(from), Some(to)) = (
839 active_buses.dense_by_analysis[from_analysis],
840 active_buses.dense_by_analysis[to_analysis],
841 ) else {
842 continue;
843 };
844 let Some((series_g, series_b)) = branch.calc_series_admittance(source_row)? else {
845 if options.skip_zero_impedance {
846 skipped_zero_impedance.push(source_row);
847 continue;
848 }
849 return Err(powerio_tx::Error::ZeroImpedance { row: source_row }.into());
850 };
851 let charging = branch.calc_terminal_charging();
852 if from == to {
853 let tap = branch.calc_divisible_tap(source_row)?;
858 let tap_squared = tap * tap;
859 let cross = 2.0 * case.to_radians(branch.shift).cos() / tap;
860 g_s[from] += ((series_g + charging.g_fr) / tap_squared + (series_g + charging.g_to)
861 - series_g * cross)
862 * y_scale;
863 b_s[from] += ((series_b + charging.b_fr) / tap_squared + (series_b + charging.b_to)
864 - series_b * cross)
865 * y_scale;
866 continue;
867 }
868 from_bus.push(from);
869 branch_identities.push(crate::opf::row_identity(
870 branch.uid.as_deref(),
871 "branches",
872 source_row,
873 ));
874 to_bus.push(to);
875 g.push(series_g * y_scale);
876 b.push(series_b * y_scale);
877 g_fr.push(charging.g_fr * y_scale);
878 b_fr.push(charging.b_fr * y_scale);
879 g_to.push(charging.g_to * y_scale);
880 b_to.push(charging.b_to * y_scale);
881 let source_amin = case.to_radians(branch.angmin);
882 let source_amax = case.to_radians(branch.angmax);
883 let (amin, amax) = if options.correct_angle_difference_bounds {
884 powerio_tx::correct_angle_difference_bounds(source_amin, source_amax)
885 } else {
886 (source_amin, source_amax)
887 };
888 tap.push(branch.calc_divisible_tap(source_row)?);
889 shift.push(case.to_radians(branch.shift));
890 s_max.push(thermal.of(
891 branch,
892 source_amin,
893 source_amax,
894 &network.buses()[from_analysis],
895 &network.buses()[to_analysis],
896 ));
897 angle_min.push(amin);
898 angle_max.push(amax);
899 branch_rows.push(source_row);
900 }
901
902 let mut vm_min = Vec::with_capacity(n_buses);
903 let mut vm_max = Vec::with_capacity(n_buses);
904 let mut initial_vm = Vec::with_capacity(n_buses);
905 let mut initial_va = Vec::with_capacity(n_buses);
906 for &analysis_row in &active_buses.analysis_rows {
907 let bus = &network.buses()[analysis_row];
908 vm_min.push(bus.vmin);
909 vm_max.push(bus.vmax);
910 initial_vm.push(bus.vm);
911 initial_va.push(case.to_radians(bus.va));
912 }
913 let n_active_generators = cost_q.len();
914 let n_active_branches = g.len();
915 let p_d = active_buses
916 .analysis_rows
917 .iter()
918 .map(|&row| case.pd()[row] * p_scale)
919 .collect();
920 let q_d = active_buses
921 .analysis_rows
922 .iter()
923 .map(|&row| case.qd()[row] * p_scale)
924 .collect();
925 let bus_analysis_rows = active_buses.analysis_rows;
926 let bus_source_rows = bus_analysis_rows.iter().copied().map(Some).collect();
927 Ok(AcOpfPreparation {
928 name: case.name().to_owned(),
929 n_buses,
930 n_source_generators: case.generators().len(),
931 n_source_branches: case.branches().len(),
932 base_mva: case.base_mva(),
933 units: options.units,
934 objective,
935 skip_zero_impedance: options.skip_zero_impedance,
936 synthesize_unrated_limits: options.synthesize_unrated_limits,
937 correct_angle_difference_bounds: options.correct_angle_difference_bounds,
938 bus_ids: active_buses.bus_ids,
939 bus_analysis_rows,
940 bus_source_rows,
941 reference_buses: active_buses.reference_buses,
942 buses: AcBusData {
943 p_d,
944 q_d,
945 g_s,
946 b_s,
947 vm_min,
948 vm_max,
949 initial_vm,
950 initial_va,
951 voltage_bound_active: vec![true; n_buses],
952 },
953 generators: AcGeneratorData {
954 identities: generator_identities,
955 bus_of_gen,
956 analysis_rows: generator_rows.clone(),
957 source_rows: generator_rows.into_iter().map(Some).collect(),
958 q: cost_q,
959 c: cost_c,
960 c0: cost_c0,
961 piecewise_linear,
962 pmax,
963 pmin,
964 qmax,
965 qmin,
966 pg,
967 qg,
968 vg,
969 capability_active: vec![true; n_active_generators],
970 },
971 storage: AcStorageData {
972 identities: storage_identities,
973 bus_of_storage,
974 source_rows: storage_source_rows,
975 p: storage_p,
976 q: storage_q,
977 energy: storage_energy,
978 energy_rating: storage_energy_rating,
979 charge_rating: storage_charge_rating,
980 discharge_rating: storage_discharge_rating,
981 charge_efficiency: storage_charge_efficiency,
982 discharge_efficiency: storage_discharge_efficiency,
983 s_max: storage_s_max,
984 qmin: storage_qmin,
985 qmax: storage_qmax,
986 r: storage_r,
987 x: storage_x,
988 p_loss: storage_p_loss,
989 q_loss: storage_q_loss,
990 in_service: storage_in_service,
991 },
992 branches: AcBranchData {
993 identities: branch_identities,
994 from_bus,
995 to_bus,
996 g,
997 b,
998 g_fr,
999 b_fr,
1000 g_to,
1001 b_to,
1002 tap,
1003 shift,
1004 s_max,
1005 angle_min,
1006 angle_max,
1007 analysis_rows: branch_rows.clone(),
1008 analysis_sources: branch_rows
1009 .iter()
1010 .copied()
1011 .map(|row| AnalysisBranchSource::Branch { row })
1012 .collect(),
1013 skipped_zero_impedance,
1014 thermal_limit_active: vec![true; n_active_branches],
1015 angle_bound_active: vec![true; n_active_branches],
1016 },
1017 })
1018}
1019
1020fn apply_instance_semantics(
1021 preparation: &mut AcOpfPreparation,
1022 source: &BalancedNetwork,
1023 constraints: &powerio_prob::ActiveConstraints,
1024) -> Result<()> {
1025 let source_generator_ids: Vec<String> = source
1026 .generators()
1027 .iter()
1028 .enumerate()
1029 .map(|(row, generator)| {
1030 crate::opf::row_identity(generator.uid.as_deref(), "generators", row)
1031 })
1032 .collect();
1033 let source_branch_ids: Vec<String> = source
1034 .branches()
1035 .iter()
1036 .enumerate()
1037 .map(|(row, branch)| crate::opf::row_identity(branch.uid.as_deref(), "branches", row))
1038 .collect();
1039 let mut analysis_bus_ids: Vec<String> = source
1040 .buses()
1041 .iter()
1042 .map(|bus| bus.id.to_string())
1043 .collect();
1044 for bus in &preparation.bus_ids {
1045 let identity = bus.to_string();
1046 if !analysis_bus_ids.iter().any(|known| known == &identity) {
1047 analysis_bus_ids.push(identity);
1048 }
1049 }
1050
1051 preparation.buses.voltage_bound_active = crate::opf::constraint_mask(
1052 "bus voltage bounds",
1053 &constraints.voltage_bounds,
1054 &analysis_bus_ids,
1055 &preparation
1056 .bus_ids
1057 .iter()
1058 .map(ToString::to_string)
1059 .collect::<Vec<_>>(),
1060 )?;
1061 preparation.generators.capability_active = crate::opf::constraint_mask(
1062 "generator capability",
1063 &constraints.generator_capability,
1064 &source_generator_ids,
1065 &preparation.generators.identities,
1066 )?;
1067 let mut analysis_branch_ids = source_branch_ids;
1068 analysis_branch_ids.extend(
1069 preparation
1070 .branches
1071 .identities
1072 .iter()
1073 .zip(&preparation.branches.analysis_rows)
1074 .filter(|(_, row)| **row >= source.branches().len())
1075 .map(|(identity, _)| identity.clone()),
1076 );
1077 preparation.branches.thermal_limit_active = crate::opf::constraint_mask(
1078 "branch thermal limits",
1079 &constraints.thermal_limits,
1080 &analysis_branch_ids,
1081 &preparation.branches.identities,
1082 )?;
1083 for (active, limit) in preparation
1084 .branches
1085 .thermal_limit_active
1086 .iter_mut()
1087 .zip(&preparation.branches.s_max)
1088 {
1089 *active &= *limit > 0.0;
1090 }
1091 preparation.branches.angle_bound_active = crate::opf::constraint_mask(
1092 "branch angle bounds",
1093 &constraints.angle_bounds,
1094 &analysis_branch_ids,
1095 &preparation.branches.identities,
1096 )?;
1097
1098 apply_source_mappings(preparation, source);
1099 Ok(())
1100}
1101
1102fn apply_source_mappings(preparation: &mut AcOpfPreparation, source: &BalancedNetwork) {
1103 preparation.n_source_generators = source.generators().len();
1104 preparation.n_source_branches = source.branches().len();
1105 preparation.bus_source_rows = preparation
1106 .bus_analysis_rows
1107 .iter()
1108 .map(|&row| (row < source.buses().len()).then_some(row))
1109 .collect();
1110 preparation.generators.source_rows = preparation
1111 .generators
1112 .analysis_rows
1113 .iter()
1114 .map(|&row| (row < source.generators().len()).then_some(row))
1115 .collect();
1116 let analysis_sources = crate::opf::analysis_branch_sources(source);
1117 preparation.branches.analysis_sources = preparation
1118 .branches
1119 .analysis_rows
1120 .iter()
1121 .map(|&row| analysis_sources[row])
1122 .collect();
1123}