Skip to main content

surge_solution/
dispatch_apply.rs

1// SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0
2//! Apply OPF/SCED dispatch solutions back into a Network.
3//!
4//! These functions close the gap between the dispatch/OPF solvers and the
5//! power flow / N-1 contingency analysis solvers. After solving OPF or SCED,
6//! call one of these to stamp the optimal dispatch onto the network so that
7//! a subsequent `solve_ac_pf`, `compute_n1`, or any other network analysis
8//! operates at the dispatched operating point.
9
10use std::collections::HashMap;
11
12use thiserror::Error;
13
14use surge_network::Network;
15
16use crate::OpfSolution;
17
18/// Error returned when applying solver outputs back into a [`Network`].
19#[derive(Debug, Error, PartialEq, Eq)]
20pub enum ApplySolutionError {
21    #[error(
22        "generator dispatch length mismatch: expected {expected} in-service generators, got {actual}"
23    )]
24    GeneratorDispatchLengthMismatch { expected: usize, actual: usize },
25    #[error(
26        "generator reactive dispatch length mismatch: expected {expected} in-service generators, got {actual}"
27    )]
28    GeneratorReactiveDispatchLengthMismatch { expected: usize, actual: usize },
29    #[error(
30        "generator bus mapping length mismatch: expected {expected} in-service generators, got {actual}"
31    )]
32    GeneratorBusMappingLengthMismatch { expected: usize, actual: usize },
33    #[error(
34        "generator machine-id mapping length mismatch: expected {expected} in-service generators, got {actual}"
35    )]
36    GeneratorMachineIdMappingLengthMismatch { expected: usize, actual: usize },
37    #[error(
38        "generator ID mapping length mismatch: expected {expected} in-service generators, got {actual}"
39    )]
40    GeneratorIdMappingLengthMismatch { expected: usize, actual: usize },
41    #[error(
42        "generator identity mismatch at OPF position {position}: expected ({expected_bus}, {expected_machine_id}), got ({actual_bus}, {actual_machine_id})"
43    )]
44    GeneratorIdentityMismatch {
45        position: usize,
46        expected_bus: u32,
47        expected_machine_id: String,
48        actual_bus: u32,
49        actual_machine_id: String,
50    },
51    #[error(
52        "ambiguous generator identity ({bus}, {machine_id}) in OPF replay; generator identities must be unique among in-service units"
53    )]
54    AmbiguousGeneratorIdentity { bus: u32, machine_id: String },
55    #[error(
56        "duplicate generator ID {generator_id} in OPF replay mapping; solution generator IDs must be unique"
57    )]
58    DuplicateGeneratorIdInSolution { generator_id: String },
59    #[error(
60        "generator ID mismatch at OPF position {position}: expected {expected_id}, got {actual_id}"
61    )]
62    GeneratorIdMismatch {
63        position: usize,
64        expected_id: String,
65        actual_id: String,
66    },
67    #[error(
68        "ambiguous generator ID {generator_id} in target network; generator IDs must be unique"
69    )]
70    AmbiguousGeneratorIdInNetwork { generator_id: String },
71    #[error("bus voltage magnitude length mismatch: expected {expected} buses, got {actual}")]
72    VoltageMagnitudeLengthMismatch { expected: usize, actual: usize },
73    #[error("bus voltage angle length mismatch: expected {expected} buses, got {actual}")]
74    VoltageAngleLengthMismatch { expected: usize, actual: usize },
75    #[error(
76        "bus voltage bus-number mapping length mismatch: expected {expected} buses, got {actual}"
77    )]
78    VoltageBusNumberLengthMismatch { expected: usize, actual: usize },
79    #[error("bus number {bus_number} from the solution was not found in the target network")]
80    VoltageBusNumberNotFound { bus_number: u32 },
81    #[error("duplicate bus number {bus_number} in target network")]
82    DuplicateBusNumberInNetwork { bus_number: u32 },
83    #[error("duplicate bus number {bus_number} in solution bus-voltage mapping")]
84    DuplicateBusNumberInSolution { bus_number: u32 },
85}
86
87fn in_service_generator_count(network: &Network) -> usize {
88    network.generators.iter().filter(|g| g.in_service).count()
89}
90
91fn normalized_machine_id(machine_id: Option<&str>) -> &str {
92    machine_id.unwrap_or("1")
93}
94
95fn canonical_in_service_generator_id_map(
96    network: &Network,
97) -> Result<HashMap<String, usize>, ApplySolutionError> {
98    let mut canonical_network = Network::new("opf_replay_ids");
99    let mut source_indices = Vec::new();
100
101    for (idx, generator) in network.generators.iter().enumerate() {
102        if !generator.in_service {
103            continue;
104        }
105        canonical_network.generators.push(generator.clone());
106        source_indices.push(idx);
107    }
108    canonical_network.canonicalize_generator_ids();
109
110    let mut id_map = HashMap::new();
111    for (canonical_generator, &source_idx) in canonical_network
112        .generators
113        .iter()
114        .zip(source_indices.iter())
115    {
116        if id_map
117            .insert(canonical_generator.id.clone(), source_idx)
118            .is_some()
119        {
120            return Err(ApplySolutionError::AmbiguousGeneratorIdInNetwork {
121                generator_id: canonical_generator.id.clone(),
122            });
123        }
124    }
125    Ok(id_map)
126}
127
128/// Apply an OPF solution's generator dispatch and (optionally) bus voltages
129/// back into a [`Network`].
130///
131/// Only in-service generators are updated. When generator identity metadata is
132/// present in the solution, dispatch is matched by `(bus, machine_id)` rather
133/// than by the current generator array order. Bus voltages are stamped by
134/// external bus number when either voltage vector is populated.
135pub fn apply_opf_dispatch(
136    network: &mut Network,
137    sol: &OpfSolution,
138) -> Result<(), ApplySolutionError> {
139    let expected_generators = in_service_generator_count(network);
140    if sol.generators.gen_p_mw.len() != expected_generators {
141        return Err(ApplySolutionError::GeneratorDispatchLengthMismatch {
142            expected: expected_generators,
143            actual: sol.generators.gen_p_mw.len(),
144        });
145    }
146    if !sol.generators.gen_q_mvar.is_empty()
147        && sol.generators.gen_q_mvar.len() != expected_generators
148    {
149        return Err(
150            ApplySolutionError::GeneratorReactiveDispatchLengthMismatch {
151                expected: expected_generators,
152                actual: sol.generators.gen_q_mvar.len(),
153            },
154        );
155    }
156    if !sol.generators.gen_bus_numbers.is_empty()
157        && sol.generators.gen_bus_numbers.len() != expected_generators
158    {
159        return Err(ApplySolutionError::GeneratorBusMappingLengthMismatch {
160            expected: expected_generators,
161            actual: sol.generators.gen_bus_numbers.len(),
162        });
163    }
164    if !sol.generators.gen_machine_ids.is_empty()
165        && sol.generators.gen_machine_ids.len() != expected_generators
166    {
167        return Err(
168            ApplySolutionError::GeneratorMachineIdMappingLengthMismatch {
169                expected: expected_generators,
170                actual: sol.generators.gen_machine_ids.len(),
171            },
172        );
173    }
174    if !sol.generators.gen_ids.is_empty() && sol.generators.gen_ids.len() != expected_generators {
175        return Err(ApplySolutionError::GeneratorIdMappingLengthMismatch {
176            expected: expected_generators,
177            actual: sol.generators.gen_ids.len(),
178        });
179    }
180
181    let mut generator_dispatch_applied = false;
182    if !sol.generators.gen_ids.is_empty() {
183        let network_indices = canonical_in_service_generator_id_map(network)?;
184        let mut seen_solution_ids = HashMap::new();
185        let mut pending_updates = Vec::with_capacity(sol.generators.gen_ids.len());
186        for (j, generator_id) in sol.generators.gen_ids.iter().enumerate() {
187            if seen_solution_ids.insert(generator_id.clone(), j).is_some() {
188                return Err(ApplySolutionError::DuplicateGeneratorIdInSolution {
189                    generator_id: generator_id.clone(),
190                });
191            }
192            let Some(&generator_index) = network_indices.get(generator_id) else {
193                if !sol.generators.gen_bus_numbers.is_empty()
194                    && !sol.generators.gen_machine_ids.is_empty()
195                {
196                    pending_updates.clear();
197                    break;
198                }
199                let actual_id = network
200                    .generators
201                    .iter()
202                    .filter(|g| g.in_service)
203                    .nth(j)
204                    .map(|g| g.id.clone())
205                    .unwrap_or_else(|| "<missing>".to_string());
206                return Err(ApplySolutionError::GeneratorIdMismatch {
207                    position: j,
208                    expected_id: generator_id.clone(),
209                    actual_id,
210                });
211            };
212            pending_updates.push((generator_index, j));
213        }
214
215        if !pending_updates.is_empty() {
216            for (generator_index, j) in pending_updates {
217                let generator = &mut network.generators[generator_index];
218                generator.p = sol.generators.gen_p_mw[j];
219                if !sol.generators.gen_q_mvar.is_empty() {
220                    generator.q = sol.generators.gen_q_mvar[j];
221                }
222            }
223            generator_dispatch_applied = true;
224        }
225    }
226
227    if !generator_dispatch_applied && !sol.generators.gen_bus_numbers.is_empty() {
228        if sol.generators.gen_machine_ids.len() != expected_generators {
229            return Err(
230                ApplySolutionError::GeneratorMachineIdMappingLengthMismatch {
231                    expected: expected_generators,
232                    actual: sol.generators.gen_machine_ids.len(),
233                },
234            );
235        }
236
237        let mut generator_indices = HashMap::new();
238        for (idx, generator) in network.generators.iter().enumerate() {
239            if !generator.in_service {
240                continue;
241            }
242            let key = (
243                generator.bus,
244                normalized_machine_id(generator.machine_id.as_deref()).to_string(),
245            );
246            if generator_indices.insert(key.clone(), idx).is_some() {
247                return Err(ApplySolutionError::AmbiguousGeneratorIdentity {
248                    bus: key.0,
249                    machine_id: key.1,
250                });
251            }
252        }
253
254        for (j, (&expected_bus, expected_machine_id)) in sol
255            .generators
256            .gen_bus_numbers
257            .iter()
258            .zip(sol.generators.gen_machine_ids.iter())
259            .enumerate()
260        {
261            let Some(&generator_index) =
262                generator_indices.get(&(expected_bus, expected_machine_id.clone()))
263            else {
264                let actual = network
265                    .generators
266                    .iter()
267                    .filter(|g| g.in_service)
268                    .nth(j)
269                    .map(|g| {
270                        (
271                            g.bus,
272                            normalized_machine_id(g.machine_id.as_deref()).to_string(),
273                        )
274                    })
275                    .unwrap_or((0, "<missing>".to_string()));
276                return Err(ApplySolutionError::GeneratorIdentityMismatch {
277                    position: j,
278                    expected_bus,
279                    expected_machine_id: expected_machine_id.clone(),
280                    actual_bus: actual.0,
281                    actual_machine_id: actual.1,
282                });
283            };
284
285            let generator = &mut network.generators[generator_index];
286            generator.p = sol.generators.gen_p_mw[j];
287            if !sol.generators.gen_q_mvar.is_empty() {
288                generator.q = sol.generators.gen_q_mvar[j];
289            }
290        }
291        generator_dispatch_applied = true;
292    }
293
294    if !generator_dispatch_applied {
295        let mut j = 0usize;
296        for g in network.generators.iter_mut() {
297            if !g.in_service {
298                continue;
299            }
300            g.p = sol.generators.gen_p_mw[j];
301            if !sol.generators.gen_q_mvar.is_empty() {
302                g.q = sol.generators.gen_q_mvar[j];
303            }
304            j += 1;
305        }
306    }
307
308    if !sol.power_flow.voltage_magnitude_pu.is_empty()
309        || !sol.power_flow.voltage_angle_rad.is_empty()
310    {
311        apply_bus_voltages_by_bus_number(
312            network,
313            &sol.power_flow.bus_numbers,
314            &sol.power_flow.voltage_magnitude_pu,
315            &sol.power_flow.voltage_angle_rad,
316        )?;
317    }
318
319    Ok(())
320}
321
322/// Apply a generator dispatch vector (MW, one per in-service generator) to
323/// the network.
324///
325/// Use this with SCED/SCUC period results:
326/// ```ignore
327/// apply_dispatch_mw(network, &period.gen_p_mw)?;
328/// ```
329///
330/// Only in-service generators are updated. Out-of-service generators are
331/// skipped and do not consume an index from `gen_p_mw`.
332pub fn apply_dispatch_mw(
333    network: &mut Network,
334    gen_p_mw: &[f64],
335) -> Result<(), ApplySolutionError> {
336    let expected_generators = in_service_generator_count(network);
337    if gen_p_mw.len() != expected_generators {
338        return Err(ApplySolutionError::GeneratorDispatchLengthMismatch {
339            expected: expected_generators,
340            actual: gen_p_mw.len(),
341        });
342    }
343
344    let mut j = 0usize;
345    for g in network.generators.iter_mut() {
346        if !g.in_service {
347            continue;
348        }
349        g.p = gen_p_mw[j];
350        j += 1;
351    }
352
353    Ok(())
354}
355
356/// Stamp bus voltage magnitudes and angles onto the network by external bus number.
357///
358/// `bus_numbers`, `vm`, and `va` must all have one entry per bus in the solution.
359/// Each `bus_numbers[i]` is matched against the target network and the
360/// corresponding voltage values are stamped onto that bus.
361pub fn apply_bus_voltages_by_bus_number(
362    network: &mut Network,
363    bus_numbers: &[u32],
364    vm: &[f64],
365    va: &[f64],
366) -> Result<(), ApplySolutionError> {
367    if bus_numbers.len() != network.buses.len() {
368        return Err(ApplySolutionError::VoltageBusNumberLengthMismatch {
369            expected: network.buses.len(),
370            actual: bus_numbers.len(),
371        });
372    }
373    if vm.len() != network.buses.len() {
374        return Err(ApplySolutionError::VoltageMagnitudeLengthMismatch {
375            expected: network.buses.len(),
376            actual: vm.len(),
377        });
378    }
379    if va.len() != network.buses.len() {
380        return Err(ApplySolutionError::VoltageAngleLengthMismatch {
381            expected: network.buses.len(),
382            actual: va.len(),
383        });
384    }
385
386    let mut bus_indices = HashMap::new();
387    for (idx, bus) in network.buses.iter().enumerate() {
388        if bus_indices.insert(bus.number, idx).is_some() {
389            return Err(ApplySolutionError::DuplicateBusNumberInNetwork {
390                bus_number: bus.number,
391            });
392        }
393    }
394
395    let mut seen_solution_bus_numbers = HashMap::new();
396    for (i, &bus_number) in bus_numbers.iter().enumerate() {
397        if seen_solution_bus_numbers.insert(bus_number, i).is_some() {
398            return Err(ApplySolutionError::DuplicateBusNumberInSolution { bus_number });
399        }
400        let Some(&bus_idx) = bus_indices.get(&bus_number) else {
401            return Err(ApplySolutionError::VoltageBusNumberNotFound { bus_number });
402        };
403        let bus = &mut network.buses[bus_idx];
404        bus.voltage_magnitude_pu = vm[i];
405        bus.voltage_angle_rad = va[i];
406    }
407
408    Ok(())
409}
410
411/// Stamp bus voltage magnitudes and angles onto the network in current bus order.
412///
413/// This low-level helper is positional: `vm` and `va` must be aligned with the
414/// current `network.buses` array. Prefer [`apply_bus_voltages_by_bus_number`]
415/// when replaying solved voltages back into a possibly re-ordered network.
416pub fn apply_bus_voltages(
417    network: &mut Network,
418    vm: &[f64],
419    va: &[f64],
420) -> Result<(), ApplySolutionError> {
421    if vm.len() != network.buses.len() {
422        return Err(ApplySolutionError::VoltageMagnitudeLengthMismatch {
423            expected: network.buses.len(),
424            actual: vm.len(),
425        });
426    }
427    if va.len() != network.buses.len() {
428        return Err(ApplySolutionError::VoltageAngleLengthMismatch {
429            expected: network.buses.len(),
430            actual: va.len(),
431        });
432    }
433
434    for (i, bus) in network.buses.iter_mut().enumerate() {
435        bus.voltage_magnitude_pu = vm[i];
436        bus.voltage_angle_rad = va[i];
437    }
438
439    Ok(())
440}
441
442#[cfg(test)]
443mod tests {
444    use super::*;
445    use crate::{OpfGeneratorResults, PfSolution};
446    use surge_network::network::{Bus, BusType, Generator};
447    fn make_generator(bus: u32, in_service: bool) -> Generator {
448        let mut g = Generator::new(bus, 0.0, 1.0);
449        g.in_service = in_service;
450        g
451    }
452
453    fn make_opf_solution(
454        gen_p_mw: Vec<f64>,
455        gen_q_mvar: Vec<f64>,
456        voltage_magnitude_pu: Vec<f64>,
457        voltage_angle_rad: Vec<f64>,
458    ) -> OpfSolution {
459        OpfSolution {
460            power_flow: PfSolution {
461                voltage_magnitude_pu,
462                voltage_angle_rad,
463                ..Default::default()
464            },
465            generators: OpfGeneratorResults {
466                gen_p_mw,
467                gen_q_mvar,
468                ..Default::default()
469            },
470            ..Default::default()
471        }
472    }
473
474    #[test]
475    fn test_apply_opf_dispatch_pg_qg() {
476        let mut net = Network::new("test");
477        let mut g0 = make_generator(1, true);
478        g0.machine_id = Some("A".to_string());
479        let mut g1 = make_generator(2, false);
480        g1.machine_id = Some("B".to_string());
481        let mut g2 = make_generator(3, true);
482        g2.machine_id = Some("C".to_string());
483        net.generators.push(g0);
484        net.generators.push(g1);
485        net.generators.push(g2);
486        net.canonicalize_generator_ids();
487
488        let mut sol = make_opf_solution(vec![100.0, 200.0], vec![30.0, 60.0], vec![], vec![]);
489        sol.generators.gen_ids = vec![net.generators[0].id.clone(), net.generators[2].id.clone()];
490
491        apply_opf_dispatch(&mut net, &sol).unwrap();
492
493        assert_eq!(net.generators[0].p, 100.0);
494        assert_eq!(net.generators[0].q, 30.0);
495        assert_eq!(net.generators[1].p, 0.0);
496        assert_eq!(net.generators[1].q, 0.0);
497        assert_eq!(net.generators[2].p, 200.0);
498        assert_eq!(net.generators[2].q, 60.0);
499    }
500
501    #[test]
502    fn test_apply_dispatch_mw_skips_out_of_service() {
503        let mut net = Network::new("test");
504        net.generators.push(make_generator(1, true));
505        net.generators.push(make_generator(2, false));
506        net.generators.push(make_generator(3, true));
507
508        apply_dispatch_mw(&mut net, &[150.0, 250.0]).unwrap();
509
510        assert_eq!(net.generators[0].p, 150.0);
511        assert_eq!(net.generators[1].p, 0.0);
512        assert_eq!(net.generators[2].p, 250.0);
513    }
514
515    #[test]
516    fn test_apply_dispatch_rejects_short_generator_vector() {
517        let mut net = Network::new("test");
518        net.generators.push(make_generator(1, true));
519        net.generators.push(make_generator(2, true));
520
521        let err = apply_dispatch_mw(&mut net, &[150.0]).unwrap_err();
522        assert_eq!(
523            err,
524            ApplySolutionError::GeneratorDispatchLengthMismatch {
525                expected: 2,
526                actual: 1,
527            }
528        );
529    }
530
531    #[test]
532    fn test_apply_bus_voltages_requires_full_vectors() {
533        let mut net = Network::new("test");
534        net.buses.push(Bus::new(1, BusType::Slack, 138.0));
535        net.buses.push(Bus::new(2, BusType::PQ, 138.0));
536
537        let err = apply_bus_voltages(&mut net, &[1.0], &[0.0, 0.1]).unwrap_err();
538        assert_eq!(
539            err,
540            ApplySolutionError::VoltageMagnitudeLengthMismatch {
541                expected: 2,
542                actual: 1,
543            }
544        );
545    }
546
547    #[test]
548    fn test_apply_bus_voltages_by_bus_number_matches_identity() {
549        let mut net = Network::new("test");
550        net.buses.push(Bus::new(20, BusType::Slack, 138.0));
551        net.buses.push(Bus::new(10, BusType::PQ, 138.0));
552
553        apply_bus_voltages_by_bus_number(&mut net, &[10, 20], &[1.02, 1.05], &[0.1, -0.2]).unwrap();
554
555        assert_eq!(net.buses[0].number, 20);
556        assert!((net.buses[0].voltage_magnitude_pu - 1.05).abs() < 1e-12);
557        assert!((net.buses[0].voltage_angle_rad + 0.2).abs() < 1e-12);
558        assert_eq!(net.buses[1].number, 10);
559        assert!((net.buses[1].voltage_magnitude_pu - 1.02).abs() < 1e-12);
560        assert!((net.buses[1].voltage_angle_rad - 0.1).abs() < 1e-12);
561    }
562
563    #[test]
564    fn test_apply_opf_dispatch_requires_voltage_bus_numbers() {
565        let mut net = Network::new("test");
566        net.buses.push(Bus::new(10, BusType::Slack, 138.0));
567        net.buses.push(Bus::new(20, BusType::PQ, 138.0));
568        net.generators.push(make_generator(10, true));
569
570        let sol = make_opf_solution(vec![100.0], vec![], vec![1.0, 0.98], vec![0.0, -0.1]);
571        let err = apply_opf_dispatch(&mut net, &sol).unwrap_err();
572        assert_eq!(
573            err,
574            ApplySolutionError::VoltageBusNumberLengthMismatch {
575                expected: 2,
576                actual: 0,
577            }
578        );
579    }
580
581    #[test]
582    fn test_apply_opf_dispatch_matches_generators_by_identity() {
583        let mut net = Network::new("test");
584        let mut gen_a = make_generator(10, true);
585        gen_a.machine_id = Some("A".to_string());
586        let mut gen_b = make_generator(20, true);
587        gen_b.machine_id = Some("B".to_string());
588        net.generators.push(gen_b);
589        net.generators.push(gen_a);
590        net.canonicalize_generator_ids();
591
592        let mut sol = make_opf_solution(vec![75.0, 125.0], vec![15.0, 25.0], vec![], vec![]);
593        sol.generators.gen_ids = vec![net.generators[1].id.clone(), net.generators[0].id.clone()];
594
595        apply_opf_dispatch(&mut net, &sol).unwrap();
596
597        assert_eq!(net.generators[0].p, 125.0);
598        assert_eq!(net.generators[0].q, 25.0);
599        assert_eq!(net.generators[1].p, 75.0);
600        assert_eq!(net.generators[1].q, 15.0);
601    }
602
603    #[test]
604    fn test_apply_opf_dispatch_matches_bus_voltages_by_identity() {
605        let mut net = Network::new("test");
606        net.buses.push(Bus::new(20, BusType::Slack, 138.0));
607        net.buses.push(Bus::new(10, BusType::PQ, 138.0));
608
609        let mut sol = make_opf_solution(vec![], vec![], vec![1.02, 1.05], vec![0.1, -0.2]);
610        sol.power_flow.bus_numbers = vec![10, 20];
611
612        apply_opf_dispatch(&mut net, &sol).unwrap();
613
614        assert!((net.buses[0].voltage_magnitude_pu - 1.05).abs() < 1e-12);
615        assert!((net.buses[0].voltage_angle_rad + 0.2).abs() < 1e-12);
616        assert!((net.buses[1].voltage_magnitude_pu - 1.02).abs() < 1e-12);
617        assert!((net.buses[1].voltage_angle_rad - 0.1).abs() < 1e-12);
618    }
619
620    #[test]
621    fn test_apply_opf_dispatch_rejects_identity_mismatch() {
622        let mut net = Network::new("test");
623        let mut generator = make_generator(10, true);
624        generator.machine_id = Some("A".to_string());
625        net.generators.push(generator);
626
627        let mut sol = make_opf_solution(vec![75.0], vec![], vec![], vec![]);
628        sol.generators.gen_ids = vec!["missing-generator".to_string()];
629
630        let err = apply_opf_dispatch(&mut net, &sol).unwrap_err();
631        assert_eq!(
632            err,
633            ApplySolutionError::GeneratorIdMismatch {
634                position: 0,
635                expected_id: "missing-generator".to_string(),
636                actual_id: net.generators[0].id.clone(),
637            }
638        );
639    }
640
641    #[test]
642    fn test_apply_opf_dispatch_rejects_duplicate_solution_generator_ids() {
643        let mut net = Network::new("test");
644        net.generators.push(make_generator(10, true));
645        net.generators.push(make_generator(10, true));
646        net.canonicalize_generator_ids();
647
648        let mut sol = make_opf_solution(vec![75.0, 125.0], vec![], vec![], vec![]);
649        sol.generators.gen_ids = vec![net.generators[0].id.clone(), net.generators[0].id.clone()];
650
651        let err = apply_opf_dispatch(&mut net, &sol).unwrap_err();
652        assert_eq!(
653            err,
654            ApplySolutionError::DuplicateGeneratorIdInSolution {
655                generator_id: net.generators[0].id.clone(),
656            }
657        );
658    }
659
660    #[test]
661    fn test_apply_opf_dispatch_rejects_partial_voltage_vectors() {
662        let mut net = Network::new("test");
663        net.buses.push(Bus::new(1, BusType::Slack, 138.0));
664        net.buses.push(Bus::new(2, BusType::PQ, 138.0));
665
666        let sol = make_opf_solution(vec![], vec![], vec![], vec![0.0, 0.1]);
667
668        let err = apply_opf_dispatch(&mut net, &sol).unwrap_err();
669        assert_eq!(
670            err,
671            ApplySolutionError::VoltageBusNumberLengthMismatch {
672                expected: 2,
673                actual: 0,
674            }
675        );
676    }
677
678    #[test]
679    fn test_apply_opf_dispatch_matches_canonicalized_generator_ids() {
680        let mut net = Network::new("test");
681        let mut gen_a = make_generator(10, true);
682        gen_a.machine_id = Some("A".to_string());
683        gen_a.id = "  ".to_string();
684        let mut gen_b = make_generator(20, true);
685        gen_b.machine_id = Some("B".to_string());
686        gen_b.id = "\t".to_string();
687        net.generators.push(gen_b);
688        net.generators.push(gen_a);
689
690        let mut canonical = net.clone();
691        canonical.canonicalize_generator_ids();
692
693        let mut sol = make_opf_solution(vec![75.0, 125.0], vec![15.0, 25.0], vec![], vec![]);
694        sol.generators.gen_ids = vec![
695            canonical.generators[1].id.clone(),
696            canonical.generators[0].id.clone(),
697        ];
698
699        apply_opf_dispatch(&mut net, &sol).unwrap();
700
701        assert_eq!(net.generators[0].p, 125.0);
702        assert_eq!(net.generators[0].q, 25.0);
703        assert_eq!(net.generators[1].p, 75.0);
704        assert_eq!(net.generators[1].q, 15.0);
705    }
706}