1use std::collections::HashMap;
32
33use surge_network::dynamics::{
34 DynamicModel, Esdc1aParams, Esst1aParams, ExciterDyn, ExciterModel, GastParams, GenclsParams,
35 GeneratorDyn, GeneratorModel, GenrouParams, GensalParams, GovernorDyn, GovernorModel,
36 HygovParams, Oel1bParams, OelDyn, OelModel, Pss1aParams, Pss2bParams, PssDyn, PssModel,
37 ScrxParams, SexsParams, Tgov1Params, Uel1Params, UelDyn, UelModel,
38};
39use thiserror::Error;
40
41use super::{CgmesError, CimObj, ObjMap, collect_objects};
42
43#[derive(Error, Debug)]
49pub enum CgmesDyError {
50 #[error("missing required parameter '{0}' on {1}")]
52 MissingParam(String, String),
53 #[error("CGMES parse error: {0}")]
55 Cgmes(#[from] CgmesError),
56 #[error("could not resolve SynchronousMachine for dynamics object '{0}'")]
58 UnresolvedMachine(String),
59}
60
61pub fn parse_cgmes_dy(
77 dy_xml: &[&str],
78 sm_bus_map: &HashMap<String, (u32, String)>,
79) -> Result<DynamicModel, CgmesDyError> {
80 let mut objects: ObjMap = ObjMap::new();
82 for xml in dy_xml {
83 collect_objects(xml, &mut objects)?;
84 }
85
86 let smd_to_sm: HashMap<String, String> = objects
90 .iter()
91 .filter(|(_, o)| {
92 matches!(
94 o.class.as_str(),
95 "SynchronousMachineTimeConstantReactance"
96 | "SynchronousMachineSimplified"
97 | "SynchronousMachineEquivalentCircuit"
98 | "SynchronousMachineDetailedFDX"
99 | "SynchronousMachineDetailed"
100 )
101 })
102 .filter_map(|(id, o)| {
103 let sm_ref = o.get_ref("SynchronousMachine")?;
104 Some((id.clone(), sm_ref.to_string()))
105 })
106 .collect();
107
108 let mut dm = DynamicModel::default();
110
111 for (obj_id, obj) in &objects {
112 let cls = obj.class.as_str();
113
114 match cls {
116 "SynchronousMachineTimeConstantReactance" => {
120 let sm_mrid_direct = obj.get_ref("SynchronousMachine").map(|s| s.to_string());
122 let effective_sm = sm_mrid_direct.as_deref().unwrap_or("");
123
124 let (bus, machine_id) = match sm_bus_map.get(effective_sm) {
125 Some(pair) => pair.clone(),
126 None => {
127 tracing::warn!(
128 obj_id,
129 sm_mrid = effective_sm,
130 "SynchronousMachineTimeConstantReactance: cannot resolve SM to bus — skipping"
131 );
132 continue;
133 }
134 };
135
136 let rotor_type = obj.get_text("rotorType").unwrap_or("roundRotor");
137 let is_salient = rotor_type.contains("salientPole")
138 || rotor_type.ends_with("salient")
139 || rotor_type.contains("Salient");
140
141 let h = require_f64(obj, "inertia", obj_id)?;
142 let d = obj.parse_f64("damping").unwrap_or(0.0);
143 let xd = require_f64(obj, "xDirectSync", obj_id)?;
144 let xq = require_f64(obj, "xQuadSync", obj_id)?;
145 let xd_prime = require_f64(obj, "xDirectTrans", obj_id)?;
146 let xd_pprime = require_f64(obj, "xDirectSubtrans", obj_id)?;
147 let xl = obj.parse_f64("statorLeakageReactance").unwrap_or(0.0);
148 let td0_prime = require_f64(obj, "tpdo", obj_id)?;
149 let td0_pprime = require_f64(obj, "tppdo", obj_id)?;
150 let tq0_pprime = require_f64(obj, "tppqo", obj_id)?;
151 let s1 = obj.parse_f64("saturationFactor").unwrap_or(0.0);
152 let s12 = obj.parse_f64("saturationFactor120").unwrap_or(0.0);
153
154 if is_salient {
155 let xtran = obj.parse_f64("xQuadTrans").unwrap_or(xd_prime);
157 dm.generators.push(GeneratorDyn {
158 bus,
159 machine_id,
160 model: GeneratorModel::Gensal(GensalParams {
161 td0_prime,
162 td0_pprime,
163 tq0_pprime,
164 h,
165 d,
166 xd,
167 xq,
168 xd_prime,
169 xd_pprime,
170 xl,
171 s1,
172 s12,
173 xtran,
174 }),
175 });
176 } else {
177 let tq0_prime = obj.parse_f64("tpqo").unwrap_or(0.4);
179 let xq_prime = obj.parse_f64("xQuadTrans").unwrap_or(xq * 0.6);
180 dm.generators.push(GeneratorDyn {
181 bus,
182 machine_id,
183 model: GeneratorModel::Genrou(GenrouParams {
184 td0_prime,
185 td0_pprime,
186 tq0_prime,
187 tq0_pprime,
188 h,
189 d,
190 xd,
191 xq,
192 xd_prime,
193 xq_prime,
194 xd_pprime,
195 xl,
196 s1,
197 s12,
198 ra: obj.parse_f64("statorResistance"),
199 }),
200 });
201 }
202 }
203
204 "SynchronousMachineSimplified" => {
205 let sm_mrid_direct = obj.get_ref("SynchronousMachine").map(|s| s.to_string());
206 let effective_sm = sm_mrid_direct.as_deref().unwrap_or("");
207
208 let (bus, machine_id) = match sm_bus_map.get(effective_sm) {
209 Some(pair) => pair.clone(),
210 None => {
211 tracing::warn!(
212 obj_id,
213 sm_mrid = effective_sm,
214 "SynchronousMachineSimplified: cannot resolve SM to bus — skipping"
215 );
216 continue;
217 }
218 };
219
220 let h = require_f64(obj, "inertia", obj_id)?;
221 let d = obj.parse_f64("damping").unwrap_or(0.0);
222 dm.generators.push(GeneratorDyn {
223 bus,
224 machine_id,
225 model: GeneratorModel::Gencls(GenclsParams { h, d }),
226 });
227 }
228
229 "ExcIEEEST1A" => {
233 let (bus, machine_id) = match resolve_sm(obj, &smd_to_sm, sm_bus_map, obj_id) {
234 Some(pair) => pair,
235 None => continue,
236 };
237 let tr = obj.parse_f64("tr").unwrap_or(0.0);
238 let vimax = obj.parse_f64("vimax").unwrap_or(999.0);
239 let vimin = obj.parse_f64("vimin").unwrap_or(-999.0);
240 let tc = require_f64(obj, "tc", obj_id)?;
241 let tb = require_f64(obj, "tb", obj_id)?;
242 let tc1 = obj.parse_f64("tc1").unwrap_or(0.0);
243 let tb1 = obj.parse_f64("tb1").unwrap_or(0.0);
244 let ka = require_f64(obj, "ka", obj_id)?;
245 let ta = obj.parse_f64("ta").unwrap_or(0.0);
246 let vamax = obj.parse_f64("vamax").unwrap_or(14.5);
247 let vamin = obj.parse_f64("vamin").unwrap_or(-14.5);
248 let vrmax = require_f64(obj, "vrmax", obj_id)?;
249 let vrmin = require_f64(obj, "vrmin", obj_id)?;
250 let kc = obj.parse_f64("kc").unwrap_or(0.0);
251 let kf = obj.parse_f64("kf").unwrap_or(0.0);
252 let tf = obj.parse_f64("tf").unwrap_or(1.0);
253 let klr = obj.parse_f64("klr").unwrap_or(0.0);
254 let ilr = obj.parse_f64("ilr").unwrap_or(0.0);
255 dm.exciters.push(ExciterDyn {
256 bus,
257 machine_id,
258 model: ExciterModel::Esst1a(Esst1aParams {
259 tr,
260 vimax,
261 vimin,
262 tc,
263 tb,
264 tc1,
265 tb1,
266 ka,
267 ta,
268 vamax,
269 vamin,
270 vrmax,
271 vrmin,
272 kc,
273 kf,
274 tf,
275 klr,
276 ilr,
277 }),
278 });
279 }
280
281 "ExcDC1A" | "ExcIEEEDC1A" => {
282 let (bus, machine_id) = match resolve_sm(obj, &smd_to_sm, sm_bus_map, obj_id) {
283 Some(pair) => pair,
284 None => continue,
285 };
286 let tr = obj.parse_f64("tr").unwrap_or(0.0);
287 let ka = require_f64(obj, "ka", obj_id)?;
288 let ta = require_f64(obj, "ta", obj_id)?;
289 let vrmax = require_f64(obj, "vrmax", obj_id)?;
290 let vrmin = require_f64(obj, "vrmin", obj_id)?;
291 let ke = obj.parse_f64("ke").unwrap_or(1.0);
292 let te = require_f64(obj, "te", obj_id)?;
293 let kf = require_f64(obj, "kf", obj_id)?;
294 let tf = obj
295 .parse_f64("tf1")
296 .or_else(|| obj.parse_f64("tf"))
297 .unwrap_or(1.0);
298 let e1 = obj.parse_f64("e1").unwrap_or(0.0);
299 let se1 = obj.parse_f64("se1").unwrap_or(0.0);
300 let e2 = obj.parse_f64("e2").unwrap_or(0.0);
301 let se2 = obj.parse_f64("se2").unwrap_or(0.0);
302 dm.exciters.push(ExciterDyn {
303 bus,
304 machine_id,
305 model: ExciterModel::Esdc1a(Esdc1aParams {
306 tr,
307 ka,
308 ta,
309 kf,
310 tf,
311 ke,
312 te,
313 e1,
314 se1,
315 e2,
316 se2,
317 vrmax,
318 vrmin,
319 }),
320 });
321 }
322
323 "ExcSEXS" => {
324 let (bus, machine_id) = match resolve_sm(obj, &smd_to_sm, sm_bus_map, obj_id) {
325 Some(pair) => pair,
326 None => continue,
327 };
328 let tb = require_f64(obj, "tb", obj_id)?;
329 let tc = require_f64(obj, "tc", obj_id)?;
330 let k = require_f64(obj, "k", obj_id)?;
331 let te = require_f64(obj, "te", obj_id)?;
332 let emin = require_f64(obj, "emin", obj_id)?;
333 let emax = require_f64(obj, "emax", obj_id)?;
334 dm.exciters.push(ExciterDyn {
335 bus,
336 machine_id,
337 model: ExciterModel::Sexs(SexsParams {
338 tb,
339 tc,
340 k,
341 te,
342 emin,
343 emax,
344 }),
345 });
346 }
347
348 "ExcSCRX" => {
349 let (bus, machine_id) = match resolve_sm(obj, &smd_to_sm, sm_bus_map, obj_id) {
350 Some(pair) => pair,
351 None => continue,
352 };
353 let tr = obj.parse_f64("tr").unwrap_or(0.0);
354 let k = require_f64(obj, "k", obj_id)?;
355 let te = require_f64(obj, "te", obj_id)?;
356 let emin = require_f64(obj, "emin", obj_id)?;
357 let emax = require_f64(obj, "emax", obj_id)?;
358 let rcrfd = obj.parse_f64("rcrfd");
359 dm.exciters.push(ExciterDyn {
360 bus,
361 machine_id,
362 model: ExciterModel::Scrx(ScrxParams {
363 tr,
364 k,
365 te,
366 emin,
367 emax,
368 rcrfd,
369 }),
370 });
371 }
372
373 "GovGAST" => {
377 let (bus, machine_id) = match resolve_sm(obj, &smd_to_sm, sm_bus_map, obj_id) {
378 Some(pair) => pair,
379 None => continue,
380 };
381 let r = require_f64(obj, "r", obj_id)?;
382 let t1 = require_f64(obj, "t1", obj_id)?;
383 let t2 = require_f64(obj, "t2", obj_id)?;
384 let t3 = require_f64(obj, "t3", obj_id)?;
385 let at = obj.parse_f64("at").unwrap_or(1.0);
386 let kt = obj.parse_f64("kt").unwrap_or(2.0);
387 let vmin = obj.parse_f64("vmin").unwrap_or(0.0);
388 let vmax = obj.parse_f64("vmax").unwrap_or(1.0);
389 dm.governors.push(GovernorDyn {
390 bus,
391 machine_id,
392 model: GovernorModel::Gast(GastParams {
393 r,
394 t1,
395 t2,
396 t3,
397 at,
398 kt,
399 vmin,
400 vmax,
401 }),
402 });
403 }
404
405 "GovSteamIEEE1" | "GovSteam1" => {
406 let (bus, machine_id) = match resolve_sm(obj, &smd_to_sm, sm_bus_map, obj_id) {
407 Some(pair) => pair,
408 None => continue,
409 };
410 let r = require_f64(obj, "r", obj_id)?;
411 let t1 = require_f64(obj, "t1", obj_id)?;
412 let vmax = require_f64(obj, "vmax", obj_id)?;
413 let vmin = require_f64(obj, "vmin", obj_id)?;
414 let t2 = obj.parse_f64("t2").unwrap_or(0.0);
415 let t3 = require_f64(obj, "t3", obj_id)?;
416 let dt = obj.parse_f64("dt");
417 dm.governors.push(GovernorDyn {
418 bus,
419 machine_id,
420 model: GovernorModel::Tgov1(Tgov1Params {
421 r,
422 t1,
423 vmax,
424 vmin,
425 t2,
426 t3,
427 dt,
428 }),
429 });
430 }
431
432 "GovHydro1" => {
433 let (bus, machine_id) = match resolve_sm(obj, &smd_to_sm, sm_bus_map, obj_id) {
434 Some(pair) => pair,
435 None => continue,
436 };
437 let r = require_f64(obj, "r", obj_id)?;
438 let tp = require_f64(obj, "tp", obj_id)?;
439 let velm = obj.parse_f64("velm").unwrap_or(0.2);
440 let tg = require_f64(obj, "tg", obj_id)?;
441 let gmax = obj.parse_f64("gmax").unwrap_or(1.0);
442 let gmin = obj.parse_f64("gmin").unwrap_or(0.0);
443 let tw = require_f64(obj, "tw", obj_id)?;
444 let at = obj.parse_f64("at").unwrap_or(1.2);
445 let dturb = obj.parse_f64("dturb").unwrap_or(0.5);
446 let qnl = obj.parse_f64("qnl").unwrap_or(0.08);
447 dm.governors.push(GovernorDyn {
448 bus,
449 machine_id,
450 model: GovernorModel::Hygov(HygovParams {
451 r,
452 tp,
453 velm,
454 tg,
455 gmax,
456 gmin,
457 tw,
458 at,
459 dturb,
460 qnl,
461 }),
462 });
463 }
464
465 "PssIEEE1A" => {
469 let (bus, machine_id) = match resolve_sm(obj, &smd_to_sm, sm_bus_map, obj_id) {
470 Some(pair) => pair,
471 None => continue,
472 };
473 let ks = require_f64(obj, "ks", obj_id)?;
474 let t1 = require_f64(obj, "t1", obj_id)?;
475 let t2 = require_f64(obj, "t2", obj_id)?;
476 let t3 = require_f64(obj, "t3", obj_id)?;
477 let t4 = require_f64(obj, "t4", obj_id)?;
478 let vstmax = require_f64(obj, "vstmax", obj_id)?;
479 let vstmin = require_f64(obj, "vstmin", obj_id)?;
480 dm.pss.push(PssDyn {
481 bus,
482 machine_id,
483 model: PssModel::Pss1a(Pss1aParams {
484 ks,
485 t1,
486 t2,
487 t3,
488 t4,
489 vstmax,
490 vstmin,
491 }),
492 });
493 }
494
495 "PssIEEE2B" | "Pss2B" => {
496 let (bus, machine_id) = match resolve_sm(obj, &smd_to_sm, sm_bus_map, obj_id) {
497 Some(pair) => pair,
498 None => continue,
499 };
500 let m1 = obj.parse_f64("m").unwrap_or(5.0);
501 let t6 = obj.parse_f64("t6").unwrap_or(0.0);
502 let t7 = require_f64(obj, "t7", obj_id)?;
503 let ks2 = obj.parse_f64("ks2").unwrap_or(0.99);
504 let t8 = require_f64(obj, "t8", obj_id)?;
505 let t9 = require_f64(obj, "t9", obj_id)?;
506 let m2 = obj.parse_f64("n").unwrap_or(1.0);
507 let tw1 = require_f64(obj, "tw1", obj_id)?;
508 let tw2 = require_f64(obj, "tw2", obj_id)?;
509 let tw3 = require_f64(obj, "tw3", obj_id)?;
510 let tw4 = obj.parse_f64("tw4").unwrap_or(0.0);
511 let t1 = require_f64(obj, "t1", obj_id)?;
512 let t2 = require_f64(obj, "t2", obj_id)?;
513 let t3 = require_f64(obj, "t3", obj_id)?;
514 let t4 = require_f64(obj, "t4", obj_id)?;
515 let ks1 = require_f64(obj, "ks1", obj_id)?;
516 let ks3 = obj.parse_f64("ks3").unwrap_or(1.0);
517 let vstmax = require_f64(obj, "vstmax", obj_id)?;
518 let vstmin = require_f64(obj, "vstmin", obj_id)?;
519 let t10 = obj.parse_f64("t10").unwrap_or(0.0);
520 let t11 = obj.parse_f64("t11").unwrap_or(0.0);
521 dm.pss.push(PssDyn {
522 bus,
523 machine_id,
524 model: PssModel::Pss2b(Pss2bParams {
525 m1,
526 t6,
527 t7,
528 ks2,
529 t8,
530 t9,
531 m2,
532 tw1,
533 tw2,
534 tw3,
535 tw4,
536 t1,
537 t2,
538 t3,
539 t4,
540 ks1,
541 ks3,
542 vstmax,
543 vstmin,
544 t10,
545 t11,
546 }),
547 });
548 }
549
550 "OverexcLimIEEE" | "OverexcLimX1" | "OverexcLimX2" => {
554 let (bus, machine_id) = match resolve_sm(obj, &smd_to_sm, sm_bus_map, obj_id) {
555 Some(pair) => pair,
556 None => continue,
557 };
558 let ifdmax = require_f64(obj, "ifdmax", obj_id)?;
559 let ifdlim = obj.parse_f64("ifdlim").unwrap_or(ifdmax * 1.05);
560 let vrmax = obj.parse_f64("vrmax").unwrap_or(5.0);
561 let vamin = obj.parse_f64("vamin").unwrap_or(-5.0);
562 let kramp = obj.parse_f64("kramp").unwrap_or(10.0);
563 let tff = obj.parse_f64("tff").unwrap_or(0.05);
564 dm.oels.push(OelDyn {
565 bus,
566 machine_id,
567 model: OelModel::Oel1b(Oel1bParams {
568 ifdmax,
569 ifdlim,
570 vrmax,
571 vamin,
572 kramp,
573 tff,
574 }),
575 });
576 }
577
578 "UnderexcLimIEEE1" | "UnderexcLim2Simplified" => {
579 let (bus, machine_id) = match resolve_sm(obj, &smd_to_sm, sm_bus_map, obj_id) {
580 Some(pair) => pair,
581 None => continue,
582 };
583 let kul = require_f64(obj, "kul", obj_id)?;
584 let tu1 = obj.parse_f64("tu1").unwrap_or(0.0);
585 let vucmax = obj.parse_f64("vucmax").unwrap_or(5.0);
586 let vucmin = obj.parse_f64("vucmin").unwrap_or(-5.0);
587 let kur = obj.parse_f64("kur").unwrap_or(0.0);
588 dm.uels.push(UelDyn {
589 bus,
590 machine_id,
591 model: UelModel::Uel1(Uel1Params {
592 kul,
593 tu1,
594 vucmax,
595 vucmin,
596 kur,
597 }),
598 });
599 }
600
601 "SynchronousMachineDynamics"
605 | "SynchronousMachineEquivalentCircuit"
606 | "SynchronousMachineDetailedFDX"
607 | "SynchronousMachineDetailed"
608 | "FullModel"
609 | "Model"
610 | "Analog"
611 | "Control"
612 | "Terminal"
613 | "TopologicalNode" => {
614 }
616
617 _ => {
621 tracing::warn!(
622 class = cls,
623 obj_id,
624 "CGMES DY: unrecognised dynamics class — skipping"
625 );
626 }
627 }
628 }
629
630 Ok(dm)
631}
632
633fn require_f64(obj: &CimObj, key: &str, obj_id: &str) -> Result<f64, CgmesDyError> {
639 obj.parse_f64(key).ok_or_else(|| {
640 CgmesDyError::MissingParam(key.to_string(), format!("{}({})", obj.class, obj_id))
641 })
642}
643
644fn resolve_sm(
654 obj: &CimObj,
655 smd_to_sm: &HashMap<String, String>,
656 sm_bus_map: &HashMap<String, (u32, String)>,
657 obj_id: &str,
658) -> Option<(u32, String)> {
659 let sm_mrid: Option<String> = obj
660 .get_ref("SynchronousMachineDynamics")
661 .and_then(|smd_id| smd_to_sm.get(smd_id))
662 .map(|s| s.to_string())
663 .or_else(|| obj.get_ref("SynchronousMachine").map(|s| s.to_string()))
664 .or_else(|| {
667 obj.get_ref("ExcitationSystemDynamics")
668 .and_then(|smd_id| smd_to_sm.get(smd_id))
669 .map(|s| s.to_string())
670 });
671
672 let sm_mrid = match sm_mrid {
673 Some(m) => m,
674 None => {
675 tracing::warn!(
676 class = obj.class.as_str(),
677 obj_id,
678 "CGMES DY: cannot resolve SynchronousMachine reference — skipping"
679 );
680 return None;
681 }
682 };
683
684 match sm_bus_map.get(&sm_mrid) {
685 Some(pair) => Some(pair.clone()),
686 None => {
687 tracing::warn!(
688 class = obj.class.as_str(),
689 obj_id,
690 sm_mrid,
691 "CGMES DY: SM mRID not found in bus map (EQ profile may not include this SM) — skipping"
692 );
693 None
694 }
695 }
696}
697
698#[cfg(test)]
703mod tests {
704 use super::*;
705
706 fn single_sm_map(mrid: &str, bus: u32, id: &str) -> HashMap<String, (u32, String)> {
708 let mut m = HashMap::new();
709 m.insert(mrid.to_string(), (bus, id.to_string()));
710 m
711 }
712
713 #[test]
718 fn test_cgmes_dy_genrou() {
719 let dy_xml = r##"<?xml version="1.0"?>
720<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
721 xmlns:cim="http://iec.ch/TC57/CIM100#">
722 <cim:SynchronousMachineTimeConstantReactance rdf:ID="gen-dyn-001">
723 <cim:SynchronousMachineDynamics.SynchronousMachine rdf:resource="#sm-001"/>
724 <cim:SynchronousMachineTimeConstantReactance.rotorType>roundRotor</cim:SynchronousMachineTimeConstantReactance.rotorType>
725 <cim:RotatingMachineDynamics.inertia>6.5</cim:RotatingMachineDynamics.inertia>
726 <cim:RotatingMachineDynamics.damping>0.0</cim:RotatingMachineDynamics.damping>
727 <cim:SynchronousMachineTimeConstantReactance.tpdo>8.0</cim:SynchronousMachineTimeConstantReactance.tpdo>
728 <cim:SynchronousMachineTimeConstantReactance.tppdo>0.03</cim:SynchronousMachineTimeConstantReactance.tppdo>
729 <cim:SynchronousMachineTimeConstantReactance.tpqo>0.4</cim:SynchronousMachineTimeConstantReactance.tpqo>
730 <cim:SynchronousMachineTimeConstantReactance.tppqo>0.05</cim:SynchronousMachineTimeConstantReactance.tppqo>
731 <cim:SynchronousMachineTimeConstantReactance.xDirectSync>1.8</cim:SynchronousMachineTimeConstantReactance.xDirectSync>
732 <cim:SynchronousMachineTimeConstantReactance.xQuadSync>1.7</cim:SynchronousMachineTimeConstantReactance.xQuadSync>
733 <cim:SynchronousMachineTimeConstantReactance.xDirectTrans>0.3</cim:SynchronousMachineTimeConstantReactance.xDirectTrans>
734 <cim:SynchronousMachineTimeConstantReactance.xDirectSubtrans>0.25</cim:SynchronousMachineTimeConstantReactance.xDirectSubtrans>
735 <cim:SynchronousMachineTimeConstantReactance.xQuadTrans>0.55</cim:SynchronousMachineTimeConstantReactance.xQuadTrans>
736 <cim:SynchronousMachineTimeConstantReactance.statorLeakageReactance>0.2</cim:SynchronousMachineTimeConstantReactance.statorLeakageReactance>
737 </cim:SynchronousMachineTimeConstantReactance>
738</rdf:RDF>"##;
739
740 let sm_bus_map = single_sm_map("sm-001", 1, "1");
741 let dm = parse_cgmes_dy(&[dy_xml], &sm_bus_map).unwrap();
742 assert_eq!(dm.generators.len(), 1, "should have 1 generator");
743 let gdyn = &dm.generators[0];
744 assert_eq!(gdyn.bus, 1);
745 match &gdyn.model {
746 GeneratorModel::Genrou(p) => {
747 assert!((p.h - 6.5).abs() < 1e-9, "inertia H");
748 assert!((p.xd - 1.8).abs() < 1e-9, "xd");
749 assert!((p.xq - 1.7).abs() < 1e-9, "xq");
750 assert!((p.td0_prime - 8.0).abs() < 1e-9, "td0'");
751 assert!((p.xl - 0.2).abs() < 1e-9, "xl");
752 }
753 other => panic!("expected Genrou, got {other:?}"),
754 }
755 }
756
757 #[test]
762 fn test_cgmes_dy_gensal() {
763 let dy_xml = r##"<?xml version="1.0"?>
764<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
765 xmlns:cim="http://iec.ch/TC57/CIM100#">
766 <cim:SynchronousMachineTimeConstantReactance rdf:ID="gen-dyn-002">
767 <cim:SynchronousMachineDynamics.SynchronousMachine rdf:resource="#sm-002"/>
768 <cim:SynchronousMachineTimeConstantReactance.rotorType>salientPole</cim:SynchronousMachineTimeConstantReactance.rotorType>
769 <cim:RotatingMachineDynamics.inertia>4.0</cim:RotatingMachineDynamics.inertia>
770 <cim:RotatingMachineDynamics.damping>2.0</cim:RotatingMachineDynamics.damping>
771 <cim:SynchronousMachineTimeConstantReactance.tpdo>5.9</cim:SynchronousMachineTimeConstantReactance.tpdo>
772 <cim:SynchronousMachineTimeConstantReactance.tppdo>0.033</cim:SynchronousMachineTimeConstantReactance.tppdo>
773 <cim:SynchronousMachineTimeConstantReactance.tppqo>0.078</cim:SynchronousMachineTimeConstantReactance.tppqo>
774 <cim:SynchronousMachineTimeConstantReactance.xDirectSync>1.05</cim:SynchronousMachineTimeConstantReactance.xDirectSync>
775 <cim:SynchronousMachineTimeConstantReactance.xQuadSync>0.66</cim:SynchronousMachineTimeConstantReactance.xQuadSync>
776 <cim:SynchronousMachineTimeConstantReactance.xDirectTrans>0.32</cim:SynchronousMachineTimeConstantReactance.xDirectTrans>
777 <cim:SynchronousMachineTimeConstantReactance.xDirectSubtrans>0.25</cim:SynchronousMachineTimeConstantReactance.xDirectSubtrans>
778 <cim:SynchronousMachineTimeConstantReactance.statorLeakageReactance>0.15</cim:SynchronousMachineTimeConstantReactance.statorLeakageReactance>
779 </cim:SynchronousMachineTimeConstantReactance>
780</rdf:RDF>"##;
781
782 let sm_bus_map = single_sm_map("sm-002", 2, "1");
783 let dm = parse_cgmes_dy(&[dy_xml], &sm_bus_map).unwrap();
784 assert_eq!(dm.generators.len(), 1);
785 match &dm.generators[0].model {
786 GeneratorModel::Gensal(p) => {
787 assert!((p.h - 4.0).abs() < 1e-9, "H");
788 assert!((p.xd - 1.05).abs() < 1e-9, "xd");
789 assert!((p.td0_prime - 5.9).abs() < 1e-9, "td0'");
790 }
791 other => panic!("expected Gensal, got {other:?}"),
792 }
793 }
794
795 #[test]
800 fn test_cgmes_dy_exciter_st1a() {
801 let dy_xml = r##"<?xml version="1.0"?>
802<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
803 xmlns:cim="http://iec.ch/TC57/CIM100#">
804 <cim:SynchronousMachineTimeConstantReactance rdf:ID="smd-003">
805 <cim:SynchronousMachineDynamics.SynchronousMachine rdf:resource="#sm-003"/>
806 <cim:SynchronousMachineTimeConstantReactance.rotorType>roundRotor</cim:SynchronousMachineTimeConstantReactance.rotorType>
807 <cim:RotatingMachineDynamics.inertia>5.0</cim:RotatingMachineDynamics.inertia>
808 <cim:RotatingMachineDynamics.damping>0.0</cim:RotatingMachineDynamics.damping>
809 <cim:SynchronousMachineTimeConstantReactance.tpdo>6.0</cim:SynchronousMachineTimeConstantReactance.tpdo>
810 <cim:SynchronousMachineTimeConstantReactance.tppdo>0.04</cim:SynchronousMachineTimeConstantReactance.tppdo>
811 <cim:SynchronousMachineTimeConstantReactance.tpqo>0.5</cim:SynchronousMachineTimeConstantReactance.tpqo>
812 <cim:SynchronousMachineTimeConstantReactance.tppqo>0.05</cim:SynchronousMachineTimeConstantReactance.tppqo>
813 <cim:SynchronousMachineTimeConstantReactance.xDirectSync>1.79</cim:SynchronousMachineTimeConstantReactance.xDirectSync>
814 <cim:SynchronousMachineTimeConstantReactance.xQuadSync>1.71</cim:SynchronousMachineTimeConstantReactance.xQuadSync>
815 <cim:SynchronousMachineTimeConstantReactance.xDirectTrans>0.169</cim:SynchronousMachineTimeConstantReactance.xDirectTrans>
816 <cim:SynchronousMachineTimeConstantReactance.xDirectSubtrans>0.135</cim:SynchronousMachineTimeConstantReactance.xDirectSubtrans>
817 <cim:SynchronousMachineTimeConstantReactance.statorLeakageReactance>0.13</cim:SynchronousMachineTimeConstantReactance.statorLeakageReactance>
818 </cim:SynchronousMachineTimeConstantReactance>
819 <cim:ExcIEEEST1A rdf:ID="exc-003">
820 <cim:ExcitationSystemDynamics.SynchronousMachineDynamics rdf:resource="#smd-003"/>
821 <cim:ExcIEEEST1A.tc>10.0</cim:ExcIEEEST1A.tc>
822 <cim:ExcIEEEST1A.tb>10.0</cim:ExcIEEEST1A.tb>
823 <cim:ExcIEEEST1A.ka>200.0</cim:ExcIEEEST1A.ka>
824 <cim:ExcIEEEST1A.vrmax>6.43</cim:ExcIEEEST1A.vrmax>
825 <cim:ExcIEEEST1A.vrmin>-6.43</cim:ExcIEEEST1A.vrmin>
826 </cim:ExcIEEEST1A>
827</rdf:RDF>"##;
828
829 let sm_bus_map = single_sm_map("sm-003", 3, "1");
830 let dm = parse_cgmes_dy(&[dy_xml], &sm_bus_map).unwrap();
831 assert_eq!(dm.generators.len(), 1, "generator");
832 assert_eq!(dm.exciters.len(), 1, "exciter");
833 let exc = &dm.exciters[0];
834 assert_eq!(exc.bus, 3);
835 match &exc.model {
836 ExciterModel::Esst1a(p) => {
837 assert!((p.ka - 200.0).abs() < 1e-9, "ka");
838 assert!((p.vrmax - 6.43).abs() < 1e-9, "vrmax");
839 }
840 other => panic!("expected Esst1a, got {other:?}"),
841 }
842 }
843
844 #[test]
849 fn test_cgmes_dy_governor_gast() {
850 let dy_xml = r##"<?xml version="1.0"?>
851<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
852 xmlns:cim="http://iec.ch/TC57/CIM100#">
853 <cim:SynchronousMachineTimeConstantReactance rdf:ID="smd-004">
854 <cim:SynchronousMachineDynamics.SynchronousMachine rdf:resource="#sm-004"/>
855 <cim:SynchronousMachineTimeConstantReactance.rotorType>roundRotor</cim:SynchronousMachineTimeConstantReactance.rotorType>
856 <cim:RotatingMachineDynamics.inertia>7.0</cim:RotatingMachineDynamics.inertia>
857 <cim:RotatingMachineDynamics.damping>0.0</cim:RotatingMachineDynamics.damping>
858 <cim:SynchronousMachineTimeConstantReactance.tpdo>7.0</cim:SynchronousMachineTimeConstantReactance.tpdo>
859 <cim:SynchronousMachineTimeConstantReactance.tppdo>0.04</cim:SynchronousMachineTimeConstantReactance.tppdo>
860 <cim:SynchronousMachineTimeConstantReactance.tpqo>0.5</cim:SynchronousMachineTimeConstantReactance.tpqo>
861 <cim:SynchronousMachineTimeConstantReactance.tppqo>0.05</cim:SynchronousMachineTimeConstantReactance.tppqo>
862 <cim:SynchronousMachineTimeConstantReactance.xDirectSync>1.8</cim:SynchronousMachineTimeConstantReactance.xDirectSync>
863 <cim:SynchronousMachineTimeConstantReactance.xQuadSync>1.7</cim:SynchronousMachineTimeConstantReactance.xQuadSync>
864 <cim:SynchronousMachineTimeConstantReactance.xDirectTrans>0.3</cim:SynchronousMachineTimeConstantReactance.xDirectTrans>
865 <cim:SynchronousMachineTimeConstantReactance.xDirectSubtrans>0.25</cim:SynchronousMachineTimeConstantReactance.xDirectSubtrans>
866 <cim:SynchronousMachineTimeConstantReactance.statorLeakageReactance>0.2</cim:SynchronousMachineTimeConstantReactance.statorLeakageReactance>
867 </cim:SynchronousMachineTimeConstantReactance>
868 <cim:GovGAST rdf:ID="gov-004">
869 <cim:TurbineGovernorDynamics.SynchronousMachineDynamics rdf:resource="#smd-004"/>
870 <cim:GovGAST.r>0.05</cim:GovGAST.r>
871 <cim:GovGAST.t1>0.5</cim:GovGAST.t1>
872 <cim:GovGAST.t2>3.0</cim:GovGAST.t2>
873 <cim:GovGAST.t3>10.0</cim:GovGAST.t3>
874 <cim:GovGAST.at>1.0</cim:GovGAST.at>
875 <cim:GovGAST.kt>2.0</cim:GovGAST.kt>
876 <cim:GovGAST.voltage_min_pu>0.0</cim:GovGAST.voltage_min_pu>
877 <cim:GovGAST.voltage_max_pu>1.0</cim:GovGAST.voltage_max_pu>
878 </cim:GovGAST>
879</rdf:RDF>"##;
880
881 let sm_bus_map = single_sm_map("sm-004", 4, "G4");
882 let dm = parse_cgmes_dy(&[dy_xml], &sm_bus_map).unwrap();
883 assert_eq!(dm.governors.len(), 1, "governor");
884 let gov = &dm.governors[0];
885 assert_eq!(gov.bus, 4);
886 assert_eq!(gov.machine_id, "G4");
887 match &gov.model {
888 GovernorModel::Gast(p) => {
889 assert!((p.r - 0.05).abs() < 1e-9, "r");
890 assert!((p.t1 - 0.5).abs() < 1e-9, "t1");
891 assert!((p.t3 - 10.0).abs() < 1e-9, "t3");
892 }
893 other => panic!("expected Gast, got {other:?}"),
894 }
895 }
896
897 #[test]
902 fn test_cgmes_dy_pss_ieee2b() {
903 let dy_xml = r##"<?xml version="1.0"?>
904<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
905 xmlns:cim="http://iec.ch/TC57/CIM100#">
906 <cim:SynchronousMachineTimeConstantReactance rdf:ID="smd-005">
907 <cim:SynchronousMachineDynamics.SynchronousMachine rdf:resource="#sm-005"/>
908 <cim:SynchronousMachineTimeConstantReactance.rotorType>roundRotor</cim:SynchronousMachineTimeConstantReactance.rotorType>
909 <cim:RotatingMachineDynamics.inertia>3.0</cim:RotatingMachineDynamics.inertia>
910 <cim:RotatingMachineDynamics.damping>0.0</cim:RotatingMachineDynamics.damping>
911 <cim:SynchronousMachineTimeConstantReactance.tpdo>6.0</cim:SynchronousMachineTimeConstantReactance.tpdo>
912 <cim:SynchronousMachineTimeConstantReactance.tppdo>0.04</cim:SynchronousMachineTimeConstantReactance.tppdo>
913 <cim:SynchronousMachineTimeConstantReactance.tpqo>0.5</cim:SynchronousMachineTimeConstantReactance.tpqo>
914 <cim:SynchronousMachineTimeConstantReactance.tppqo>0.05</cim:SynchronousMachineTimeConstantReactance.tppqo>
915 <cim:SynchronousMachineTimeConstantReactance.xDirectSync>1.8</cim:SynchronousMachineTimeConstantReactance.xDirectSync>
916 <cim:SynchronousMachineTimeConstantReactance.xQuadSync>1.7</cim:SynchronousMachineTimeConstantReactance.xQuadSync>
917 <cim:SynchronousMachineTimeConstantReactance.xDirectTrans>0.3</cim:SynchronousMachineTimeConstantReactance.xDirectTrans>
918 <cim:SynchronousMachineTimeConstantReactance.xDirectSubtrans>0.25</cim:SynchronousMachineTimeConstantReactance.xDirectSubtrans>
919 <cim:SynchronousMachineTimeConstantReactance.statorLeakageReactance>0.2</cim:SynchronousMachineTimeConstantReactance.statorLeakageReactance>
920 </cim:SynchronousMachineTimeConstantReactance>
921 <cim:PssIEEE2B rdf:ID="pss-005">
922 <cim:PowerSystemStabilizerDynamics.ExcitationSystemDynamics rdf:resource="#smd-005"/>
923 <cim:PssIEEE2B.tw1>10.0</cim:PssIEEE2B.tw1>
924 <cim:PssIEEE2B.tw2>10.0</cim:PssIEEE2B.tw2>
925 <cim:PssIEEE2B.tw3>2.0</cim:PssIEEE2B.tw3>
926 <cim:PssIEEE2B.t1>0.12</cim:PssIEEE2B.t1>
927 <cim:PssIEEE2B.t2>0.02</cim:PssIEEE2B.t2>
928 <cim:PssIEEE2B.t3>0.3</cim:PssIEEE2B.t3>
929 <cim:PssIEEE2B.t4>0.15</cim:PssIEEE2B.t4>
930 <cim:PssIEEE2B.t7>2.0</cim:PssIEEE2B.t7>
931 <cim:PssIEEE2B.t8>0.5</cim:PssIEEE2B.t8>
932 <cim:PssIEEE2B.t9>0.1</cim:PssIEEE2B.t9>
933 <cim:PssIEEE2B.ks1>12.0</cim:PssIEEE2B.ks1>
934 <cim:PssIEEE2B.vstmax>0.1</cim:PssIEEE2B.vstmax>
935 <cim:PssIEEE2B.vstmin>-0.1</cim:PssIEEE2B.vstmin>
936 </cim:PssIEEE2B>
937</rdf:RDF>"##;
938
939 let sm_bus_map = single_sm_map("sm-005", 5, "1");
940 let dm = parse_cgmes_dy(&[dy_xml], &sm_bus_map).unwrap();
941 assert_eq!(
944 dm.generators.len(),
945 1,
946 "generator from SynchronousMachineTimeConstantReactance"
947 );
948 assert_eq!(
949 dm.pss.len(),
950 1,
951 "PSS resolved via ExcitationSystemDynamics 3-hop"
952 );
953 assert_eq!(dm.pss[0].bus, 5);
954 assert_eq!(dm.pss[0].machine_id, "1");
955 }
956
957 #[test]
962 fn test_cgmes_dy_unsupported_warns() {
963 let dy_xml = r##"<?xml version="1.0"?>
964<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
965 xmlns:cim="http://iec.ch/TC57/CIM100#">
966 <cim:SomeFutureModel2035 rdf:ID="future-001">
967 <cim:SomeFutureModel2035.SynchronousMachine rdf:resource="#sm-006"/>
968 <cim:SomeFutureModel2035.param1>42.0</cim:SomeFutureModel2035.param1>
969 </cim:SomeFutureModel2035>
970</rdf:RDF>"##;
971
972 let sm_bus_map = single_sm_map("sm-006", 6, "1");
973 let dm = parse_cgmes_dy(&[dy_xml], &sm_bus_map).unwrap();
975 assert_eq!(dm.generators.len(), 0);
976 assert_eq!(dm.exciters.len(), 0);
977 assert_eq!(dm.governors.len(), 0);
978 }
979
980 #[test]
985 fn test_cgmes_dy_full_machine() {
986 let dy_xml = r##"<?xml version="1.0"?>
987<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
988 xmlns:cim="http://iec.ch/TC57/CIM100#">
989 <!-- Generator dynamics -->
990 <cim:SynchronousMachineTimeConstantReactance rdf:ID="smd-007">
991 <cim:SynchronousMachineDynamics.SynchronousMachine rdf:resource="#sm-007"/>
992 <cim:SynchronousMachineTimeConstantReactance.rotorType>roundRotor</cim:SynchronousMachineTimeConstantReactance.rotorType>
993 <cim:RotatingMachineDynamics.inertia>6.0</cim:RotatingMachineDynamics.inertia>
994 <cim:RotatingMachineDynamics.damping>0.0</cim:RotatingMachineDynamics.damping>
995 <cim:SynchronousMachineTimeConstantReactance.tpdo>8.0</cim:SynchronousMachineTimeConstantReactance.tpdo>
996 <cim:SynchronousMachineTimeConstantReactance.tppdo>0.03</cim:SynchronousMachineTimeConstantReactance.tppdo>
997 <cim:SynchronousMachineTimeConstantReactance.tpqo>0.4</cim:SynchronousMachineTimeConstantReactance.tpqo>
998 <cim:SynchronousMachineTimeConstantReactance.tppqo>0.05</cim:SynchronousMachineTimeConstantReactance.tppqo>
999 <cim:SynchronousMachineTimeConstantReactance.xDirectSync>1.8</cim:SynchronousMachineTimeConstantReactance.xDirectSync>
1000 <cim:SynchronousMachineTimeConstantReactance.xQuadSync>1.7</cim:SynchronousMachineTimeConstantReactance.xQuadSync>
1001 <cim:SynchronousMachineTimeConstantReactance.xDirectTrans>0.3</cim:SynchronousMachineTimeConstantReactance.xDirectTrans>
1002 <cim:SynchronousMachineTimeConstantReactance.xDirectSubtrans>0.25</cim:SynchronousMachineTimeConstantReactance.xDirectSubtrans>
1003 <cim:SynchronousMachineTimeConstantReactance.statorLeakageReactance>0.2</cim:SynchronousMachineTimeConstantReactance.statorLeakageReactance>
1004 </cim:SynchronousMachineTimeConstantReactance>
1005 <!-- Exciter directly referencing the SM dynamics via SynchronousMachineDynamics -->
1006 <cim:ExcIEEEST1A rdf:ID="exc-007">
1007 <cim:ExcitationSystemDynamics.SynchronousMachineDynamics rdf:resource="#smd-007"/>
1008 <cim:ExcIEEEST1A.tc>10.0</cim:ExcIEEEST1A.tc>
1009 <cim:ExcIEEEST1A.tb>10.0</cim:ExcIEEEST1A.tb>
1010 <cim:ExcIEEEST1A.ka>200.0</cim:ExcIEEEST1A.ka>
1011 <cim:ExcIEEEST1A.vrmax>6.43</cim:ExcIEEEST1A.vrmax>
1012 <cim:ExcIEEEST1A.vrmin>-6.43</cim:ExcIEEEST1A.vrmin>
1013 </cim:ExcIEEEST1A>
1014 <!-- Governor directly referencing the SM dynamics -->
1015 <cim:GovGAST rdf:ID="gov-007">
1016 <cim:TurbineGovernorDynamics.SynchronousMachineDynamics rdf:resource="#smd-007"/>
1017 <cim:GovGAST.r>0.05</cim:GovGAST.r>
1018 <cim:GovGAST.t1>0.5</cim:GovGAST.t1>
1019 <cim:GovGAST.t2>3.0</cim:GovGAST.t2>
1020 <cim:GovGAST.t3>10.0</cim:GovGAST.t3>
1021 </cim:GovGAST>
1022</rdf:RDF>"##;
1023
1024 let sm_bus_map = single_sm_map("sm-007", 7, "G7");
1025 let dm = parse_cgmes_dy(&[dy_xml], &sm_bus_map).unwrap();
1026 assert_eq!(dm.generators.len(), 1, "generator");
1027 assert_eq!(dm.exciters.len(), 1, "exciter");
1028 assert_eq!(dm.governors.len(), 1, "governor");
1029
1030 assert_eq!(dm.generators[0].bus, 7);
1032 assert_eq!(dm.generators[0].machine_id, "G7");
1033 assert_eq!(dm.exciters[0].bus, 7);
1034 assert_eq!(dm.exciters[0].machine_id, "G7");
1035 assert_eq!(dm.governors[0].bus, 7);
1036 assert_eq!(dm.governors[0].machine_id, "G7");
1037 }
1038
1039 #[test]
1044 fn test_cgmes_dy_gencls() {
1045 let dy_xml = r##"<?xml version="1.0"?>
1046<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
1047 xmlns:cim="http://iec.ch/TC57/CIM100#">
1048 <cim:SynchronousMachineSimplified rdf:ID="sms-008">
1049 <cim:SynchronousMachineDynamics.SynchronousMachine rdf:resource="#sm-008"/>
1050 <cim:RotatingMachineDynamics.inertia>3.0</cim:RotatingMachineDynamics.inertia>
1051 <cim:RotatingMachineDynamics.damping>1.0</cim:RotatingMachineDynamics.damping>
1052 </cim:SynchronousMachineSimplified>
1053</rdf:RDF>"##;
1054
1055 let sm_bus_map = single_sm_map("sm-008", 8, "CLK");
1056 let dm = parse_cgmes_dy(&[dy_xml], &sm_bus_map).unwrap();
1057 assert_eq!(dm.generators.len(), 1);
1058 match &dm.generators[0].model {
1059 GeneratorModel::Gencls(p) => {
1060 assert!((p.h - 3.0).abs() < 1e-9);
1061 assert!((p.d - 1.0).abs() < 1e-9);
1062 }
1063 other => panic!("expected Gencls, got {other:?}"),
1064 }
1065 }
1066}