cimoxide 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cimoxide-0.1.0/Cargo.toml +4 -0
- cimoxide-0.1.0/PKG-INFO +7 -0
- cimoxide-0.1.0/cimdecoder/Cargo.toml +15 -0
- cimoxide-0.1.0/cimdecoder/benches/real_grid.rs +100 -0
- cimoxide-0.1.0/cimdecoder/src/lib.rs +280 -0
- cimoxide-0.1.0/cimdecoder/tests/real_grid.rs +24 -0
- cimoxide-0.1.0/cimdecoder/tests/smoke.rs +38 -0
- cimoxide-0.1.0/cimoxide-py/Cargo.lock +336 -0
- cimoxide-0.1.0/cimoxide-py/Cargo.toml +15 -0
- cimoxide-0.1.0/cimoxide-py/src/lib.rs +309 -0
- cimoxide-0.1.0/cimoxide-py/tests/test_api.py +67 -0
- cimoxide-0.1.0/cimoxide-py/tests/test_decode.py +87 -0
- cimoxide-0.1.0/cimoxide-py/tests/test_validate.py +76 -0
- cimoxide-0.1.0/cimstructs/Cargo.toml +8 -0
- cimoxide-0.1.0/cimvalidation/Cargo.toml +9 -0
- cimoxide-0.1.0/cimvalidation/src/detect.rs +68 -0
- cimoxide-0.1.0/cimvalidation/src/helpers.rs +29 -0
- cimoxide-0.1.0/cimvalidation/src/lib.rs +9 -0
- cimoxide-0.1.0/cimvalidation/src/sparql/common.rs +292 -0
- cimoxide-0.1.0/cimvalidation/src/sparql/common_solved_mas.rs +689 -0
- cimoxide-0.1.0/cimvalidation/src/sparql/diagram_layout.rs +47 -0
- cimoxide-0.1.0/cimvalidation/src/sparql/dynamics.rs +526 -0
- cimoxide-0.1.0/cimvalidation/src/sparql/equipment.rs +2068 -0
- cimoxide-0.1.0/cimvalidation/src/sparql/equipment_boundary.rs +65 -0
- cimoxide-0.1.0/cimvalidation/src/sparql/equipment_not_solved_mas.rs +246 -0
- cimoxide-0.1.0/cimvalidation/src/sparql/mod.rs +120 -0
- cimoxide-0.1.0/cimvalidation/src/sparql/operation.rs +103 -0
- cimoxide-0.1.0/cimvalidation/src/sparql/prof10.rs +187 -0
- cimoxide-0.1.0/cimvalidation/src/sparql/quality.rs +522 -0
- cimoxide-0.1.0/cimvalidation/src/sparql/shortcircuit.rs +135 -0
- cimoxide-0.1.0/cimvalidation/src/sparql/shortcircuit_not_solved_mas.rs +53 -0
- cimoxide-0.1.0/cimvalidation/src/sparql/ssh.rs +466 -0
- cimoxide-0.1.0/cimvalidation/src/sparql/ssh_not_solved_mas.rs +422 -0
- cimoxide-0.1.0/cimvalidation/src/sparql/state_variables.rs +72 -0
- cimoxide-0.1.0/cimvalidation/src/sparql/state_variables_solved_mas.rs +431 -0
- cimoxide-0.1.0/cimvalidation/src/sparql/topology_not_solved_mas.rs +191 -0
- cimoxide-0.1.0/cimvalidation/src/violation.rs +11 -0
- cimoxide-0.1.0/cimvalidation/tests/common/mod.rs +18 -0
- cimoxide-0.1.0/cimvalidation/tests/shacl.rs +153 -0
- cimoxide-0.1.0/cimvalidation/tests/sparql.rs +408 -0
- cimoxide-0.1.0/pyproject.toml +18 -0
- cimoxide-0.1.0/python/cimoxide/__init__.py +18 -0
- cimoxide-0.1.0/python/cimoxide/__init__.pyi +139 -0
cimoxide-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "cimdecoder"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2024"
|
|
5
|
+
|
|
6
|
+
[dependencies]
|
|
7
|
+
cimstructs = { path = "../cimstructs" }
|
|
8
|
+
quick-xml = { version = "0.37", features = ["encoding"] }
|
|
9
|
+
|
|
10
|
+
[dev-dependencies]
|
|
11
|
+
criterion = { version = "0.5", features = ["html_reports"] }
|
|
12
|
+
|
|
13
|
+
[[bench]]
|
|
14
|
+
name = "real_grid"
|
|
15
|
+
harness = false
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
//! Benchmark methodology matches the Go BenchmarkRealGridLoad* benchmarks in
|
|
2
|
+
//! cimgo/validation/cgmes_config_test.go: files are pre-loaded into memory
|
|
3
|
+
//! before the timed loop so only parse/decode time is measured, not I/O.
|
|
4
|
+
|
|
5
|
+
use std::path::Path;
|
|
6
|
+
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
|
7
|
+
use cimdecoder::CimDataset;
|
|
8
|
+
|
|
9
|
+
const BASE: &str = "../CGMES-Test-Configurations/v3.0/RealGrid/RealGrid-Merged";
|
|
10
|
+
|
|
11
|
+
fn load_files() -> Vec<(String, String)> {
|
|
12
|
+
["RealGrid_EQ.xml", "RealGrid_SSH.xml", "RealGrid_TP.xml", "RealGrid_SV.xml"]
|
|
13
|
+
.iter()
|
|
14
|
+
.map(|name| {
|
|
15
|
+
let content = std::fs::read_to_string(Path::new(BASE).join(name))
|
|
16
|
+
.unwrap_or_else(|e| panic!("cannot read {name}: {e}"));
|
|
17
|
+
(name.to_string(), content)
|
|
18
|
+
})
|
|
19
|
+
.collect()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Decode each file independently (sequential, like BenchmarkRealGridLoadSequential).
|
|
23
|
+
fn bench_sequential(c: &mut Criterion) {
|
|
24
|
+
let blobs = load_files();
|
|
25
|
+
let total_bytes: u64 = blobs.iter().map(|(_, s)| s.len() as u64).sum();
|
|
26
|
+
|
|
27
|
+
let mut group = c.benchmark_group("decode_sequential");
|
|
28
|
+
group.throughput(Throughput::Bytes(total_bytes));
|
|
29
|
+
|
|
30
|
+
group.bench_function("all_four_profiles", |b| {
|
|
31
|
+
b.iter(|| {
|
|
32
|
+
let mut ds = CimDataset::new();
|
|
33
|
+
for (_, content) in &blobs {
|
|
34
|
+
ds.merge(CimDataset::decode_str(content).unwrap());
|
|
35
|
+
}
|
|
36
|
+
ds
|
|
37
|
+
})
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
group.finish();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Decode all four files in parallel using one thread per file, then merge sequentially.
|
|
44
|
+
/// Uses the same pre-loaded in-memory blobs as bench_sequential for a fair comparison.
|
|
45
|
+
fn bench_parallel(c: &mut Criterion) {
|
|
46
|
+
let blobs = load_files();
|
|
47
|
+
let total_bytes: u64 = blobs.iter().map(|(_, s)| s.len() as u64).sum();
|
|
48
|
+
|
|
49
|
+
let mut group = c.benchmark_group("decode_parallel");
|
|
50
|
+
group.throughput(Throughput::Bytes(total_bytes));
|
|
51
|
+
|
|
52
|
+
group.bench_function("all_four_profiles", |b| {
|
|
53
|
+
b.iter(|| {
|
|
54
|
+
let datasets: Vec<CimDataset> = std::thread::scope(|s| {
|
|
55
|
+
blobs
|
|
56
|
+
.iter()
|
|
57
|
+
.map(|(_, content)| s.spawn(|| CimDataset::decode_str(content).unwrap()))
|
|
58
|
+
.collect::<Vec<_>>()
|
|
59
|
+
.into_iter()
|
|
60
|
+
.map(|h| h.join().expect("decode thread panicked"))
|
|
61
|
+
.collect()
|
|
62
|
+
});
|
|
63
|
+
datasets
|
|
64
|
+
.into_iter()
|
|
65
|
+
.reduce(|mut a, b| {
|
|
66
|
+
a.merge(b);
|
|
67
|
+
a
|
|
68
|
+
})
|
|
69
|
+
.unwrap()
|
|
70
|
+
})
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
group.finish();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Decode individual profiles to show per-file throughput.
|
|
77
|
+
fn bench_per_profile(c: &mut Criterion) {
|
|
78
|
+
let blobs = load_files();
|
|
79
|
+
let mut group = c.benchmark_group("decode_single");
|
|
80
|
+
|
|
81
|
+
for (name, content) in &blobs {
|
|
82
|
+
let label = name.trim_end_matches(".xml").trim_start_matches("RealGrid_");
|
|
83
|
+
let bytes = content.len() as u64;
|
|
84
|
+
group.throughput(Throughput::Bytes(bytes));
|
|
85
|
+
group.bench_with_input(
|
|
86
|
+
BenchmarkId::new("profile", label),
|
|
87
|
+
content,
|
|
88
|
+
|b, c| b.iter(|| CimDataset::decode_str(c).unwrap()),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
group.finish();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
criterion_group! {
|
|
96
|
+
name = benches;
|
|
97
|
+
config = Criterion::default().sample_size(10);
|
|
98
|
+
targets = bench_sequential, bench_parallel, bench_per_profile
|
|
99
|
+
}
|
|
100
|
+
criterion_main!(benches);
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
use std::collections::HashMap;
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
|
|
4
|
+
use quick_xml::events::Event;
|
|
5
|
+
use quick_xml::Reader;
|
|
6
|
+
|
|
7
|
+
use cimstructs::base::{CimElement, FieldValue, RdfBlock};
|
|
8
|
+
use cimstructs::registry::{self, ParseFn};
|
|
9
|
+
|
|
10
|
+
pub struct CimEntry {
|
|
11
|
+
pub element: Box<dyn CimElement>,
|
|
12
|
+
pub block: RdfBlock,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
pub struct CimDataset {
|
|
16
|
+
pub entries: HashMap<String, CimEntry>,
|
|
17
|
+
/// Maps `type_name()` → list of MRIDs of that type. Populated on insert, maintained on merge.
|
|
18
|
+
pub by_type: HashMap<String, Vec<String>>,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
impl Default for CimDataset {
|
|
22
|
+
fn default() -> Self {
|
|
23
|
+
Self::new()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
impl CimDataset {
|
|
28
|
+
pub fn new() -> Self {
|
|
29
|
+
Self {
|
|
30
|
+
entries: HashMap::new(),
|
|
31
|
+
by_type: HashMap::new(),
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
pub fn decode_str(content: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
|
36
|
+
let mut ds = Self::new();
|
|
37
|
+
let reg = registry::registry();
|
|
38
|
+
parse_rdf(content, ®, &mut ds)?;
|
|
39
|
+
Ok(ds)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pub fn decode_file(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
|
|
43
|
+
Self::decode_str(&std::fs::read_to_string(path)?)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
pub fn decode_files(paths: &[&Path]) -> Result<Self, Box<dyn std::error::Error>> {
|
|
47
|
+
let mut combined = Self::new();
|
|
48
|
+
for path in paths {
|
|
49
|
+
let file_ds = Self::decode_file(path)?;
|
|
50
|
+
combined.merge(file_ds);
|
|
51
|
+
}
|
|
52
|
+
Ok(combined)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Decode files in parallel using one thread per file, then merge sequentially.
|
|
56
|
+
/// Falls back to `decode_files` for 0–1 paths to avoid thread-spawn overhead.
|
|
57
|
+
pub fn decode_files_parallel(paths: &[&Path]) -> Result<Self, Box<dyn std::error::Error>> {
|
|
58
|
+
if paths.len() <= 1 {
|
|
59
|
+
return Self::decode_files(paths);
|
|
60
|
+
}
|
|
61
|
+
let results: Vec<Result<Self, String>> = std::thread::scope(|s| {
|
|
62
|
+
paths
|
|
63
|
+
.iter()
|
|
64
|
+
.map(|p| s.spawn(|| Self::decode_file(p).map_err(|e| e.to_string())))
|
|
65
|
+
.collect::<Vec<_>>()
|
|
66
|
+
.into_iter()
|
|
67
|
+
.map(|h| h.join().expect("decode thread panicked"))
|
|
68
|
+
.collect()
|
|
69
|
+
});
|
|
70
|
+
let datasets: Vec<Self> = results
|
|
71
|
+
.into_iter()
|
|
72
|
+
.collect::<Result<_, String>>()
|
|
73
|
+
.map_err(|e| -> Box<dyn std::error::Error> { e.into() })?;
|
|
74
|
+
Ok(datasets
|
|
75
|
+
.into_iter()
|
|
76
|
+
.reduce(|mut a, b| {
|
|
77
|
+
a.merge(b);
|
|
78
|
+
a
|
|
79
|
+
})
|
|
80
|
+
.unwrap_or_default())
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Merge another dataset into self, combining objects with the same MRID.
|
|
84
|
+
/// For conflicting MRIDs: merge RdfBlocks (later scalar wins, lists union),
|
|
85
|
+
/// then re-instantiate the typed element.
|
|
86
|
+
pub fn merge(&mut self, other: CimDataset) {
|
|
87
|
+
let reg = registry::registry();
|
|
88
|
+
for (mrid, incoming) in other.entries {
|
|
89
|
+
if let Some(existing) = self.entries.get_mut(&mrid) {
|
|
90
|
+
existing.block.merge_from(&incoming.block);
|
|
91
|
+
let type_name = existing.block.type_name.clone();
|
|
92
|
+
if let Some(f) = reg.get(type_name.as_str()) {
|
|
93
|
+
existing.element = f(&existing.block);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
let type_name = incoming.element.type_name().to_string();
|
|
97
|
+
self.by_type.entry(type_name).or_default().push(mrid.clone());
|
|
98
|
+
self.entries.insert(mrid, incoming);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Release all RdfBlocks to free memory after the final merge.
|
|
104
|
+
pub fn drop_blocks(&mut self) {
|
|
105
|
+
for entry in self.entries.values_mut() {
|
|
106
|
+
entry.block = RdfBlock::default();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --- XML streaming parser ---------------------------------------------------
|
|
112
|
+
|
|
113
|
+
fn parse_rdf(
|
|
114
|
+
content: &str,
|
|
115
|
+
reg: &HashMap<&'static str, ParseFn>,
|
|
116
|
+
ds: &mut CimDataset,
|
|
117
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
118
|
+
let mut reader = Reader::from_str(content);
|
|
119
|
+
let mut buf = Vec::new();
|
|
120
|
+
|
|
121
|
+
let mut depth: u32 = 0;
|
|
122
|
+
let mut current: Option<RdfBlock> = None;
|
|
123
|
+
let mut pending_key: Option<String> = None;
|
|
124
|
+
|
|
125
|
+
loop {
|
|
126
|
+
match reader.read_event_into(&mut buf) {
|
|
127
|
+
Ok(Event::Start(ref e)) => {
|
|
128
|
+
depth += 1;
|
|
129
|
+
let local = local_name(e.name().as_ref())?;
|
|
130
|
+
|
|
131
|
+
match depth {
|
|
132
|
+
2 => {
|
|
133
|
+
let mrid = extract_about(e.attributes())?;
|
|
134
|
+
current = Some(RdfBlock {
|
|
135
|
+
type_name: local,
|
|
136
|
+
mrid,
|
|
137
|
+
fields: HashMap::new(),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
3 => {
|
|
141
|
+
if let Some(ref mut block) = current {
|
|
142
|
+
if let Some(res) = find_resource(e.attributes())? {
|
|
143
|
+
add_field(block, &local, FieldValue::Resource(res));
|
|
144
|
+
} else {
|
|
145
|
+
pending_key = Some(local);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
_ => {}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
Ok(Event::Empty(ref e)) => {
|
|
154
|
+
let local = local_name(e.name().as_ref())?;
|
|
155
|
+
match depth {
|
|
156
|
+
1 => {
|
|
157
|
+
// Top-level self-closing element: <cim:Foo rdf:ID="x" />
|
|
158
|
+
let mrid = extract_about(e.attributes())?;
|
|
159
|
+
if !mrid.is_empty() {
|
|
160
|
+
if let Some(f) = reg.get(local.as_str()) {
|
|
161
|
+
let block = RdfBlock { type_name: local, mrid: mrid.clone(), fields: HashMap::new() };
|
|
162
|
+
let element = f(&block);
|
|
163
|
+
let type_name = element.type_name().to_string();
|
|
164
|
+
ds.by_type.entry(type_name).or_default().push(mrid.clone());
|
|
165
|
+
ds.entries.insert(mrid, CimEntry { element, block });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
2 => {
|
|
170
|
+
// Self-closing field element within the current type block.
|
|
171
|
+
if let Some(ref mut block) = current {
|
|
172
|
+
if let Some(res) = find_resource(e.attributes())? {
|
|
173
|
+
add_field(block, &local, FieldValue::Resource(res));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
_ => {}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
Ok(Event::Text(ref e)) => {
|
|
182
|
+
if depth == 3 {
|
|
183
|
+
if let (Some(block), Some(key)) =
|
|
184
|
+
(&mut current, pending_key.take())
|
|
185
|
+
{
|
|
186
|
+
let text = e.unescape()?.trim().to_string();
|
|
187
|
+
if !text.is_empty() {
|
|
188
|
+
add_field(block, &key, FieldValue::Text(text));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
Ok(Event::End(_)) => {
|
|
195
|
+
if depth == 2 {
|
|
196
|
+
pending_key = None;
|
|
197
|
+
if let Some(block) = current.take() {
|
|
198
|
+
if !block.mrid.is_empty() {
|
|
199
|
+
if let Some(f) = reg.get(block.type_name.as_str()) {
|
|
200
|
+
let element = f(&block);
|
|
201
|
+
let type_name = element.type_name().to_string();
|
|
202
|
+
ds.by_type.entry(type_name).or_default().push(block.mrid.clone());
|
|
203
|
+
ds.entries.insert(block.mrid.clone(), CimEntry { element, block });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
depth = depth.saturating_sub(1);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
Ok(Event::Eof) => break,
|
|
212
|
+
Err(e) => return Err(Box::new(e)),
|
|
213
|
+
_ => {}
|
|
214
|
+
}
|
|
215
|
+
buf.clear();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
Ok(())
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// --- helpers ----------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
fn local_name(raw: &[u8]) -> Result<String, Box<dyn std::error::Error>> {
|
|
224
|
+
let s = std::str::from_utf8(raw)?;
|
|
225
|
+
Ok(s.find(':').map(|i| &s[i + 1..]).unwrap_or(s).to_string())
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fn strip_fragment(s: &str) -> String {
|
|
229
|
+
if let Some(i) = s.rfind('#') {
|
|
230
|
+
s[i + 1..].to_string()
|
|
231
|
+
} else {
|
|
232
|
+
s.trim_start_matches('#').to_string()
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
fn extract_about(
|
|
237
|
+
attrs: quick_xml::events::attributes::Attributes<'_>,
|
|
238
|
+
) -> Result<String, Box<dyn std::error::Error>> {
|
|
239
|
+
for attr in attrs.flatten() {
|
|
240
|
+
let key = std::str::from_utf8(attr.key.as_ref())?;
|
|
241
|
+
if key == "rdf:about" || key == "rdf:ID" {
|
|
242
|
+
return Ok(strip_fragment(std::str::from_utf8(&attr.value)?));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
Ok(String::new())
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
fn find_resource(
|
|
249
|
+
attrs: quick_xml::events::attributes::Attributes<'_>,
|
|
250
|
+
) -> Result<Option<String>, Box<dyn std::error::Error>> {
|
|
251
|
+
for attr in attrs.flatten() {
|
|
252
|
+
let key = std::str::from_utf8(attr.key.as_ref())?;
|
|
253
|
+
if key == "rdf:resource" {
|
|
254
|
+
return Ok(Some(strip_fragment(std::str::from_utf8(&attr.value)?)));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
Ok(None)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/// Insert a field value, upgrading Resource → ResourceList on repeated keys.
|
|
261
|
+
fn add_field(block: &mut RdfBlock, key: &str, val: FieldValue) {
|
|
262
|
+
if let FieldValue::Resource(ref new_ref) = val {
|
|
263
|
+
match block.fields.get_mut(key) {
|
|
264
|
+
Some(FieldValue::ResourceList(list)) => {
|
|
265
|
+
list.push(new_ref.clone());
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
Some(existing @ FieldValue::Resource(_)) => {
|
|
269
|
+
let old = match existing {
|
|
270
|
+
FieldValue::Resource(s) => s.clone(),
|
|
271
|
+
_ => unreachable!(),
|
|
272
|
+
};
|
|
273
|
+
*existing = FieldValue::ResourceList(vec![old, new_ref.clone()]);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
_ => {}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
block.fields.insert(key.to_string(), val);
|
|
280
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
use cimdecoder::CimDataset;
|
|
3
|
+
|
|
4
|
+
const BASE: &str = "../CGMES-Test-Configurations/v3.0/RealGrid/RealGrid-Merged";
|
|
5
|
+
|
|
6
|
+
#[test]
|
|
7
|
+
fn decode_eq_counts() {
|
|
8
|
+
let ds = CimDataset::decode_file(Path::new(&format!("{BASE}/RealGrid_EQ.xml")))
|
|
9
|
+
.expect("decode EQ failed");
|
|
10
|
+
eprintln!("EQ: {} objects", ds.entries.len());
|
|
11
|
+
assert!(ds.entries.len() > 10_000, "expected many EQ objects");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#[test]
|
|
15
|
+
fn decode_and_merge_all() {
|
|
16
|
+
let files: Vec<std::path::PathBuf> = ["RealGrid_EQ.xml", "RealGrid_SSH.xml", "RealGrid_TP.xml", "RealGrid_SV.xml"]
|
|
17
|
+
.iter()
|
|
18
|
+
.map(|f| Path::new(BASE).join(f))
|
|
19
|
+
.collect();
|
|
20
|
+
let paths: Vec<&Path> = files.iter().map(|p| p.as_path()).collect();
|
|
21
|
+
let ds = CimDataset::decode_files(&paths).expect("decode_files failed");
|
|
22
|
+
eprintln!("Merged: {} objects", ds.entries.len());
|
|
23
|
+
assert!(ds.entries.len() > 10_000);
|
|
24
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
use cimdecoder::CimDataset;
|
|
3
|
+
|
|
4
|
+
#[test]
|
|
5
|
+
fn decode_small_file() {
|
|
6
|
+
let path = Path::new("../testdata/test_003.xml");
|
|
7
|
+
let ds = CimDataset::decode_file(path).expect("decode failed");
|
|
8
|
+
assert!(!ds.entries.is_empty(), "expected at least one object");
|
|
9
|
+
|
|
10
|
+
let node = ds.entries.get("N0").expect("TopologicalNode N0 not found");
|
|
11
|
+
assert_eq!(node.element.type_name(), "TopologicalNode");
|
|
12
|
+
assert_eq!(node.element.mrid(), "N0");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#[test]
|
|
16
|
+
fn decode_eq_file_fields() {
|
|
17
|
+
let path = Path::new("../testdata/test_sparql_EQ_001.xml");
|
|
18
|
+
let ds = CimDataset::decode_file(path).expect("decode failed");
|
|
19
|
+
|
|
20
|
+
// BaseVoltage.nominalVoltage (f64 field) should be decoded
|
|
21
|
+
let bv = ds.entries.get("BV.110").expect("BaseVoltage BV.110 not found");
|
|
22
|
+
assert_eq!(bv.element.type_name(), "BaseVoltage");
|
|
23
|
+
// Check raw block has the field
|
|
24
|
+
assert!(bv.block.fields.contains_key("BaseVoltage.nominalVoltage"), "missing nominalVoltage field");
|
|
25
|
+
|
|
26
|
+
// VoltageLevel → BaseVoltage (MridRef) should be decoded
|
|
27
|
+
let vl = ds.entries.get("VL.110").expect("VoltageLevel VL.110 not found");
|
|
28
|
+
assert_eq!(vl.element.type_name(), "VoltageLevel");
|
|
29
|
+
assert!(vl.block.fields.contains_key("VoltageLevel.BaseVoltage"), "missing BaseVoltage ref");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[test]
|
|
33
|
+
fn decode_merge_two_files() {
|
|
34
|
+
let eq = Path::new("../testdata/test_003.xml");
|
|
35
|
+
let tp = Path::new("../testdata/test_004.xml");
|
|
36
|
+
let ds = CimDataset::decode_files(&[eq, tp]).expect("decode_files failed");
|
|
37
|
+
assert!(!ds.entries.is_empty(), "expected objects after merge");
|
|
38
|
+
}
|