1use std::collections::HashMap;
15use std::io::{BufReader, Read, Write};
16use std::path::Path;
17
18use serde::{Deserialize, Serialize, de::DeserializeOwned};
19use surge_network::dynamics::{CoreLossModel, CoreType, TransformerSaturation};
20use surge_network::market::{
21 AmbientConditions, CombinedCyclePlant, CostCurve, DispatchableLoad, EmissionPolicy,
22 EmissionRates, EnergyOffer, MarketRules, OutageEntry, PumpedHydroUnit, QualificationMap,
23 ReserveOffer, ReserveZone,
24};
25use surge_network::network::asset::AssetCatalog;
26use surge_network::network::boundary::BoundaryData;
27use surge_network::network::breaker::BreakerRating;
28use surge_network::network::flowgate::OperatingNomogram;
29use surge_network::network::grounding::GroundingEntry;
30use surge_network::network::impedance_correction::ImpedanceCorrectionTable;
31use surge_network::network::induction_machine::InductionMachine;
32use surge_network::network::market_data::MarketData;
33use surge_network::network::measurement::CimMeasurement;
34use surge_network::network::model::{GeoPoint, MutualCoupling, PhaseImpedanceEntry};
35use surge_network::network::multi_section_line::MultiSectionLineGroup;
36use surge_network::network::net_ops::NetworkOperationsData;
37use surge_network::network::op_limits::OperationalLimits;
38use surge_network::network::protection::ProtectionData;
39use surge_network::network::scheduled_area_transfer::ScheduledAreaTransfer;
40use surge_network::network::{
41 AreaSchedule, Branch, BranchConditionalRatings, BranchOpfControl, BranchType, Bus, BusType,
42 FactsDevice, FixedShunt, Flowgate, FuelSupply, GenType, Generator, GeneratorTechnology,
43 HarmonicData, HvdcModel, Interface, LineData, Load, LoadClass, LoadConnection, Network,
44 NodeBreakerTopology, OltcSpec, Owner, OwnershipEntry, ParSpec, PhaseMode, PowerInjection,
45 Region, SeriesCompData, StorageParams, SwitchedShunt, SwitchedShuntOpf, TapMode,
46 TransformerConnection, TransformerData, WindingConnection, ZeroSeqData,
47};
48use thiserror::Error;
49
50pub const SURGE_BIN_FORMAT: &str = "surge-bin";
51pub const SURGE_BIN_SCHEMA_VERSION: &str = "0.1.0";
52
53const MAGIC: &[u8; 8] = b"SRGBIN02";
54const BINARY_REVISION: u16 = 2;
55const HEADER_LEN: usize = 8 + 2 + 2 + 4 + 8 + 4;
56const SECTION_HEADER_LEN: usize = 2 + 2 + 4 + 8;
57
58const SECTION_NETWORK_HEADER: u16 = 1;
59const SECTION_STRINGS: u16 = 2;
60const SECTION_BUSES: u16 = 3;
61const SECTION_BUS_EXTRAS: u16 = 4;
62const SECTION_BRANCHES: u16 = 5;
63const SECTION_BRANCH_EXTRAS: u16 = 6;
64const SECTION_GENERATORS: u16 = 7;
65const SECTION_GENERATOR_EXTRAS: u16 = 8;
66const SECTION_LOADS: u16 = 9;
67const SECTION_NETWORK_EXTRAS: u16 = 10;
68const SECTION_LOAD_EXTRAS: u16 = 11;
69
70#[derive(Error, Debug)]
71pub enum Error {
72 #[error("I/O error: {0}")]
73 Io(#[from] std::io::Error),
74
75 #[error("CBOR encode error: {0}")]
76 Encode(#[from] ciborium::ser::Error<std::io::Error>),
77
78 #[error("CBOR decode error: {0}")]
79 Decode(#[from] ciborium::de::Error<std::io::Error>),
80
81 #[error("invalid binary document: {0}")]
82 InvalidDocument(String),
83}
84
85#[derive(Clone)]
86struct Section {
87 id: u16,
88 count: u32,
89 payload: Vec<u8>,
90}
91
92#[derive(Clone, Copy)]
93struct SectionView<'a> {
94 count: u32,
95 payload: &'a [u8],
96}
97
98#[derive(Default)]
99struct StringTableBuilder {
100 ids: HashMap<String, u32>,
101 values: Vec<String>,
102}
103
104impl StringTableBuilder {
105 fn new() -> Self {
106 let mut ids = HashMap::new();
107 ids.insert(String::new(), 0);
108 Self {
109 ids,
110 values: vec![String::new()],
111 }
112 }
113
114 fn intern(&mut self, value: &str) -> u32 {
115 if let Some(id) = self.ids.get(value) {
116 return *id;
117 }
118 let id = self.values.len() as u32;
119 let owned = value.to_string();
120 self.ids.insert(owned.clone(), id);
121 self.values.push(owned);
122 id
123 }
124
125 fn freeze(self) -> StringTable {
126 StringTable {
127 values: self.values,
128 }
129 }
130}
131
132struct StringTable {
133 values: Vec<String>,
134}
135
136impl StringTable {
137 fn resolve(&self, id: u32) -> Result<&str, Error> {
138 self.values
139 .get(id as usize)
140 .map(String::as_str)
141 .ok_or_else(|| Error::InvalidDocument(format!("invalid string table index {id}")))
142 }
143}
144
145struct Reader<'a> {
146 bytes: &'a [u8],
147 pos: usize,
148}
149
150impl<'a> Reader<'a> {
151 fn new(bytes: &'a [u8]) -> Self {
152 Self { bytes, pos: 0 }
153 }
154
155 fn remaining(&self) -> usize {
156 self.bytes.len().saturating_sub(self.pos)
157 }
158
159 fn is_empty(&self) -> bool {
160 self.pos == self.bytes.len()
161 }
162
163 fn take(&mut self, len: usize) -> Result<&'a [u8], Error> {
164 let end = self
165 .pos
166 .checked_add(len)
167 .ok_or_else(|| Error::InvalidDocument("binary document length overflow".to_string()))?;
168 if end > self.bytes.len() {
169 return Err(Error::InvalidDocument(format!(
170 "truncated binary payload: need {len} bytes, have {}",
171 self.remaining()
172 )));
173 }
174 let slice = &self.bytes[self.pos..end];
175 self.pos = end;
176 Ok(slice)
177 }
178
179 fn u8(&mut self) -> Result<u8, Error> {
180 Ok(self.take(1)?[0])
181 }
182
183 fn u16(&mut self) -> Result<u16, Error> {
184 let mut bytes = [0u8; 2];
185 bytes.copy_from_slice(self.take(2)?);
186 Ok(u16::from_le_bytes(bytes))
187 }
188
189 fn u32(&mut self) -> Result<u32, Error> {
190 let mut bytes = [0u8; 4];
191 bytes.copy_from_slice(self.take(4)?);
192 Ok(u32::from_le_bytes(bytes))
193 }
194
195 fn u64(&mut self) -> Result<u64, Error> {
196 let mut bytes = [0u8; 8];
197 bytes.copy_from_slice(self.take(8)?);
198 Ok(u64::from_le_bytes(bytes))
199 }
200
201 fn f64(&mut self) -> Result<f64, Error> {
202 let mut bytes = [0u8; 8];
203 bytes.copy_from_slice(self.take(8)?);
204 Ok(f64::from_le_bytes(bytes))
205 }
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
209struct IndexedBusExtra {
210 index: u32,
211 extra: BusExtra,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
215struct BusExtra {
216 latitude: Option<f64>,
217 longitude: Option<f64>,
218 freq_hz: Option<f64>,
219 ambient: Option<AmbientConditions>,
220 reserve_zone: Option<String>,
221 owners: Vec<OwnershipEntry>,
222}
223
224impl BusExtra {
225 fn from_bus(bus: &Bus) -> Option<Self> {
226 let extra = Self {
227 latitude: bus.latitude,
228 longitude: bus.longitude,
229 freq_hz: bus.freq_hz,
230 ambient: bus.ambient.clone(),
231 reserve_zone: bus.reserve_zone.clone(),
232 owners: bus.owners.clone(),
233 };
234 (!extra.is_empty()).then_some(extra)
235 }
236
237 fn is_empty(&self) -> bool {
238 self.latitude.is_none()
239 && self.longitude.is_none()
240 && self.freq_hz.is_none()
241 && self.ambient.is_none()
242 && self.reserve_zone.is_none()
243 && self.owners.is_empty()
244 }
245
246 fn apply(self, bus: &mut Bus) {
247 bus.latitude = self.latitude;
248 bus.longitude = self.longitude;
249 bus.freq_hz = self.freq_hz;
250 bus.ambient = self.ambient;
251 bus.reserve_zone = self.reserve_zone;
252 bus.owners = self.owners;
253 }
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
257struct IndexedBranchExtra {
258 index: u32,
259 extra: BranchExtra,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
263struct BranchExtra {
264 r0: Option<f64>,
265 x0: Option<f64>,
266 b0: Option<f64>,
267 zn: Option<(f64, f64)>,
268 tab: Option<u32>,
269 oil_temp_limit_c: Option<f64>,
270 winding_temp_limit_c: Option<f64>,
271 impedance_limit_ohm: Option<f64>,
272 saturation: Option<TransformerSaturation>,
273 core_type: Option<CoreType>,
274 core_loss_model: Option<CoreLossModel>,
275 length_km: Option<f64>,
276 line_type: Option<surge_network::network::LineType>,
277 conductor: Option<String>,
278 n_bundles: Option<u32>,
279 winding_rated_kv: Option<f64>,
280 winding_rated_mva: Option<f64>,
281 parent_transformer_id: Option<String>,
282 winding_number: Option<u8>,
283 winding_connection: Option<WindingConnection>,
284 zn_winding: Option<(f64, f64)>,
285 bypass_current_ka: Option<f64>,
286 rated_mvar_series: Option<f64>,
287 ambient: Option<AmbientConditions>,
288 owners: Vec<OwnershipEntry>,
289}
290
291impl BranchExtra {
292 fn from_branch(branch: &Branch) -> Option<Self> {
293 let zs = branch.zero_seq.as_ref();
294 let td = branch.transformer_data.as_ref();
295 let hd = branch.harmonic.as_ref();
296 let ld = branch.line_data.as_ref();
297 let sc = branch.series_comp.as_ref();
298 let extra = Self {
299 r0: zs.map(|z| z.r0),
300 x0: zs.map(|z| z.x0),
301 b0: zs.map(|z| z.b0),
302 zn: zs.and_then(|z| z.zn).map(|value| (value.re, value.im)),
303 tab: branch.tab,
304 oil_temp_limit_c: td.and_then(|t| t.oil_temp_limit_c),
305 winding_temp_limit_c: td.and_then(|t| t.winding_temp_limit_c),
306 impedance_limit_ohm: td.and_then(|t| t.impedance_limit_ohm),
307 saturation: hd.and_then(|h| h.saturation.clone()),
308 core_type: hd.and_then(|h| h.core_type),
309 core_loss_model: hd.and_then(|h| h.core_loss_model),
310 length_km: ld.and_then(|l| l.length_km),
311 line_type: ld.and_then(|l| l.line_type),
312 conductor: ld.and_then(|l| l.conductor.clone()),
313 n_bundles: ld.and_then(|l| l.n_bundles),
314 winding_rated_kv: td.and_then(|t| t.winding_rated_kv),
315 winding_rated_mva: td.and_then(|t| t.winding_rated_mva),
316 parent_transformer_id: td.and_then(|t| t.parent_transformer_id.clone()),
317 winding_number: td.and_then(|t| t.winding_number),
318 winding_connection: td.and_then(|t| t.winding_connection),
319 zn_winding: td
320 .and_then(|t| t.zn_winding)
321 .map(|value| (value.re, value.im)),
322 bypass_current_ka: sc.and_then(|s| s.bypass_current_ka),
323 rated_mvar_series: sc.and_then(|s| s.rated_mvar_series),
324 ambient: branch.ambient.clone(),
325 owners: branch.owners.clone(),
326 };
327 (!extra.is_empty()).then_some(extra)
328 }
329
330 fn is_empty(&self) -> bool {
331 self.r0.is_none()
332 && self.x0.is_none()
333 && self.b0.is_none()
334 && self.zn.is_none()
335 && self.tab.is_none()
336 && self.oil_temp_limit_c.is_none()
337 && self.winding_temp_limit_c.is_none()
338 && self.impedance_limit_ohm.is_none()
339 && self.saturation.is_none()
340 && self.core_type.is_none()
341 && self.core_loss_model.is_none()
342 && self.length_km.is_none()
343 && self.line_type.is_none()
344 && self.conductor.is_none()
345 && self.n_bundles.is_none()
346 && self.winding_rated_kv.is_none()
347 && self.winding_rated_mva.is_none()
348 && self.parent_transformer_id.is_none()
349 && self.winding_number.is_none()
350 && self.winding_connection.is_none()
351 && self.zn_winding.is_none()
352 && self.bypass_current_ka.is_none()
353 && self.rated_mvar_series.is_none()
354 && self.ambient.is_none()
355 && self.owners.is_empty()
356 }
357
358 fn apply(self, branch: &mut Branch) {
359 if self.r0.is_some() || self.x0.is_some() || self.b0.is_some() || self.zn.is_some() {
361 let zs = branch.zero_seq.get_or_insert_with(ZeroSeqData::default);
362 if let Some(r0) = self.r0 {
363 zs.r0 = r0;
364 }
365 if let Some(x0) = self.x0 {
366 zs.x0 = x0;
367 }
368 if let Some(b0) = self.b0 {
369 zs.b0 = b0;
370 }
371 zs.zn = self.zn.map(|(re, im)| num_complex::Complex64::new(re, im));
372 }
373 branch.tab = self.tab;
374 if self.oil_temp_limit_c.is_some()
376 || self.winding_temp_limit_c.is_some()
377 || self.impedance_limit_ohm.is_some()
378 || self.winding_rated_kv.is_some()
379 || self.winding_rated_mva.is_some()
380 || self.parent_transformer_id.is_some()
381 || self.winding_number.is_some()
382 || self.winding_connection.is_some()
383 || self.zn_winding.is_some()
384 {
385 let td = branch
386 .transformer_data
387 .get_or_insert_with(TransformerData::default);
388 td.oil_temp_limit_c = self.oil_temp_limit_c;
389 td.winding_temp_limit_c = self.winding_temp_limit_c;
390 td.impedance_limit_ohm = self.impedance_limit_ohm;
391 td.winding_rated_kv = self.winding_rated_kv;
392 td.winding_rated_mva = self.winding_rated_mva;
393 td.parent_transformer_id = self.parent_transformer_id;
394 td.winding_number = self.winding_number;
395 td.winding_connection = self.winding_connection;
396 td.zn_winding = self
397 .zn_winding
398 .map(|(re, im)| num_complex::Complex64::new(re, im));
399 }
400 if self.saturation.is_some() || self.core_type.is_some() || self.core_loss_model.is_some() {
402 let hd = branch.harmonic.get_or_insert_with(HarmonicData::default);
403 hd.saturation = self.saturation;
404 hd.core_type = self.core_type;
405 hd.core_loss_model = self.core_loss_model;
406 }
407 if self.length_km.is_some()
409 || self.line_type.is_some()
410 || self.conductor.is_some()
411 || self.n_bundles.is_some()
412 {
413 let ld = branch.line_data.get_or_insert_with(LineData::default);
414 ld.length_km = self.length_km;
415 ld.line_type = self.line_type;
416 ld.conductor = self.conductor;
417 ld.n_bundles = self.n_bundles;
418 }
419 if self.bypass_current_ka.is_some() || self.rated_mvar_series.is_some() {
421 let sc = branch
422 .series_comp
423 .get_or_insert_with(SeriesCompData::default);
424 sc.bypass_current_ka = self.bypass_current_ka;
425 sc.rated_mvar_series = self.rated_mvar_series;
426 }
427 branch.ambient = self.ambient;
428 branch.owners = self.owners;
429 }
430}
431
432#[derive(Debug, Clone, Serialize, Deserialize)]
433struct IndexedGeneratorExtra {
434 index: u32,
435 extra: GeneratorExtra,
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize)]
439struct GeneratorExtra {
440 technology: Option<GeneratorTechnology>,
441 source_technology_code: Option<String>,
442 xs: Option<f64>,
443 x2_pu: Option<f64>,
444 r2_pu: Option<f64>,
445 x0_pu: Option<f64>,
446 r0_pu: Option<f64>,
447 zn: Option<(f64, f64)>,
448 s_rated_mva: Option<f64>,
449 p_available_mw: Option<f64>,
450 p_ecomin: Option<f64>,
451 p_ecomax: Option<f64>,
452 p_emergency_min: Option<f64>,
453 p_emergency_max: Option<f64>,
454 p_reg_min: Option<f64>,
455 p_reg_max: Option<f64>,
456 min_up_time_hr: Option<f64>,
457 min_down_time_hr: Option<f64>,
458 max_up_time_hr: Option<f64>,
459 min_run_at_pmin_hr: Option<f64>,
460 max_starts_per_day: Option<u32>,
461 max_starts_per_week: Option<u32>,
462 max_energy_mwh_per_day: Option<f64>,
463 shutdown_ramp_mw_per_min: Option<f64>,
464 startup_ramp_mw_per_min: Option<f64>,
465 forbidden_zones: Vec<(f64, f64)>,
466 ramp_up_curve: Vec<(f64, f64)>,
467 ramp_down_curve: Vec<(f64, f64)>,
468 emergency_ramp_up_curve: Vec<(f64, f64)>,
469 emergency_ramp_down_curve: Vec<(f64, f64)>,
470 reg_ramp_up_curve: Vec<(f64, f64)>,
471 reg_ramp_down_curve: Vec<(f64, f64)>,
472 primary_fuel: Option<FuelSupply>,
473 backup_fuel: Option<FuelSupply>,
474 fuel_switch_time_min: Option<f64>,
475 fuel_type: Option<String>,
476 heat_rate_btu_mwh: Option<f64>,
477 energy_offer: Option<EnergyOffer>,
478 reserve_offers: Vec<ReserveOffer>,
479 qualifications: QualificationMap,
480 forced_outage_rate: Option<f64>,
481 pq_curve: Vec<(f64, f64, f64)>,
482 storage: Option<StorageParams>,
483 owners: Vec<OwnershipEntry>,
484}
485
486impl GeneratorExtra {
487 fn from_generator(generator: &Generator) -> Option<Self> {
488 let fd = generator.fault_data.as_ref();
489 let inv = generator.inverter.as_ref();
490 let cm = generator.commitment.as_ref();
491 let rp = generator.ramping.as_ref();
492 let fl = generator.fuel.as_ref();
493 let mk = generator.market.as_ref();
494 let rc = generator.reactive_capability.as_ref();
495 let extra = Self {
496 technology: generator.technology,
497 source_technology_code: generator.source_technology_code.clone(),
498 xs: fd.and_then(|f| f.xs),
499 x2_pu: fd.and_then(|f| f.x2_pu),
500 r2_pu: fd.and_then(|f| f.r2_pu),
501 x0_pu: fd.and_then(|f| f.x0_pu),
502 r0_pu: fd.and_then(|f| f.r0_pu),
503 zn: fd.and_then(|f| f.zn).map(|value| (value.re, value.im)),
504 s_rated_mva: inv.and_then(|i| i.s_rated_mva),
505 p_available_mw: inv.and_then(|i| i.p_available_mw),
506 p_ecomin: cm.and_then(|c| c.p_ecomin),
507 p_ecomax: cm.and_then(|c| c.p_ecomax),
508 p_emergency_min: cm.and_then(|c| c.p_emergency_min),
509 p_emergency_max: cm.and_then(|c| c.p_emergency_max),
510 p_reg_min: cm.and_then(|c| c.p_reg_min),
511 p_reg_max: cm.and_then(|c| c.p_reg_max),
512 min_up_time_hr: cm.and_then(|c| c.min_up_time_hr),
513 min_down_time_hr: cm.and_then(|c| c.min_down_time_hr),
514 max_up_time_hr: cm.and_then(|c| c.max_up_time_hr),
515 min_run_at_pmin_hr: cm.and_then(|c| c.min_run_at_pmin_hr),
516 max_starts_per_day: cm.and_then(|c| c.max_starts_per_day),
517 max_starts_per_week: cm.and_then(|c| c.max_starts_per_week),
518 max_energy_mwh_per_day: cm.and_then(|c| c.max_energy_mwh_per_day),
519 shutdown_ramp_mw_per_min: cm.and_then(|c| c.shutdown_ramp_mw_per_min),
520 startup_ramp_mw_per_min: cm.and_then(|c| c.startup_ramp_mw_per_min),
521 forbidden_zones: cm.map(|c| c.forbidden_zones.clone()).unwrap_or_default(),
522 ramp_up_curve: rp.map(|r| r.ramp_up_curve.clone()).unwrap_or_default(),
523 ramp_down_curve: rp.map(|r| r.ramp_down_curve.clone()).unwrap_or_default(),
524 emergency_ramp_up_curve: rp
525 .map(|r| r.emergency_ramp_up_curve.clone())
526 .unwrap_or_default(),
527 emergency_ramp_down_curve: rp
528 .map(|r| r.emergency_ramp_down_curve.clone())
529 .unwrap_or_default(),
530 reg_ramp_up_curve: rp.map(|r| r.reg_ramp_up_curve.clone()).unwrap_or_default(),
531 reg_ramp_down_curve: rp
532 .map(|r| r.reg_ramp_down_curve.clone())
533 .unwrap_or_default(),
534 primary_fuel: fl.and_then(|f| f.primary_fuel.clone()),
535 backup_fuel: fl.and_then(|f| f.backup_fuel.clone()),
536 fuel_switch_time_min: fl.and_then(|f| f.fuel_switch_time_min),
537 fuel_type: fl.and_then(|f| f.fuel_type.clone()),
538 heat_rate_btu_mwh: fl.and_then(|f| f.heat_rate_btu_mwh),
539 energy_offer: mk.and_then(|m| m.energy_offer.clone()),
540 reserve_offers: mk.map(|m| m.reserve_offers.clone()).unwrap_or_default(),
541 qualifications: mk.map(|m| m.qualifications.clone()).unwrap_or_default(),
542 forced_outage_rate: generator.forced_outage_rate,
543 pq_curve: rc.map(|r| r.pq_curve.clone()).unwrap_or_default(),
544 storage: generator.storage.clone(),
545 owners: generator.owners.clone(),
546 };
547 (!extra.is_empty()).then_some(extra)
548 }
549
550 fn is_empty(&self) -> bool {
551 self.xs.is_none()
552 && self.technology.is_none()
553 && self.source_technology_code.is_none()
554 && self.x2_pu.is_none()
555 && self.r2_pu.is_none()
556 && self.x0_pu.is_none()
557 && self.r0_pu.is_none()
558 && self.zn.is_none()
559 && self.s_rated_mva.is_none()
560 && self.p_available_mw.is_none()
561 && self.p_ecomin.is_none()
562 && self.p_ecomax.is_none()
563 && self.p_emergency_min.is_none()
564 && self.p_emergency_max.is_none()
565 && self.p_reg_min.is_none()
566 && self.p_reg_max.is_none()
567 && self.min_up_time_hr.is_none()
568 && self.min_down_time_hr.is_none()
569 && self.max_up_time_hr.is_none()
570 && self.min_run_at_pmin_hr.is_none()
571 && self.max_starts_per_day.is_none()
572 && self.max_starts_per_week.is_none()
573 && self.max_energy_mwh_per_day.is_none()
574 && self.shutdown_ramp_mw_per_min.is_none()
575 && self.startup_ramp_mw_per_min.is_none()
576 && self.forbidden_zones.is_empty()
577 && self.ramp_up_curve.is_empty()
578 && self.ramp_down_curve.is_empty()
579 && self.emergency_ramp_up_curve.is_empty()
580 && self.emergency_ramp_down_curve.is_empty()
581 && self.reg_ramp_up_curve.is_empty()
582 && self.reg_ramp_down_curve.is_empty()
583 && self.primary_fuel.is_none()
584 && self.backup_fuel.is_none()
585 && self.fuel_switch_time_min.is_none()
586 && self.fuel_type.is_none()
587 && self.heat_rate_btu_mwh.is_none()
588 && self.energy_offer.is_none()
589 && self.reserve_offers.is_empty()
590 && self.qualifications.is_empty()
591 && self.forced_outage_rate.is_none()
592 && self.pq_curve.is_empty()
593 && self.storage.is_none()
594 && self.owners.is_empty()
595 }
596
597 fn apply(self, generator: &mut Generator) {
598 generator.technology = self.technology;
599 generator.source_technology_code = self.source_technology_code;
600 if self.xs.is_some()
602 || self.x2_pu.is_some()
603 || self.r2_pu.is_some()
604 || self.x0_pu.is_some()
605 || self.r0_pu.is_some()
606 || self.zn.is_some()
607 {
608 let fd = generator.fault_data.get_or_insert_with(Default::default);
609 fd.xs = self.xs;
610 fd.x2_pu = self.x2_pu;
611 fd.r2_pu = self.r2_pu;
612 fd.x0_pu = self.x0_pu;
613 fd.r0_pu = self.r0_pu;
614 fd.zn = self.zn.map(|(re, im)| num_complex::Complex64::new(re, im));
615 }
616 if self.s_rated_mva.is_some() || self.p_available_mw.is_some() {
618 let inv = generator.inverter.get_or_insert_with(Default::default);
619 inv.s_rated_mva = self.s_rated_mva;
620 inv.p_available_mw = self.p_available_mw;
621 }
622 if self.p_ecomin.is_some()
624 || self.p_ecomax.is_some()
625 || self.p_emergency_min.is_some()
626 || self.p_emergency_max.is_some()
627 || self.p_reg_min.is_some()
628 || self.p_reg_max.is_some()
629 || self.min_up_time_hr.is_some()
630 || self.min_down_time_hr.is_some()
631 || self.max_up_time_hr.is_some()
632 || self.min_run_at_pmin_hr.is_some()
633 || self.max_starts_per_day.is_some()
634 || self.max_starts_per_week.is_some()
635 || self.max_energy_mwh_per_day.is_some()
636 || self.shutdown_ramp_mw_per_min.is_some()
637 || self.startup_ramp_mw_per_min.is_some()
638 || !self.forbidden_zones.is_empty()
639 {
640 let cm = generator.commitment.get_or_insert_with(Default::default);
641 cm.p_ecomin = self.p_ecomin;
642 cm.p_ecomax = self.p_ecomax;
643 cm.p_emergency_min = self.p_emergency_min;
644 cm.p_emergency_max = self.p_emergency_max;
645 cm.p_reg_min = self.p_reg_min;
646 cm.p_reg_max = self.p_reg_max;
647 cm.min_up_time_hr = self.min_up_time_hr;
648 cm.min_down_time_hr = self.min_down_time_hr;
649 cm.max_up_time_hr = self.max_up_time_hr;
650 cm.min_run_at_pmin_hr = self.min_run_at_pmin_hr;
651 cm.max_starts_per_day = self.max_starts_per_day;
652 cm.max_starts_per_week = self.max_starts_per_week;
653 cm.max_energy_mwh_per_day = self.max_energy_mwh_per_day;
654 cm.shutdown_ramp_mw_per_min = self.shutdown_ramp_mw_per_min;
655 cm.startup_ramp_mw_per_min = self.startup_ramp_mw_per_min;
656 cm.forbidden_zones = self.forbidden_zones;
657 }
658 if !self.ramp_up_curve.is_empty()
660 || !self.ramp_down_curve.is_empty()
661 || !self.emergency_ramp_up_curve.is_empty()
662 || !self.emergency_ramp_down_curve.is_empty()
663 || !self.reg_ramp_up_curve.is_empty()
664 || !self.reg_ramp_down_curve.is_empty()
665 {
666 let rp = generator.ramping.get_or_insert_with(Default::default);
667 rp.ramp_up_curve = self.ramp_up_curve;
668 rp.ramp_down_curve = self.ramp_down_curve;
669 rp.emergency_ramp_up_curve = self.emergency_ramp_up_curve;
670 rp.emergency_ramp_down_curve = self.emergency_ramp_down_curve;
671 rp.reg_ramp_up_curve = self.reg_ramp_up_curve;
672 rp.reg_ramp_down_curve = self.reg_ramp_down_curve;
673 }
674 if self.primary_fuel.is_some()
676 || self.backup_fuel.is_some()
677 || self.fuel_switch_time_min.is_some()
678 || self.fuel_type.is_some()
679 || self.heat_rate_btu_mwh.is_some()
680 {
681 let fl = generator.fuel.get_or_insert_with(Default::default);
682 fl.primary_fuel = self.primary_fuel;
683 fl.backup_fuel = self.backup_fuel;
684 fl.fuel_switch_time_min = self.fuel_switch_time_min;
685 fl.fuel_type = self.fuel_type;
686 fl.heat_rate_btu_mwh = self.heat_rate_btu_mwh;
687 }
688 if self.energy_offer.is_some()
690 || !self.reserve_offers.is_empty()
691 || !self.qualifications.is_empty()
692 {
693 let mk = generator.market.get_or_insert_with(Default::default);
694 mk.energy_offer = self.energy_offer;
695 mk.reserve_offers = self.reserve_offers;
696 mk.qualifications = self.qualifications;
697 }
698 if !self.pq_curve.is_empty() {
700 generator
701 .reactive_capability
702 .get_or_insert_with(Default::default)
703 .pq_curve = self.pq_curve;
704 }
705 generator.forced_outage_rate = self.forced_outage_rate;
706 generator.storage = self.storage;
707 generator.owners = self.owners;
708 }
709}
710
711#[derive(Debug, Clone, Serialize, Deserialize)]
712struct IndexedLoadExtra {
713 index: u32,
714 extra: LoadExtra,
715}
716
717#[derive(Debug, Clone, Serialize, Deserialize)]
718struct LoadExtra {
719 owners: Vec<OwnershipEntry>,
720}
721
722impl LoadExtra {
723 fn from_load(load: &Load) -> Option<Self> {
724 let extra = Self {
725 owners: load.owners.clone(),
726 };
727 (!extra.is_empty()).then_some(extra)
728 }
729
730 fn is_empty(&self) -> bool {
731 self.owners.is_empty()
732 }
733
734 fn apply(self, load: &mut Load) {
735 load.owners = self.owners;
736 }
737}
738
739#[derive(Debug, Clone, Serialize, Deserialize)]
740#[allow(clippy::type_complexity)]
741struct NetworkExtras {
742 dispatchable_loads: Vec<DispatchableLoad>,
743 switched_shunts: Vec<SwitchedShunt>,
744 switched_shunts_opf: Vec<SwitchedShuntOpf>,
745 oltc_specs: Vec<OltcSpec>,
746 par_specs: Vec<ParSpec>,
747 hvdc: HvdcModel,
748 area_schedules: Vec<AreaSchedule>,
749 facts_devices: Vec<FactsDevice>,
750 regions: Vec<Region>,
751 owners: Vec<Owner>,
752 scheduled_area_transfers: Vec<ScheduledAreaTransfer>,
753 impedance_corrections: Vec<ImpedanceCorrectionTable>,
754 multi_section_line_groups: Vec<MultiSectionLineGroup>,
755 per_length_phase_impedances: HashMap<String, Vec<PhaseImpedanceEntry>>,
756 mutual_couplings: Vec<MutualCoupling>,
757 grounding_impedances: Vec<GroundingEntry>,
758 geo_locations: HashMap<String, Vec<GeoPoint>>,
759 interfaces: Vec<Interface>,
760 flowgates: Vec<Flowgate>,
761 nomograms: Vec<OperatingNomogram>,
762 topology: Option<NodeBreakerTopology>,
763 induction_machines: Vec<InductionMachine>,
764 conditional_limits: BranchConditionalRatings,
765 pumped_hydro_units: Vec<PumpedHydroUnit>,
766 breaker_ratings: Vec<BreakerRating>,
767 fixed_shunts: Vec<FixedShunt>,
768 power_injections: Vec<PowerInjection>,
769 combined_cycle_plants: Vec<CombinedCyclePlant>,
770 outage_schedule: Vec<OutageEntry>,
771 reserve_zones: Vec<ReserveZone>,
772 ambient: Option<AmbientConditions>,
773 emission_policy: Option<EmissionPolicy>,
774 measurements: Vec<CimMeasurement>,
775 asset_catalog: AssetCatalog,
776 operational_limits: OperationalLimits,
777 market_rules: Option<MarketRules>,
778 boundary_data: BoundaryData,
779 protection_data: ProtectionData,
780 market_data: MarketData,
781 network_operations: NetworkOperationsData,
782}
783
784impl NetworkExtras {
785 fn from_network(network: &Network) -> Self {
786 Self {
787 dispatchable_loads: network.market_data.dispatchable_loads.clone(),
788 switched_shunts: network.controls.switched_shunts.clone(),
789 switched_shunts_opf: network.controls.switched_shunts_opf.clone(),
790 oltc_specs: network.controls.oltc_specs.clone(),
791 par_specs: network.controls.par_specs.clone(),
792 hvdc: network.hvdc.clone(),
793 area_schedules: network.area_schedules.clone(),
794 facts_devices: network.facts_devices.clone(),
795 regions: network.metadata.regions.clone(),
796 owners: network.metadata.owners.clone(),
797 scheduled_area_transfers: network.metadata.scheduled_area_transfers.clone(),
798 impedance_corrections: network.metadata.impedance_corrections.clone(),
799 multi_section_line_groups: network.metadata.multi_section_line_groups.clone(),
800 per_length_phase_impedances: network.cim.per_length_phase_impedances.clone(),
801 mutual_couplings: network.cim.mutual_couplings.clone(),
802 grounding_impedances: network.cim.grounding_impedances.clone(),
803 geo_locations: network.cim.geo_locations.clone(),
804 interfaces: network.interfaces.clone(),
805 flowgates: network.flowgates.clone(),
806 nomograms: network.nomograms.clone(),
807 topology: network.topology.clone(),
808 induction_machines: network.induction_machines.clone(),
809 conditional_limits: network.conditional_limits.clone(),
810 pumped_hydro_units: network.market_data.pumped_hydro_units.clone(),
811 breaker_ratings: network.breaker_ratings.clone(),
812 fixed_shunts: network.fixed_shunts.clone(),
813 power_injections: network.power_injections.clone(),
814 combined_cycle_plants: network.market_data.combined_cycle_plants.clone(),
815 outage_schedule: network.market_data.outage_schedule.clone(),
816 reserve_zones: network.market_data.reserve_zones.clone(),
817 ambient: network.market_data.ambient.clone(),
818 emission_policy: network.market_data.emission_policy.clone(),
819 measurements: network.cim.measurements.clone(),
820 asset_catalog: network.cim.asset_catalog.clone(),
821 operational_limits: network.cim.operational_limits.clone(),
822 market_rules: network.market_data.market_rules.clone(),
823 boundary_data: network.cim.boundary_data.clone(),
824 protection_data: network.cim.protection_data.clone(),
825 market_data: network.cim.market_data.clone(),
826 network_operations: network.cim.network_operations.clone(),
827 }
828 }
829
830 fn is_empty(&self) -> bool {
831 self.dispatchable_loads.is_empty()
832 && self.switched_shunts.is_empty()
833 && self.switched_shunts_opf.is_empty()
834 && self.oltc_specs.is_empty()
835 && self.par_specs.is_empty()
836 && self.hvdc.is_empty()
837 && self.area_schedules.is_empty()
838 && self.facts_devices.is_empty()
839 && self.regions.is_empty()
840 && self.owners.is_empty()
841 && self.scheduled_area_transfers.is_empty()
842 && self.impedance_corrections.is_empty()
843 && self.multi_section_line_groups.is_empty()
844 && self.per_length_phase_impedances.is_empty()
845 && self.mutual_couplings.is_empty()
846 && self.grounding_impedances.is_empty()
847 && self.geo_locations.is_empty()
848 && self.interfaces.is_empty()
849 && self.flowgates.is_empty()
850 && self.nomograms.is_empty()
851 && self.topology.is_none()
852 && self.induction_machines.is_empty()
853 && self.conditional_limits.is_empty()
854 && self.pumped_hydro_units.is_empty()
855 && self.breaker_ratings.is_empty()
856 && self.fixed_shunts.is_empty()
857 && self.power_injections.is_empty()
858 && self.combined_cycle_plants.is_empty()
859 && self.outage_schedule.is_empty()
860 && self.reserve_zones.is_empty()
861 && self.ambient.is_none()
862 && self.emission_policy.is_none()
863 && self.measurements.is_empty()
864 && self.asset_catalog.is_empty()
865 && self.operational_limits.is_empty()
866 && self.market_rules.is_none()
867 && self.boundary_data.is_empty()
868 && self.protection_data.is_empty()
869 && self.market_data.is_empty()
870 && self.network_operations.is_empty()
871 }
872
873 fn apply(self, network: &mut Network) {
874 network.market_data.dispatchable_loads = self.dispatchable_loads;
875 network.controls.switched_shunts = self.switched_shunts;
876 network.controls.switched_shunts_opf = self.switched_shunts_opf;
877 network.controls.oltc_specs = self.oltc_specs;
878 network.controls.par_specs = self.par_specs;
879 network.hvdc = self.hvdc;
880 network.area_schedules = self.area_schedules;
881 network.facts_devices = self.facts_devices;
882 network.metadata.regions = self.regions;
883 network.metadata.owners = self.owners;
884 network.metadata.scheduled_area_transfers = self.scheduled_area_transfers;
885 network.metadata.impedance_corrections = self.impedance_corrections;
886 network.metadata.multi_section_line_groups = self.multi_section_line_groups;
887 network.cim.per_length_phase_impedances = self.per_length_phase_impedances;
888 network.cim.mutual_couplings = self.mutual_couplings;
889 network.cim.grounding_impedances = self.grounding_impedances;
890 network.cim.geo_locations = self.geo_locations;
891 network.interfaces = self.interfaces;
892 network.flowgates = self.flowgates;
893 network.nomograms = self.nomograms;
894 network.topology = self.topology;
895 network.induction_machines = self.induction_machines;
896 network.conditional_limits = self.conditional_limits;
897 network.market_data.pumped_hydro_units = self.pumped_hydro_units;
898 network.breaker_ratings = self.breaker_ratings;
899 network.fixed_shunts = self.fixed_shunts;
900 network.power_injections = self.power_injections;
901 network.market_data.combined_cycle_plants = self.combined_cycle_plants;
902 network.market_data.outage_schedule = self.outage_schedule;
903 network.market_data.reserve_zones = self.reserve_zones;
904 network.market_data.ambient = self.ambient;
905 network.market_data.emission_policy = self.emission_policy;
906 network.cim.measurements = self.measurements;
907 network.cim.asset_catalog = self.asset_catalog;
908 network.cim.operational_limits = self.operational_limits;
909 network.market_data.market_rules = self.market_rules;
910 network.cim.boundary_data = self.boundary_data;
911 network.cim.protection_data = self.protection_data;
912 network.cim.market_data = self.market_data;
913 network.cim.network_operations = self.network_operations;
914 }
915}
916
917pub fn load(path: impl AsRef<Path>) -> Result<Network, Error> {
919 let file = std::fs::File::open(path)?;
920 let mut reader = BufReader::new(file);
921 let mut bytes = Vec::new();
922 reader.read_to_end(&mut bytes)?;
923 loads(&bytes)
924}
925
926pub fn loads(bytes: &[u8]) -> Result<Network, Error> {
928 decode_document(bytes)
929}
930
931pub fn save(network: &Network, path: impl AsRef<Path>) -> Result<(), Error> {
933 let bytes = dumps(network)?;
934 let mut file = std::fs::File::create(path)?;
935 file.write_all(&bytes)?;
936 Ok(())
937}
938
939pub fn dumps(network: &Network) -> Result<Vec<u8>, Error> {
941 encode_document(network)
942}
943
944fn encode_document(network: &Network) -> Result<Vec<u8>, Error> {
945 let mut strings = StringTableBuilder::new();
946
947 let network_header = encode_network_header(network, &mut strings);
948 let bus_section = encode_buses(&network.buses, &mut strings);
949 let bus_extras = encode_bus_extras(&network.buses)?;
950 let branch_section = encode_branches(&network.branches, &mut strings);
951 let branch_extras = encode_branch_extras(&network.branches)?;
952 let generator_section = encode_generators(&network.generators, &mut strings);
953 let generator_extras = encode_generator_extras(&network.generators)?;
954 let load_section = encode_loads(&network.loads, &mut strings);
955 let load_extras = encode_load_extras(&network.loads)?;
956 let string_section = encode_strings(strings.freeze());
957 let network_extras = encode_network_extras(network)?;
958
959 let mut sections = vec![
960 Section {
961 id: SECTION_NETWORK_HEADER,
962 count: 1,
963 payload: network_header,
964 },
965 string_section,
966 Section {
967 id: SECTION_BUSES,
968 count: network.buses.len() as u32,
969 payload: bus_section,
970 },
971 Section {
972 id: SECTION_BRANCHES,
973 count: network.branches.len() as u32,
974 payload: branch_section,
975 },
976 Section {
977 id: SECTION_GENERATORS,
978 count: network.generators.len() as u32,
979 payload: generator_section,
980 },
981 Section {
982 id: SECTION_LOADS,
983 count: network.loads.len() as u32,
984 payload: load_section,
985 },
986 ];
987
988 if let Some(payload) = bus_extras {
989 sections.push(Section {
990 id: SECTION_BUS_EXTRAS,
991 count: payload.1,
992 payload: payload.0,
993 });
994 }
995 if let Some(payload) = branch_extras {
996 sections.push(Section {
997 id: SECTION_BRANCH_EXTRAS,
998 count: payload.1,
999 payload: payload.0,
1000 });
1001 }
1002 if let Some(payload) = generator_extras {
1003 sections.push(Section {
1004 id: SECTION_GENERATOR_EXTRAS,
1005 count: payload.1,
1006 payload: payload.0,
1007 });
1008 }
1009 if let Some(payload) = load_extras {
1010 sections.push(Section {
1011 id: SECTION_LOAD_EXTRAS,
1012 count: payload.1,
1013 payload: payload.0,
1014 });
1015 }
1016 if let Some(payload) = network_extras {
1017 sections.push(Section {
1018 id: SECTION_NETWORK_EXTRAS,
1019 count: 1,
1020 payload,
1021 });
1022 }
1023
1024 sections.sort_by_key(|section| section.id);
1025
1026 let mut body = Vec::new();
1027 for section in §ions {
1028 write_u16(&mut body, section.id);
1029 write_u16(&mut body, 0);
1030 write_u32(&mut body, section.count);
1031 write_u64(&mut body, section.payload.len() as u64);
1032 body.extend_from_slice(§ion.payload);
1033 }
1034
1035 let checksum = crc32fast::hash(&body);
1036 let mut output = Vec::with_capacity(HEADER_LEN + body.len());
1037 output.extend_from_slice(MAGIC);
1038 output.extend_from_slice(&BINARY_REVISION.to_le_bytes());
1039 output.extend_from_slice(&0u16.to_le_bytes());
1040 output.extend_from_slice(&(sections.len() as u32).to_le_bytes());
1041 output.extend_from_slice(&(body.len() as u64).to_le_bytes());
1042 output.extend_from_slice(&checksum.to_le_bytes());
1043 output.extend_from_slice(&body);
1044 Ok(output)
1045}
1046
1047fn decode_document(bytes: &[u8]) -> Result<Network, Error> {
1048 if bytes.len() < HEADER_LEN {
1049 return Err(Error::InvalidDocument(
1050 "binary document is shorter than the minimum header".to_string(),
1051 ));
1052 }
1053
1054 if &bytes[..MAGIC.len()] != MAGIC {
1055 return Err(Error::InvalidDocument(
1056 "invalid binary magic header".to_string(),
1057 ));
1058 }
1059
1060 let mut reader = Reader::new(bytes);
1061 let _magic = reader.take(MAGIC.len())?;
1062 let revision = reader.u16()?;
1063 if revision != BINARY_REVISION {
1064 return Err(Error::InvalidDocument(format!(
1065 "unsupported binary revision {revision}"
1066 )));
1067 }
1068 let _flags = reader.u16()?;
1069 let section_count = reader.u32()? as usize;
1070 let body_len = reader.u64()? as usize;
1071 let checksum = reader.u32()?;
1072 let body = reader.take(body_len)?;
1073 if !reader.is_empty() {
1074 return Err(Error::InvalidDocument(
1075 "binary document has trailing bytes after body".to_string(),
1076 ));
1077 }
1078 if crc32fast::hash(body) != checksum {
1079 return Err(Error::InvalidDocument(
1080 "binary payload checksum mismatch".to_string(),
1081 ));
1082 }
1083
1084 let mut body_reader = Reader::new(body);
1085 let mut sections = HashMap::new();
1086 for _ in 0..section_count {
1087 if body_reader.remaining() < SECTION_HEADER_LEN {
1088 return Err(Error::InvalidDocument(
1089 "truncated section header".to_string(),
1090 ));
1091 }
1092 let id = body_reader.u16()?;
1093 let _section_flags = body_reader.u16()?;
1094 let count = body_reader.u32()?;
1095 let len = body_reader.u64()? as usize;
1096 let payload = body_reader.take(len)?;
1097 if sections
1098 .insert(id, SectionView { count, payload })
1099 .is_some()
1100 {
1101 return Err(Error::InvalidDocument(format!("duplicate section id {id}")));
1102 }
1103 }
1104
1105 if !body_reader.is_empty() {
1106 return Err(Error::InvalidDocument(
1107 "binary body contains trailing bytes after declared sections".to_string(),
1108 ));
1109 }
1110
1111 let strings = decode_strings(require_section(§ions, SECTION_STRINGS)?)?;
1112 let mut network = decode_network_header(
1113 require_section(§ions, SECTION_NETWORK_HEADER)?,
1114 &strings,
1115 )?;
1116 network.buses = decode_buses(require_section(§ions, SECTION_BUSES)?, &strings)?;
1117 network.branches = decode_branches(require_section(§ions, SECTION_BRANCHES)?, &strings)?;
1118 network.generators =
1119 decode_generators(require_section(§ions, SECTION_GENERATORS)?, &strings)?;
1120 network.loads = decode_loads(require_section(§ions, SECTION_LOADS)?, &strings)?;
1121
1122 if let Some(section) = sections.get(&SECTION_BUS_EXTRAS) {
1123 apply_bus_extras(section, &mut network)?;
1124 }
1125 if let Some(section) = sections.get(&SECTION_BRANCH_EXTRAS) {
1126 apply_branch_extras(section, &mut network)?;
1127 }
1128 if let Some(section) = sections.get(&SECTION_GENERATOR_EXTRAS) {
1129 apply_generator_extras(section, &mut network)?;
1130 }
1131 if let Some(section) = sections.get(&SECTION_LOAD_EXTRAS) {
1132 apply_load_extras(section, &mut network)?;
1133 }
1134 if let Some(section) = sections.get(&SECTION_NETWORK_EXTRAS) {
1135 decode_cbor::<NetworkExtras>(section.payload)?.apply(&mut network);
1136 }
1137
1138 Ok(network)
1139}
1140
1141fn require_section<'a>(
1142 sections: &'a HashMap<u16, SectionView<'a>>,
1143 id: u16,
1144) -> Result<SectionView<'a>, Error> {
1145 sections
1146 .get(&id)
1147 .copied()
1148 .ok_or_else(|| Error::InvalidDocument(format!("missing required section id {id}")))
1149}
1150
1151fn encode_strings(table: StringTable) -> Section {
1152 let mut payload = Vec::new();
1153 for value in &table.values {
1154 write_bytes(&mut payload, value.as_bytes());
1155 }
1156 Section {
1157 id: SECTION_STRINGS,
1158 count: table.values.len() as u32,
1159 payload,
1160 }
1161}
1162
1163fn decode_strings(section: SectionView<'_>) -> Result<StringTable, Error> {
1164 let mut reader = Reader::new(section.payload);
1165 let mut values = Vec::with_capacity(section.count as usize);
1166 for _ in 0..section.count {
1167 let bytes = read_bytes(&mut reader)?;
1168 let value = String::from_utf8(bytes)
1169 .map_err(|error| Error::InvalidDocument(format!("invalid UTF-8 string: {error}")))?;
1170 values.push(value);
1171 }
1172 if !reader.is_empty() {
1173 return Err(Error::InvalidDocument(
1174 "string table payload has trailing bytes".to_string(),
1175 ));
1176 }
1177 Ok(StringTable { values })
1178}
1179
1180fn encode_network_header(network: &Network, strings: &mut StringTableBuilder) -> Vec<u8> {
1181 let mut payload = Vec::new();
1182 write_u32(&mut payload, strings.intern(&network.name));
1183 write_f64(&mut payload, network.base_mva);
1184 write_f64(&mut payload, network.freq_hz);
1185 payload
1186}
1187
1188fn decode_network_header(
1189 section: SectionView<'_>,
1190 strings: &StringTable,
1191) -> Result<Network, Error> {
1192 let mut reader = Reader::new(section.payload);
1193 let name_id = reader.u32()?;
1194 let base_mva = reader.f64()?;
1195 let freq_hz = reader.f64()?;
1196 if !reader.is_empty() {
1197 return Err(Error::InvalidDocument(
1198 "network header payload has trailing bytes".to_string(),
1199 ));
1200 }
1201 Ok(Network {
1202 name: strings.resolve(name_id)?.to_string(),
1203 base_mva,
1204 freq_hz,
1205 ..Network::default()
1206 })
1207}
1208
1209fn encode_buses(buses: &[Bus], strings: &mut StringTableBuilder) -> Vec<u8> {
1210 let mut payload = Vec::new();
1211 for bus in buses {
1212 write_u32(&mut payload, bus.number);
1213 write_u32(&mut payload, strings.intern(&bus.name));
1214 write_u8(&mut payload, encode_bus_type(bus.bus_type));
1215 write_u32(&mut payload, bus.area);
1216 write_u32(&mut payload, bus.zone);
1217 write_u32(&mut payload, bus.island_id);
1218 write_f64(&mut payload, 0.0); write_f64(&mut payload, 0.0); write_f64(&mut payload, bus.shunt_conductance_mw);
1221 write_f64(&mut payload, bus.shunt_susceptance_mvar);
1222 write_f64(&mut payload, bus.voltage_magnitude_pu);
1223 write_f64(&mut payload, bus.voltage_angle_rad);
1224 write_f64(&mut payload, bus.base_kv);
1225 write_f64(&mut payload, bus.voltage_max_pu);
1226 write_f64(&mut payload, bus.voltage_min_pu);
1227 }
1228 payload
1229}
1230
1231fn decode_buses(section: SectionView<'_>, strings: &StringTable) -> Result<Vec<Bus>, Error> {
1232 let mut reader = Reader::new(section.payload);
1233 let mut buses = Vec::with_capacity(section.count as usize);
1234 for _ in 0..section.count {
1235 let bus = Bus {
1236 number: reader.u32()?,
1237 name: strings.resolve(reader.u32()?)?.to_string(),
1238 bus_type: decode_bus_type(reader.u8()?)?,
1239 area: reader.u32()?,
1240 zone: reader.u32()?,
1241 island_id: reader.u32()?,
1242 shunt_conductance_mw: {
1243 let _legacy_pd = reader.f64()?; let _legacy_qd = reader.f64()?; reader.f64()?
1246 },
1247 shunt_susceptance_mvar: reader.f64()?,
1248 voltage_magnitude_pu: reader.f64()?,
1249 voltage_angle_rad: reader.f64()?,
1250 base_kv: reader.f64()?,
1251 voltage_max_pu: reader.f64()?,
1252 voltage_min_pu: reader.f64()?,
1253 ..Bus::default()
1254 };
1255 buses.push(bus);
1256 }
1257 if !reader.is_empty() {
1258 return Err(Error::InvalidDocument(
1259 "bus section payload has trailing bytes".to_string(),
1260 ));
1261 }
1262 Ok(buses)
1263}
1264
1265fn encode_bus_extras(buses: &[Bus]) -> Result<Option<(Vec<u8>, u32)>, Error> {
1266 let extras: Vec<_> = buses
1267 .iter()
1268 .enumerate()
1269 .filter_map(|(index, bus)| {
1270 BusExtra::from_bus(bus).map(|extra| IndexedBusExtra {
1271 index: index as u32,
1272 extra,
1273 })
1274 })
1275 .collect();
1276 if extras.is_empty() {
1277 return Ok(None);
1278 }
1279 Ok(Some((encode_cbor(&extras)?, extras.len() as u32)))
1280}
1281
1282fn apply_bus_extras(section: &SectionView<'_>, network: &mut Network) -> Result<(), Error> {
1283 let extras: Vec<IndexedBusExtra> = decode_cbor(section.payload)?;
1284 for extra in extras {
1285 let bus = network.buses.get_mut(extra.index as usize).ok_or_else(|| {
1286 Error::InvalidDocument(format!("bus extra index {} out of range", extra.index))
1287 })?;
1288 extra.extra.apply(bus);
1289 }
1290 Ok(())
1291}
1292
1293fn encode_branches(branches: &[Branch], strings: &mut StringTableBuilder) -> Vec<u8> {
1294 let mut payload = Vec::new();
1295 for branch in branches {
1296 let zs = branch.zero_seq.as_ref();
1297 let oc = branch.opf_control.as_ref();
1298 let ld = branch.line_data.as_ref();
1299 let hd = branch.harmonic.as_ref();
1300 let td = branch.transformer_data.as_ref();
1301 let sc = branch.series_comp.as_ref();
1302
1303 let mut flags = 0u8;
1304 if branch.in_service {
1305 flags |= 1 << 0;
1306 }
1307 if zs.is_some_and(|z| z.delta_connected) {
1308 flags |= 1 << 1;
1309 }
1310 if sc.is_some_and(|s| s.bypassed) {
1311 flags |= 1 << 2;
1312 }
1313 if branch.angle_diff_min_rad.is_some() {
1314 flags |= 1 << 3;
1315 }
1316 if branch.angle_diff_max_rad.is_some() {
1317 flags |= 1 << 4;
1318 }
1319
1320 write_u32(&mut payload, branch.from_bus);
1321 write_u32(&mut payload, branch.to_bus);
1322 write_u32(&mut payload, strings.intern(&branch.circuit));
1323 write_u8(&mut payload, flags);
1324 write_u8(
1325 &mut payload,
1326 encode_transformer_connection(td.map(|t| t.transformer_connection).unwrap_or_default()),
1327 );
1328 write_u8(
1329 &mut payload,
1330 encode_tap_mode(oc.map(|c| c.tap_mode).unwrap_or_default()),
1331 );
1332 write_u8(
1333 &mut payload,
1334 encode_phase_mode(oc.map(|c| c.phase_mode).unwrap_or_default()),
1335 );
1336 write_u8(&mut payload, encode_branch_type(branch.branch_type));
1337 write_f64(&mut payload, branch.r);
1338 write_f64(&mut payload, branch.x);
1339 write_f64(&mut payload, branch.b);
1340 write_f64(&mut payload, branch.rating_a_mva);
1341 write_f64(&mut payload, branch.rating_b_mva);
1342 write_f64(&mut payload, branch.rating_c_mva);
1343 write_f64(&mut payload, branch.tap);
1344 write_f64(&mut payload, branch.phase_shift_rad);
1345 write_f64(&mut payload, hd.map(|h| h.skin_effect_alpha).unwrap_or(0.0));
1346 write_f64(&mut payload, branch.g_pi);
1347 write_f64(&mut payload, branch.g_mag);
1348 write_f64(&mut payload, branch.b_mag);
1349 write_f64(&mut payload, zs.map(|z| z.gi0).unwrap_or(0.0));
1350 write_f64(&mut payload, zs.map(|z| z.bi0).unwrap_or(0.0));
1351 write_f64(&mut payload, zs.map(|z| z.gj0).unwrap_or(0.0));
1352 write_f64(&mut payload, zs.map(|z| z.bj0).unwrap_or(0.0));
1353 write_f64(
1354 &mut payload,
1355 oc.map(|c| c.tap_min)
1356 .unwrap_or(BranchOpfControl::default().tap_min),
1357 );
1358 write_f64(
1359 &mut payload,
1360 oc.map(|c| c.tap_max)
1361 .unwrap_or(BranchOpfControl::default().tap_max),
1362 );
1363 write_f64(&mut payload, oc.map(|c| c.tap_step).unwrap_or(0.0));
1364 write_f64(
1365 &mut payload,
1366 oc.map(|c| c.phase_min_rad)
1367 .unwrap_or(BranchOpfControl::default().phase_min_rad),
1368 );
1369 write_f64(
1370 &mut payload,
1371 oc.map(|c| c.phase_max_rad)
1372 .unwrap_or(BranchOpfControl::default().phase_max_rad),
1373 );
1374 write_f64(&mut payload, oc.map(|c| c.phase_step_rad).unwrap_or(0.0));
1375 write_f64(&mut payload, ld.map(|l| l.r_temp_coeff).unwrap_or(0.0));
1376 write_f64(&mut payload, ld.map(|l| l.r_ref_temp_c).unwrap_or(20.0));
1377 if let Some(value) = branch.angle_diff_min_rad {
1378 write_f64(&mut payload, value);
1379 }
1380 if let Some(value) = branch.angle_diff_max_rad {
1381 write_f64(&mut payload, value);
1382 }
1383 }
1384 payload
1385}
1386
1387fn decode_branches(section: SectionView<'_>, strings: &StringTable) -> Result<Vec<Branch>, Error> {
1388 let mut reader = Reader::new(section.payload);
1389 let mut branches = Vec::with_capacity(section.count as usize);
1390 for _ in 0..section.count {
1391 let from_bus = reader.u32()?;
1392 let to_bus = reader.u32()?;
1393 let circuit = strings.resolve(reader.u32()?)?.to_string();
1394 let flags = reader.u8()?;
1395 let in_service = (flags & (1 << 0)) != 0;
1396 let delta_connected = (flags & (1 << 1)) != 0;
1397 let bypassed = (flags & (1 << 2)) != 0;
1398 let has_angle_min = (flags & (1 << 3)) != 0;
1399 let has_angle_max = (flags & (1 << 4)) != 0;
1400
1401 let transformer_connection = decode_transformer_connection(reader.u8()?)?;
1402 let tap_mode = decode_tap_mode(reader.u8()?)?;
1403 let phase_mode = decode_phase_mode(reader.u8()?)?;
1404 let branch_type = decode_branch_type(reader.u8()?)?;
1405 let r = reader.f64()?;
1406 let x = reader.f64()?;
1407 let b_val = reader.f64()?;
1408 let rating_a_mva = reader.f64()?;
1409 let rating_b_mva = reader.f64()?;
1410 let rating_c_mva = reader.f64()?;
1411 let tap = reader.f64()?;
1412 let phase_shift_rad = reader.f64()?;
1413 let skin_effect_alpha = reader.f64()?;
1414 let g_pi = reader.f64()?;
1415 let g_mag = reader.f64()?;
1416 let b_mag = reader.f64()?;
1417 let gi0 = reader.f64()?;
1418 let bi0 = reader.f64()?;
1419 let gj0 = reader.f64()?;
1420 let bj0 = reader.f64()?;
1421 let tap_min = reader.f64()?;
1422 let tap_max = reader.f64()?;
1423 let tap_step = reader.f64()?;
1424 let phase_min_rad = reader.f64()?;
1425 let phase_max_rad = reader.f64()?;
1426 let phase_step_rad = reader.f64()?;
1427 let r_temp_coeff = reader.f64()?;
1428 let r_ref_temp_c = reader.f64()?;
1429
1430 let has_zero_seq = delta_connected
1432 || gi0.abs() > 0.0
1433 || bi0.abs() > 0.0
1434 || gj0.abs() > 0.0
1435 || bj0.abs() > 0.0;
1436 let zero_seq = if has_zero_seq {
1437 Some(ZeroSeqData {
1438 r0: 0.0,
1439 x0: 0.0,
1440 b0: 0.0,
1441 zn: None,
1442 gi0,
1443 bi0,
1444 gj0,
1445 bj0,
1446 delta_connected,
1447 })
1448 } else {
1449 None
1450 };
1451
1452 let opf_default = BranchOpfControl::default();
1453 let has_opf = tap_mode != TapMode::Fixed
1454 || phase_mode != PhaseMode::Fixed
1455 || (tap_min - opf_default.tap_min).abs() > 1e-12
1456 || (tap_max - opf_default.tap_max).abs() > 1e-12
1457 || tap_step.abs() > 1e-12
1458 || (phase_min_rad - opf_default.phase_min_rad).abs() > 1e-12
1459 || (phase_max_rad - opf_default.phase_max_rad).abs() > 1e-12
1460 || phase_step_rad.abs() > 1e-12;
1461 let opf_control = if has_opf {
1462 Some(BranchOpfControl {
1463 tap_mode,
1464 tap_min,
1465 tap_max,
1466 tap_step,
1467 phase_mode,
1468 phase_min_rad,
1469 phase_max_rad,
1470 phase_step_rad,
1471 })
1472 } else {
1473 None
1474 };
1475
1476 let has_transformer_data =
1477 !matches!(transformer_connection, TransformerConnection::WyeGWyeG);
1478 let transformer_data = if has_transformer_data {
1479 Some(TransformerData {
1480 transformer_connection,
1481 ..TransformerData::default()
1482 })
1483 } else {
1484 None
1485 };
1486
1487 let has_line_data = r_temp_coeff.abs() > 1e-12 || (r_ref_temp_c - 20.0).abs() > 1e-12;
1488 let line_data = if has_line_data {
1489 Some(LineData {
1490 r_temp_coeff,
1491 r_ref_temp_c,
1492 ..LineData::default()
1493 })
1494 } else {
1495 None
1496 };
1497
1498 let has_harmonic = skin_effect_alpha.abs() > 1e-12;
1499 let harmonic = if has_harmonic {
1500 Some(HarmonicData {
1501 skin_effect_alpha,
1502 ..HarmonicData::default()
1503 })
1504 } else {
1505 None
1506 };
1507
1508 let bypassed_val = bypassed;
1509 let series_comp = if bypassed_val {
1510 Some(SeriesCompData {
1511 bypassed: true,
1512 ..SeriesCompData::default()
1513 })
1514 } else {
1515 None
1516 };
1517
1518 let mut branch = Branch {
1519 from_bus,
1520 to_bus,
1521 circuit,
1522 in_service,
1523 branch_type,
1524 r,
1525 x,
1526 b: b_val,
1527 rating_a_mva,
1528 rating_b_mva,
1529 rating_c_mva,
1530 tap,
1531 phase_shift_rad,
1532 g_pi,
1533 g_mag,
1534 b_mag,
1535 opf_control,
1536 transformer_data,
1537 line_data,
1538 zero_seq,
1539 harmonic,
1540 series_comp,
1541 ..Branch::default()
1542 };
1543 if has_angle_min {
1544 branch.angle_diff_min_rad = Some(reader.f64()?);
1545 }
1546 if has_angle_max {
1547 branch.angle_diff_max_rad = Some(reader.f64()?);
1548 }
1549 branches.push(branch);
1550 }
1551 if !reader.is_empty() {
1552 return Err(Error::InvalidDocument(
1553 "branch section payload has trailing bytes".to_string(),
1554 ));
1555 }
1556 Ok(branches)
1557}
1558
1559fn encode_branch_extras(branches: &[Branch]) -> Result<Option<(Vec<u8>, u32)>, Error> {
1560 let extras: Vec<_> = branches
1561 .iter()
1562 .enumerate()
1563 .filter_map(|(index, branch)| {
1564 BranchExtra::from_branch(branch).map(|extra| IndexedBranchExtra {
1565 index: index as u32,
1566 extra,
1567 })
1568 })
1569 .collect();
1570 if extras.is_empty() {
1571 return Ok(None);
1572 }
1573 Ok(Some((encode_cbor(&extras)?, extras.len() as u32)))
1574}
1575
1576fn apply_branch_extras(section: &SectionView<'_>, network: &mut Network) -> Result<(), Error> {
1577 let extras: Vec<IndexedBranchExtra> = decode_cbor(section.payload)?;
1578 for extra in extras {
1579 let branch = network
1580 .branches
1581 .get_mut(extra.index as usize)
1582 .ok_or_else(|| {
1583 Error::InvalidDocument(format!("branch extra index {} out of range", extra.index))
1584 })?;
1585 extra.extra.apply(branch);
1586 }
1587 Ok(())
1588}
1589
1590fn encode_generators(generators: &[Generator], strings: &mut StringTableBuilder) -> Vec<u8> {
1591 let mut payload = Vec::new();
1592 for generator in generators {
1593 let mut flags = 0u16;
1594 if generator.voltage_regulated {
1595 flags |= 1 << 0;
1596 }
1597 if generator.in_service {
1598 flags |= 1 << 1;
1599 }
1600 if generator.inverter.as_ref().is_some_and(|i| i.curtailable) {
1601 flags |= 1 << 2;
1602 }
1603 if generator.inverter.as_ref().is_some_and(|i| i.grid_forming) {
1604 flags |= 1 << 3;
1605 }
1606 if generator.fuel.as_ref().is_some_and(|f| f.on_backup_fuel) {
1607 flags |= 1 << 4;
1608 }
1609 if generator.quick_start {
1610 flags |= 1 << 5;
1611 }
1612 if generator.pfr_eligible {
1613 flags |= 1 << 6;
1614 }
1615 if generator.reg_bus.is_some() {
1616 flags |= 1 << 7;
1617 }
1618 if generator.agc_participation_factor.is_some() {
1619 flags |= 1 << 8;
1620 }
1621 let rc = generator.reactive_capability.as_ref();
1622 if rc.and_then(|r| r.pc1).is_some() {
1623 flags |= 1 << 9;
1624 }
1625 if rc.and_then(|r| r.pc2).is_some() {
1626 flags |= 1 << 10;
1627 }
1628 if rc.and_then(|r| r.qc1min).is_some() {
1629 flags |= 1 << 11;
1630 }
1631 if rc.and_then(|r| r.qc1max).is_some() {
1632 flags |= 1 << 12;
1633 }
1634 if rc.and_then(|r| r.qc2min).is_some() {
1635 flags |= 1 << 13;
1636 }
1637 if rc.and_then(|r| r.qc2max).is_some() {
1638 flags |= 1 << 14;
1639 }
1640 if generator.h_inertia_s.is_some() {
1641 flags |= 1 << 15;
1642 }
1643
1644 write_u32(&mut payload, strings.intern(&generator.id));
1645 write_u32(&mut payload, generator.bus);
1646 write_bool(&mut payload, generator.machine_id.is_some());
1647 if let Some(machine_id) = &generator.machine_id {
1648 write_u32(&mut payload, strings.intern(machine_id));
1649 }
1650 write_u16(&mut payload, flags);
1651 write_u8(&mut payload, encode_gen_type(generator.gen_type));
1652 write_u8(
1653 &mut payload,
1654 encode_commitment_status(
1655 generator
1656 .commitment
1657 .as_ref()
1658 .map(|c| c.status)
1659 .unwrap_or(surge_network::network::CommitmentStatus::Market),
1660 ),
1661 );
1662 write_f64(&mut payload, generator.p);
1663 write_f64(&mut payload, generator.q);
1664 write_f64(&mut payload, generator.qmax);
1665 write_f64(&mut payload, generator.qmin);
1666 write_f64(&mut payload, generator.voltage_setpoint_pu);
1667 write_f64(&mut payload, generator.machine_base_mva);
1668 write_f64(&mut payload, generator.pmax);
1669 write_f64(&mut payload, generator.pmin);
1670 write_f64(
1671 &mut payload,
1672 generator
1673 .inverter
1674 .as_ref()
1675 .map_or(0.0, |i| i.inverter_loss_a_mw),
1676 );
1677 write_f64(
1678 &mut payload,
1679 generator
1680 .inverter
1681 .as_ref()
1682 .map_or(0.0, |i| i.inverter_loss_b_pu),
1683 );
1684 write_f64(
1685 &mut payload,
1686 generator
1687 .commitment
1688 .as_ref()
1689 .map_or(0.0, |c| c.hours_online),
1690 );
1691 write_f64(
1692 &mut payload,
1693 generator
1694 .commitment
1695 .as_ref()
1696 .map_or(0.0, |c| c.hours_offline),
1697 );
1698 write_cost_curve(&mut payload, &generator.cost);
1699 {
1700 let default_rates = EmissionRates::default();
1701 let rates = generator
1702 .fuel
1703 .as_ref()
1704 .map_or(&default_rates, |f| &f.emission_rates);
1705 write_emission_rates(&mut payload, rates);
1706 }
1707 if let Some(value) = generator.reg_bus {
1708 write_u32(&mut payload, value);
1709 }
1710 if let Some(value) = generator.agc_participation_factor {
1711 write_f64(&mut payload, value);
1712 }
1713 if let Some(value) = rc.and_then(|r| r.pc1) {
1714 write_f64(&mut payload, value);
1715 }
1716 if let Some(value) = rc.and_then(|r| r.pc2) {
1717 write_f64(&mut payload, value);
1718 }
1719 if let Some(value) = rc.and_then(|r| r.qc1min) {
1720 write_f64(&mut payload, value);
1721 }
1722 if let Some(value) = rc.and_then(|r| r.qc1max) {
1723 write_f64(&mut payload, value);
1724 }
1725 if let Some(value) = rc.and_then(|r| r.qc2min) {
1726 write_f64(&mut payload, value);
1727 }
1728 if let Some(value) = rc.and_then(|r| r.qc2max) {
1729 write_f64(&mut payload, value);
1730 }
1731 if let Some(value) = generator.h_inertia_s {
1732 write_f64(&mut payload, value);
1733 }
1734 }
1735 payload
1736}
1737
1738fn decode_generators(
1739 section: SectionView<'_>,
1740 strings: &StringTable,
1741) -> Result<Vec<Generator>, Error> {
1742 let mut reader = Reader::new(section.payload);
1743 let mut generators = Vec::with_capacity(section.count as usize);
1744 for _ in 0..section.count {
1745 let id = strings.resolve(reader.u32()?)?.to_string();
1746 let bus = reader.u32()?;
1747 let has_machine_id = read_bool(&mut reader)?;
1748 let machine_id = if has_machine_id {
1749 Some(strings.resolve(reader.u32()?)?.to_string())
1750 } else {
1751 None
1752 };
1753 let flags = reader.u16()?;
1754 let gen_type = decode_gen_type(reader.u8()?)?;
1755 let commitment_status = decode_commitment_status(reader.u8()?)?;
1756 let pg = reader.f64()?;
1757 let qg = reader.f64()?;
1758 let qmax = reader.f64()?;
1759 let qmin = reader.f64()?;
1760 let voltage_setpoint_pu = reader.f64()?;
1761 let machine_base_mva = reader.f64()?;
1762 let pmax = reader.f64()?;
1763 let pmin = reader.f64()?;
1764 let inverter_loss_a_mw = reader.f64()?;
1765 let inverter_loss_b_pu = reader.f64()?;
1766 let hours_online = reader.f64()?;
1767 let hours_offline = reader.f64()?;
1768 let cost = read_cost_curve(&mut reader)?;
1769 let emission_rates = read_emission_rates(&mut reader)?;
1770 let reg_bus = (flags & (1 << 7) != 0).then(|| reader.u32()).transpose()?;
1771 let agc_participation_factor = (flags & (1 << 8) != 0).then(|| reader.f64()).transpose()?;
1772 let pc1 = (flags & (1 << 9) != 0).then(|| reader.f64()).transpose()?;
1773 let pc2 = (flags & (1 << 10) != 0).then(|| reader.f64()).transpose()?;
1774 let qc1min = (flags & (1 << 11) != 0).then(|| reader.f64()).transpose()?;
1775 let qc1max = (flags & (1 << 12) != 0).then(|| reader.f64()).transpose()?;
1776 let qc2min = (flags & (1 << 13) != 0).then(|| reader.f64()).transpose()?;
1777 let qc2max = (flags & (1 << 14) != 0).then(|| reader.f64()).transpose()?;
1778 let h_inertia_s = (flags & (1 << 15) != 0).then(|| reader.f64()).transpose()?;
1779
1780 let has_inverter = inverter_loss_a_mw.abs() > 1e-20
1782 || inverter_loss_b_pu.abs() > 1e-20
1783 || (flags & (1 << 2)) != 0
1784 || (flags & (1 << 3)) != 0;
1785 let inverter = if has_inverter {
1786 Some(surge_network::network::InverterParams {
1787 curtailable: (flags & (1 << 2)) != 0,
1788 grid_forming: (flags & (1 << 3)) != 0,
1789 inverter_loss_a_mw,
1790 inverter_loss_b_pu,
1791 ..Default::default()
1792 })
1793 } else {
1794 None
1795 };
1796 let has_commitment = commitment_status != surge_network::network::CommitmentStatus::Market
1797 || hours_online.abs() > 1e-20
1798 || hours_offline.abs() > 1e-20;
1799 let commitment = if has_commitment {
1800 Some(surge_network::network::CommitmentParams {
1801 status: commitment_status,
1802 hours_online,
1803 hours_offline,
1804 ..Default::default()
1805 })
1806 } else {
1807 None
1808 };
1809 let has_fuel = (flags & (1 << 4)) != 0
1810 || emission_rates.co2.abs() > 1e-20
1811 || emission_rates.nox.abs() > 1e-20
1812 || emission_rates.so2.abs() > 1e-20
1813 || emission_rates.pm25.abs() > 1e-20;
1814 let fuel = if has_fuel {
1815 Some(surge_network::network::FuelParams {
1816 on_backup_fuel: (flags & (1 << 4)) != 0,
1817 emission_rates,
1818 ..Default::default()
1819 })
1820 } else {
1821 None
1822 };
1823 let has_rc = pc1.is_some()
1824 || pc2.is_some()
1825 || qc1min.is_some()
1826 || qc1max.is_some()
1827 || qc2min.is_some()
1828 || qc2max.is_some();
1829 let reactive_capability = if has_rc {
1830 Some(surge_network::network::ReactiveCapability {
1831 pc1,
1832 pc2,
1833 qc1min,
1834 qc1max,
1835 qc2min,
1836 qc2max,
1837 ..Default::default()
1838 })
1839 } else {
1840 None
1841 };
1842 let generator = Generator {
1843 id,
1844 bus,
1845 machine_id,
1846 voltage_regulated: (flags & (1 << 0)) != 0,
1847 in_service: (flags & (1 << 1)) != 0,
1848 quick_start: (flags & (1 << 5)) != 0,
1849 pfr_eligible: (flags & (1 << 6)) != 0,
1850 gen_type,
1851 commitment,
1852 inverter,
1853 fuel,
1854 reactive_capability,
1855 p: pg,
1856 q: qg,
1857 qmax,
1858 qmin,
1859 voltage_setpoint_pu,
1860 machine_base_mva,
1861 pmax,
1862 pmin,
1863 cost,
1864 reg_bus,
1865 agc_participation_factor,
1866 h_inertia_s,
1867 ..Generator::default()
1868 };
1869 generators.push(generator);
1870 }
1871 if !reader.is_empty() {
1872 return Err(Error::InvalidDocument(
1873 "generator section payload has trailing bytes".to_string(),
1874 ));
1875 }
1876 Ok(generators)
1877}
1878
1879fn encode_generator_extras(generators: &[Generator]) -> Result<Option<(Vec<u8>, u32)>, Error> {
1880 let extras: Vec<_> = generators
1881 .iter()
1882 .enumerate()
1883 .filter_map(|(index, generator)| {
1884 GeneratorExtra::from_generator(generator).map(|extra| IndexedGeneratorExtra {
1885 index: index as u32,
1886 extra,
1887 })
1888 })
1889 .collect();
1890 if extras.is_empty() {
1891 return Ok(None);
1892 }
1893 Ok(Some((encode_cbor(&extras)?, extras.len() as u32)))
1894}
1895
1896fn apply_generator_extras(section: &SectionView<'_>, network: &mut Network) -> Result<(), Error> {
1897 let extras: Vec<IndexedGeneratorExtra> = decode_cbor(section.payload)?;
1898 for extra in extras {
1899 let generator = network
1900 .generators
1901 .get_mut(extra.index as usize)
1902 .ok_or_else(|| {
1903 Error::InvalidDocument(format!(
1904 "generator extra index {} out of range",
1905 extra.index
1906 ))
1907 })?;
1908 extra.extra.apply(generator);
1909 }
1910 Ok(())
1911}
1912
1913fn encode_load_extras(loads: &[Load]) -> Result<Option<(Vec<u8>, u32)>, Error> {
1914 let extras: Vec<_> = loads
1915 .iter()
1916 .enumerate()
1917 .filter_map(|(index, load)| {
1918 LoadExtra::from_load(load).map(|extra| IndexedLoadExtra {
1919 index: index as u32,
1920 extra,
1921 })
1922 })
1923 .collect();
1924 if extras.is_empty() {
1925 return Ok(None);
1926 }
1927 Ok(Some((encode_cbor(&extras)?, extras.len() as u32)))
1928}
1929
1930fn apply_load_extras(section: &SectionView<'_>, network: &mut Network) -> Result<(), Error> {
1931 let extras: Vec<IndexedLoadExtra> = decode_cbor(section.payload)?;
1932 for extra in extras {
1933 let load = network.loads.get_mut(extra.index as usize).ok_or_else(|| {
1934 Error::InvalidDocument(format!("load extra index {} out of range", extra.index))
1935 })?;
1936 extra.extra.apply(load);
1937 }
1938 Ok(())
1939}
1940
1941fn encode_loads(loads: &[Load], strings: &mut StringTableBuilder) -> Vec<u8> {
1942 let mut payload = Vec::new();
1943 for load in loads {
1944 let mut flags = 0u8;
1945 if load.in_service {
1946 flags |= 1 << 0;
1947 }
1948 if load.conforming {
1949 flags |= 1 << 1;
1950 }
1951 if load.load_class.is_some() {
1952 flags |= 1 << 2;
1953 }
1954 if load.shedding_priority.is_some() {
1955 flags |= 1 << 3;
1956 }
1957 write_u32(&mut payload, load.bus);
1958 write_u32(&mut payload, strings.intern(&load.id));
1959 write_u8(&mut payload, flags);
1960 write_u8(&mut payload, encode_load_connection(load.connection));
1961 write_f64(&mut payload, load.active_power_demand_mw);
1962 write_f64(&mut payload, load.reactive_power_demand_mvar);
1963 write_f64(&mut payload, load.zip_p_impedance_frac);
1964 write_f64(&mut payload, load.zip_p_current_frac);
1965 write_f64(&mut payload, load.zip_p_power_frac);
1966 write_f64(&mut payload, load.zip_q_impedance_frac);
1967 write_f64(&mut payload, load.zip_q_current_frac);
1968 write_f64(&mut payload, load.zip_q_power_frac);
1969 write_f64(&mut payload, load.freq_sensitivity_p_pct_per_hz);
1970 write_f64(&mut payload, load.freq_sensitivity_q_pct_per_hz);
1971 write_f64(&mut payload, load.frac_motor_a);
1972 write_f64(&mut payload, load.frac_motor_b);
1973 write_f64(&mut payload, load.frac_motor_c);
1974 write_f64(&mut payload, load.frac_motor_d);
1975 write_f64(&mut payload, load.frac_electronic);
1976 write_f64(&mut payload, load.frac_static);
1977 if let Some(value) = load.load_class {
1978 write_u8(&mut payload, encode_load_class(value));
1979 }
1980 if let Some(value) = load.shedding_priority {
1981 write_u32(&mut payload, value);
1982 }
1983 }
1984 payload
1985}
1986
1987fn decode_loads(section: SectionView<'_>, strings: &StringTable) -> Result<Vec<Load>, Error> {
1988 let mut reader = Reader::new(section.payload);
1989 let mut loads = Vec::with_capacity(section.count as usize);
1990 for _ in 0..section.count {
1991 let bus = reader.u32()?;
1992 let id = strings.resolve(reader.u32()?)?.to_string();
1993 let flags = reader.u8()?;
1994 let connection = decode_load_connection(reader.u8()?)?;
1995 let has_load_class = (flags & (1 << 2)) != 0;
1996 let has_shedding_priority = (flags & (1 << 3)) != 0;
1997 let load = Load {
1998 bus,
1999 id,
2000 in_service: (flags & (1 << 0)) != 0,
2001 conforming: (flags & (1 << 1)) != 0,
2002 connection,
2003 active_power_demand_mw: reader.f64()?,
2004 reactive_power_demand_mvar: reader.f64()?,
2005 zip_p_impedance_frac: reader.f64()?,
2006 zip_p_current_frac: reader.f64()?,
2007 zip_p_power_frac: reader.f64()?,
2008 zip_q_impedance_frac: reader.f64()?,
2009 zip_q_current_frac: reader.f64()?,
2010 zip_q_power_frac: reader.f64()?,
2011 freq_sensitivity_p_pct_per_hz: reader.f64()?,
2012 freq_sensitivity_q_pct_per_hz: reader.f64()?,
2013 frac_motor_a: reader.f64()?,
2014 frac_motor_b: reader.f64()?,
2015 frac_motor_c: reader.f64()?,
2016 frac_motor_d: reader.f64()?,
2017 frac_electronic: reader.f64()?,
2018 frac_static: reader.f64()?,
2019 load_class: if has_load_class {
2020 Some(decode_load_class(reader.u8()?)?)
2021 } else {
2022 None
2023 },
2024 shedding_priority: if has_shedding_priority {
2025 Some(reader.u32()?)
2026 } else {
2027 None
2028 },
2029 owners: Vec::new(),
2030 };
2031 loads.push(load);
2032 }
2033 if !reader.is_empty() {
2034 return Err(Error::InvalidDocument(
2035 "load section payload has trailing bytes".to_string(),
2036 ));
2037 }
2038 Ok(loads)
2039}
2040
2041fn encode_network_extras(network: &Network) -> Result<Option<Vec<u8>>, Error> {
2042 let extras = NetworkExtras::from_network(network);
2043 if extras.is_empty() {
2044 return Ok(None);
2045 }
2046 Ok(Some(encode_cbor(&extras)?))
2047}
2048
2049fn encode_cbor<T: Serialize>(value: &T) -> Result<Vec<u8>, Error> {
2050 let mut payload = Vec::new();
2051 ciborium::into_writer(value, &mut payload)?;
2052 Ok(payload)
2053}
2054
2055fn decode_cbor<T: DeserializeOwned>(payload: &[u8]) -> Result<T, Error> {
2056 Ok(ciborium::from_reader(payload)?)
2057}
2058
2059fn write_cost_curve(payload: &mut Vec<u8>, cost: &Option<CostCurve>) {
2060 match cost {
2061 None => write_u8(payload, 0),
2062 Some(CostCurve::Polynomial {
2063 startup,
2064 shutdown,
2065 coeffs,
2066 }) => {
2067 write_u8(payload, 1);
2068 write_f64(payload, *startup);
2069 write_f64(payload, *shutdown);
2070 write_u32(payload, coeffs.len() as u32);
2071 for value in coeffs {
2072 write_f64(payload, *value);
2073 }
2074 }
2075 Some(CostCurve::PiecewiseLinear {
2076 startup,
2077 shutdown,
2078 points,
2079 }) => {
2080 write_u8(payload, 2);
2081 write_f64(payload, *startup);
2082 write_f64(payload, *shutdown);
2083 write_u32(payload, points.len() as u32);
2084 for (x, y) in points {
2085 write_f64(payload, *x);
2086 write_f64(payload, *y);
2087 }
2088 }
2089 }
2090}
2091
2092fn read_cost_curve(reader: &mut Reader<'_>) -> Result<Option<CostCurve>, Error> {
2093 Ok(match reader.u8()? {
2094 0 => None,
2095 1 => {
2096 let startup = reader.f64()?;
2097 let shutdown = reader.f64()?;
2098 let len = reader.u32()? as usize;
2099 let mut coeffs = Vec::with_capacity(len);
2100 for _ in 0..len {
2101 coeffs.push(reader.f64()?);
2102 }
2103 Some(CostCurve::Polynomial {
2104 startup,
2105 shutdown,
2106 coeffs,
2107 })
2108 }
2109 2 => {
2110 let startup = reader.f64()?;
2111 let shutdown = reader.f64()?;
2112 let len = reader.u32()? as usize;
2113 let mut points = Vec::with_capacity(len);
2114 for _ in 0..len {
2115 points.push((reader.f64()?, reader.f64()?));
2116 }
2117 Some(CostCurve::PiecewiseLinear {
2118 startup,
2119 shutdown,
2120 points,
2121 })
2122 }
2123 tag => {
2124 return Err(Error::InvalidDocument(format!(
2125 "unsupported generator cost curve tag {tag}"
2126 )));
2127 }
2128 })
2129}
2130
2131fn write_emission_rates(payload: &mut Vec<u8>, emission_rates: &EmissionRates) {
2132 write_f64(payload, emission_rates.co2);
2133 write_f64(payload, emission_rates.nox);
2134 write_f64(payload, emission_rates.so2);
2135 write_f64(payload, emission_rates.pm25);
2136}
2137
2138fn read_emission_rates(reader: &mut Reader<'_>) -> Result<EmissionRates, Error> {
2139 Ok(EmissionRates {
2140 co2: reader.f64()?,
2141 nox: reader.f64()?,
2142 so2: reader.f64()?,
2143 pm25: reader.f64()?,
2144 })
2145}
2146
2147fn write_u8(payload: &mut Vec<u8>, value: u8) {
2148 payload.push(value);
2149}
2150
2151fn write_u16(payload: &mut Vec<u8>, value: u16) {
2152 payload.extend_from_slice(&value.to_le_bytes());
2153}
2154
2155fn write_u32(payload: &mut Vec<u8>, value: u32) {
2156 payload.extend_from_slice(&value.to_le_bytes());
2157}
2158
2159fn write_u64(payload: &mut Vec<u8>, value: u64) {
2160 payload.extend_from_slice(&value.to_le_bytes());
2161}
2162
2163fn write_f64(payload: &mut Vec<u8>, value: f64) {
2164 payload.extend_from_slice(&value.to_le_bytes());
2165}
2166
2167fn write_bool(payload: &mut Vec<u8>, value: bool) {
2168 write_u8(payload, u8::from(value));
2169}
2170
2171fn read_bool(reader: &mut Reader<'_>) -> Result<bool, Error> {
2172 Ok(match reader.u8()? {
2173 0 => false,
2174 1 => true,
2175 value => {
2176 return Err(Error::InvalidDocument(format!(
2177 "invalid encoded boolean value {value}"
2178 )));
2179 }
2180 })
2181}
2182
2183fn write_bytes(payload: &mut Vec<u8>, bytes: &[u8]) {
2184 write_u32(payload, bytes.len() as u32);
2185 payload.extend_from_slice(bytes);
2186}
2187
2188fn read_bytes(reader: &mut Reader<'_>) -> Result<Vec<u8>, Error> {
2189 let len = reader.u32()? as usize;
2190 Ok(reader.take(len)?.to_vec())
2191}
2192
2193fn encode_bus_type(value: BusType) -> u8 {
2194 match value {
2195 BusType::PQ => 1,
2196 BusType::PV => 2,
2197 BusType::Slack => 3,
2198 BusType::Isolated => 4,
2199 }
2200}
2201
2202fn decode_bus_type(value: u8) -> Result<BusType, Error> {
2203 match value {
2204 1 => Ok(BusType::PQ),
2205 2 => Ok(BusType::PV),
2206 3 => Ok(BusType::Slack),
2207 4 => Ok(BusType::Isolated),
2208 _ => Err(Error::InvalidDocument(format!(
2209 "invalid bus type code {value}"
2210 ))),
2211 }
2212}
2213
2214fn encode_transformer_connection(value: TransformerConnection) -> u8 {
2215 match value {
2216 TransformerConnection::WyeGWyeG => 0,
2217 TransformerConnection::WyeGDelta => 1,
2218 TransformerConnection::DeltaWyeG => 2,
2219 TransformerConnection::DeltaDelta => 3,
2220 TransformerConnection::WyeGWye => 4,
2221 }
2222}
2223
2224fn decode_transformer_connection(value: u8) -> Result<TransformerConnection, Error> {
2225 match value {
2226 0 => Ok(TransformerConnection::WyeGWyeG),
2227 1 => Ok(TransformerConnection::WyeGDelta),
2228 2 => Ok(TransformerConnection::DeltaWyeG),
2229 3 => Ok(TransformerConnection::DeltaDelta),
2230 4 => Ok(TransformerConnection::WyeGWye),
2231 _ => Err(Error::InvalidDocument(format!(
2232 "invalid transformer connection code {value}"
2233 ))),
2234 }
2235}
2236
2237fn encode_tap_mode(value: TapMode) -> u8 {
2238 match value {
2239 TapMode::Fixed => 0,
2240 TapMode::Continuous => 1,
2241 }
2242}
2243
2244fn decode_tap_mode(value: u8) -> Result<TapMode, Error> {
2245 match value {
2246 0 => Ok(TapMode::Fixed),
2247 1 => Ok(TapMode::Continuous),
2248 _ => Err(Error::InvalidDocument(format!(
2249 "invalid tap mode code {value}"
2250 ))),
2251 }
2252}
2253
2254fn encode_phase_mode(value: PhaseMode) -> u8 {
2255 match value {
2256 PhaseMode::Fixed => 0,
2257 PhaseMode::Continuous => 1,
2258 }
2259}
2260
2261fn decode_phase_mode(value: u8) -> Result<PhaseMode, Error> {
2262 match value {
2263 0 => Ok(PhaseMode::Fixed),
2264 1 => Ok(PhaseMode::Continuous),
2265 _ => Err(Error::InvalidDocument(format!(
2266 "invalid phase mode code {value}"
2267 ))),
2268 }
2269}
2270
2271fn encode_branch_type(value: BranchType) -> u8 {
2272 match value {
2273 BranchType::Line => 0,
2274 BranchType::Transformer => 1,
2275 BranchType::Transformer3W => 2,
2276 BranchType::SeriesCapacitor => 3,
2277 BranchType::ZeroImpedanceTie => 4,
2278 }
2279}
2280
2281fn decode_branch_type(value: u8) -> Result<BranchType, Error> {
2282 match value {
2283 0 => Ok(BranchType::Line),
2284 1 => Ok(BranchType::Transformer),
2285 2 => Ok(BranchType::Transformer3W),
2286 3 => Ok(BranchType::SeriesCapacitor),
2287 4 => Ok(BranchType::ZeroImpedanceTie),
2288 _ => Err(Error::InvalidDocument(format!(
2289 "invalid branch type code {value}"
2290 ))),
2291 }
2292}
2293
2294fn encode_gen_type(value: GenType) -> u8 {
2295 match value {
2296 GenType::Synchronous => 0,
2297 GenType::Asynchronous => 1,
2298 GenType::InverterBased => 2,
2299 GenType::Hybrid => 3,
2300 GenType::Unknown => 4,
2301 }
2302}
2303
2304fn decode_gen_type(value: u8) -> Result<GenType, Error> {
2305 match value {
2306 0 => Ok(GenType::Synchronous),
2307 1 => Ok(GenType::Asynchronous),
2308 2 => Ok(GenType::InverterBased),
2309 3 => Ok(GenType::Hybrid),
2310 4 => Ok(GenType::Unknown),
2311 _ => Err(Error::InvalidDocument(format!(
2312 "invalid generator type code {value}"
2313 ))),
2314 }
2315}
2316
2317fn encode_commitment_status(value: surge_network::network::CommitmentStatus) -> u8 {
2318 match value {
2319 surge_network::network::CommitmentStatus::Market => 0,
2320 surge_network::network::CommitmentStatus::SelfCommitted => 1,
2321 surge_network::network::CommitmentStatus::MustRun => 2,
2322 surge_network::network::CommitmentStatus::Unavailable => 3,
2323 surge_network::network::CommitmentStatus::EmergencyOnly => 4,
2324 }
2325}
2326
2327fn decode_commitment_status(value: u8) -> Result<surge_network::network::CommitmentStatus, Error> {
2328 match value {
2329 0 => Ok(surge_network::network::CommitmentStatus::Market),
2330 1 => Ok(surge_network::network::CommitmentStatus::SelfCommitted),
2331 2 => Ok(surge_network::network::CommitmentStatus::MustRun),
2332 3 => Ok(surge_network::network::CommitmentStatus::Unavailable),
2333 4 => Ok(surge_network::network::CommitmentStatus::EmergencyOnly),
2334 _ => Err(Error::InvalidDocument(format!(
2335 "invalid commitment status code {value}"
2336 ))),
2337 }
2338}
2339
2340fn encode_load_connection(value: LoadConnection) -> u8 {
2341 match value {
2342 LoadConnection::WyeGrounded => 0,
2343 LoadConnection::WyeUngrounded => 1,
2344 LoadConnection::Delta => 2,
2345 }
2346}
2347
2348fn decode_load_connection(value: u8) -> Result<LoadConnection, Error> {
2349 match value {
2350 0 => Ok(LoadConnection::WyeGrounded),
2351 1 => Ok(LoadConnection::WyeUngrounded),
2352 2 => Ok(LoadConnection::Delta),
2353 _ => Err(Error::InvalidDocument(format!(
2354 "invalid load connection code {value}"
2355 ))),
2356 }
2357}
2358
2359fn encode_load_class(value: LoadClass) -> u8 {
2360 match value {
2361 LoadClass::Residential => 0,
2362 LoadClass::Commercial => 1,
2363 LoadClass::Industrial => 2,
2364 LoadClass::Agricultural => 3,
2365 LoadClass::DataCenter => 4,
2366 LoadClass::EvCharging => 5,
2367 LoadClass::Other => 6,
2368 }
2369}
2370
2371fn decode_load_class(value: u8) -> Result<LoadClass, Error> {
2372 match value {
2373 0 => Ok(LoadClass::Residential),
2374 1 => Ok(LoadClass::Commercial),
2375 2 => Ok(LoadClass::Industrial),
2376 3 => Ok(LoadClass::Agricultural),
2377 4 => Ok(LoadClass::DataCenter),
2378 5 => Ok(LoadClass::EvCharging),
2379 6 => Ok(LoadClass::Other),
2380 _ => Err(Error::InvalidDocument(format!(
2381 "invalid load class code {value}"
2382 ))),
2383 }
2384}
2385
2386#[cfg(test)]
2387mod tests {
2388 use super::*;
2389 use surge_network::network::{
2390 Branch, Bus, GenType, Generator, GeneratorTechnology, OwnershipEntry,
2391 };
2392
2393 fn mini_network() -> Network {
2394 let mut network = Network::new("test_bin");
2395 network.base_mva = 100.0;
2396 network.buses.push(Bus::new(1, BusType::Slack, 138.0));
2397 network.buses.push(Bus::new(2, BusType::PQ, 138.0));
2398 network.buses[0].latitude = Some(30.0);
2399 network.buses[0].reserve_zone = Some("Reserve Zone 1".to_string());
2400 network.buses[0].owners = vec![OwnershipEntry {
2401 owner: 7,
2402 fraction: 1.0,
2403 }];
2404 let mut generator = Generator::new(1, 100.0, 1.06);
2405 generator.cost = Some(CostCurve::Polynomial {
2406 startup: 10.0,
2407 shutdown: 5.0,
2408 coeffs: vec![0.01, 1.5, 3.0],
2409 });
2410 generator.gen_type = GenType::InverterBased;
2411 generator.technology = Some(GeneratorTechnology::SolarPv);
2412 generator.source_technology_code = Some("PV".to_string());
2413 generator
2414 .fuel
2415 .get_or_insert_with(Default::default)
2416 .fuel_type = Some("solar".to_string());
2417 generator.storage = Some(StorageParams::with_energy_capacity_mwh(50.0));
2418 generator.owners = vec![OwnershipEntry {
2419 owner: 11,
2420 fraction: 0.75,
2421 }];
2422 network.generators.push(generator);
2423 let mut branch = Branch::new_line(1, 2, 0.01, 0.1, 0.02);
2424 {
2425 let zs = branch.zero_seq.get_or_insert_with(ZeroSeqData::default);
2426 zs.r0 = 0.03;
2427 }
2428 {
2429 let td = branch
2430 .transformer_data
2431 .get_or_insert_with(TransformerData::default);
2432 td.parent_transformer_id = Some("tx1".to_string());
2433 }
2434 branch.owners = vec![OwnershipEntry {
2435 owner: 13,
2436 fraction: 0.5,
2437 }];
2438 network.branches.push(branch);
2439 let mut load = Load::new(2, 50.0, 20.0);
2440 load.owners = vec![OwnershipEntry {
2441 owner: 17,
2442 fraction: 1.0,
2443 }];
2444 network.loads.push(load);
2445 network.market_data.reserve_zones.push(ReserveZone {
2446 name: "Reserve Zone 1".to_string(),
2447 zonal_requirements: Vec::new(),
2448 });
2449 network
2450 }
2451
2452 #[test]
2453 fn test_roundtrip_preserves_network_json_shape() {
2454 let network = mini_network();
2455 let before = crate::json::encode_network(&network).expect("encode original");
2456 let bytes = dumps(&network).expect("failed to dump binary");
2457 let parsed = loads(&bytes).expect("failed to parse binary");
2458 let after = crate::json::encode_network(&parsed).expect("encode parsed");
2459 assert_eq!(before, after);
2460 }
2461
2462 #[test]
2463 fn test_file_roundtrip() {
2464 let network = mini_network();
2465 let tmp = std::env::temp_dir().join("surge_test_roundtrip.surge.bin");
2466 save(&network, &tmp).expect("failed to save binary");
2467 let parsed = load(&tmp).expect("failed to load binary");
2468 assert_eq!(parsed.name, network.name);
2469 assert_eq!(parsed.n_buses(), network.n_buses());
2470 assert_eq!(parsed.buses[0].owners, network.buses[0].owners);
2471 assert_eq!(parsed.branches[0].owners, network.branches[0].owners);
2472 assert_eq!(parsed.generators[0].owners, network.generators[0].owners);
2473 assert_eq!(parsed.loads[0].owners, network.loads[0].owners);
2474 let _ = std::fs::remove_file(&tmp);
2475 }
2476
2477 #[test]
2478 fn test_corrupt_checksum_is_rejected() {
2479 let network = mini_network();
2480 let mut bytes = dumps(&network).expect("failed to dump binary");
2481 *bytes.last_mut().expect("binary output is non-empty") ^= 0x01;
2482 let result = loads(&bytes);
2483 assert!(result.is_err(), "corrupt payload should be rejected");
2484 }
2485}