emtiop 1.0.0__py3-none-any.whl
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.
- emtiop/__init__.py +21 -0
- emtiop/api.py +126 -0
- emtiop/buslists.py +74 -0
- emtiop/cim_examples.py +161 -0
- emtiop/cim_sparql.py +275 -0
- emtiop/cim_summary.py +100 -0
- emtiop/cim_support.py +404 -0
- emtiop/create_atp.py +1768 -0
- emtiop/create_mpow.py +398 -0
- emtiop/create_rdf.py +1699 -0
- emtiop/create_sql.py +222 -0
- emtiop/dll_config.py +517 -0
- emtiop/examples/IEEE118.atp +369 -0
- emtiop/examples/IEEE118.dyr +397 -0
- emtiop/examples/IEEE118.prm +33 -0
- emtiop/examples/IEEE118.raw +940 -0
- emtiop/examples/IEEE118_Network.json +4903 -0
- emtiop/examples/IEEE118_mRIDs.dat +2870 -0
- emtiop/examples/IEEE39.atp +97 -0
- emtiop/examples/IEEE39.dyr +65 -0
- emtiop/examples/IEEE39.prm +33 -0
- emtiop/examples/IEEE39.raw +252 -0
- emtiop/examples/IEEE39_Network.json +939 -0
- emtiop/examples/IEEE39_mRIDs.dat +552 -0
- emtiop/examples/SMIBDLL.atp +53 -0
- emtiop/examples/SMIBDLL.prm +47 -0
- emtiop/examples/SMIBDLL_Network.json +375 -0
- emtiop/examples/SMIBDLL_mRIDs.dat +109 -0
- emtiop/examples/WECC240.atp +605 -0
- emtiop/examples/WECC240.dyr +959 -0
- emtiop/examples/WECC240.prm +33 -0
- emtiop/examples/WECC240.raw +1907 -0
- emtiop/examples/WECC240_Network.json +8438 -0
- emtiop/examples/WECC240_mRIDs.dat +5505 -0
- emtiop/examples/XfmrSat.atp +41 -0
- emtiop/examples/XfmrSat.prm +28 -0
- emtiop/examples/XfmrSat.raw +97 -0
- emtiop/examples/XfmrSat_Network.json +106 -0
- emtiop/examples/XfmrSat_mRIDs.dat +48 -0
- emtiop/examples/__init__.py +1 -0
- emtiop/examples/atp.py +537 -0
- emtiop/examples/bps_make_mpow.py +55 -0
- emtiop/examples/cim_summary.py +31 -0
- emtiop/examples/cim_to_atp.py +59 -0
- emtiop/examples/create_smib_dll.py +148 -0
- emtiop/examples/ibr.pch +294 -0
- emtiop/examples/ic_to_rdf.py +146 -0
- emtiop/examples/matpower_write_ic.m +50 -0
- emtiop/examples/mpow.py +325 -0
- emtiop/examples/plot_bps.py +46 -0
- emtiop/examples/raw_to_rdf.py +45 -0
- emtiop/examples/syncmach.pch +156 -0
- emtiop/examples/tacspv3.pch +144 -0
- emtiop/examples/test_cim_sparql.py +65 -0
- emtiop/mpow_utilities.py +911 -0
- emtiop/plot_utils.py +347 -0
- emtiop/queries/detailed_model_types.json +2322 -0
- emtiop/queries/dynamics_defaults.json +1213 -0
- emtiop/queries/dyr_mapping.json +1107 -0
- emtiop/queries/raw_mapping.json +859 -0
- emtiop/queries/sparql_queries.xml +1994 -0
- emtiop/version.py +37 -0
- emtiop-1.0.0.dist-info/METADATA +126 -0
- emtiop-1.0.0.dist-info/RECORD +68 -0
- emtiop-1.0.0.dist-info/WHEEL +5 -0
- emtiop-1.0.0.dist-info/entry_points.txt +3 -0
- emtiop-1.0.0.dist-info/licenses/LICENSE +201 -0
- emtiop-1.0.0.dist-info/top_level.txt +1 -0
emtiop/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2026 3743 Authors
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
See the LICENSE file distributed with this work for copyright and
|
|
17
|
+
licensing information, the AUTHORS file for a list of copyright
|
|
18
|
+
holders, and the CONTRIBUTORS file for the list of contributors.
|
|
19
|
+
|
|
20
|
+
SPDX-License-Identifier: Apache-2.0"""
|
|
21
|
+
from .version import __version__
|
emtiop/api.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import absolute_import
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Copyright 2026 3743 Authors
|
|
5
|
+
|
|
6
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
you may not use this file except in compliance with the License.
|
|
8
|
+
You may obtain a copy of the License at
|
|
9
|
+
|
|
10
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
|
|
12
|
+
Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
See the License for the specific language governing permissions and
|
|
16
|
+
limitations under the License.
|
|
17
|
+
|
|
18
|
+
See the LICENSE file distributed with this work for copyright and
|
|
19
|
+
licensing information, the AUTHORS file for a list of copyright
|
|
20
|
+
holders, and the CONTRIBUTORS file for the list of contributors.
|
|
21
|
+
|
|
22
|
+
SPDX-License-Identifier: Apache-2.0"""
|
|
23
|
+
|
|
24
|
+
"""Functions intended for public access.
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
To show print CIM summaries interface::
|
|
28
|
+
|
|
29
|
+
import emtiop.api as emtiop
|
|
30
|
+
emtiop.print_cim_summaries(['XfmrSat','IEEE39','IEEE118','WECC240','SMIBDLL'])
|
|
31
|
+
|
|
32
|
+
Public Functions:
|
|
33
|
+
:add_ibr_plant: Add an IBR plant with attributes and a DLL interface.
|
|
34
|
+
:adhoc_sparql_dict: Load the result of a user-written SPARQL query into a Python dictionary.
|
|
35
|
+
:build_bus_lists: Order the buses (connectivity nodes) sequentially.
|
|
36
|
+
:build_system_graph: Auto-layout a diagram of the CIM network, returning a networkx graph.
|
|
37
|
+
:create_atp: Write an ATP netlist from CIM loaded into a Python dictionary.
|
|
38
|
+
:create_cim_rdf: Create RDF in TTL/XML formats from results of load_rawfile.
|
|
39
|
+
:create_cim_sql: Create SQL db from results of load_rawfile.
|
|
40
|
+
:create_matpower: Write a MATPOWER netlist from CIM loaded into a Python dictionary.
|
|
41
|
+
:extract_case: Copy example files for one case into the current directory.
|
|
42
|
+
:get_dll_interface: Returns a Python dictionary of the interface to an IEEE/Cigre DLL.
|
|
43
|
+
:get_swingbus_id: Find the CIM ConnectivityNode ID by matching the Name/Number from the original raw file.
|
|
44
|
+
:list_cases: List the names and descriptions of examples provided with the package.
|
|
45
|
+
:load_detailed_model_types: Load the library of detailed dynamic models with parameter descriptors.
|
|
46
|
+
:list_dict_table: Print the fields and attributes of a Python dictionary loaded from SPARQL.
|
|
47
|
+
:load_dynamics_defaults: Load example default settings for dynamics into a Python dictionary.
|
|
48
|
+
:load_dynamics_mapping: Load a dictionary of CIM classes and attributes corresponding to dyr file contents.
|
|
49
|
+
:load_emt_dict: Load an RDF graph into Python dictionary using packaged SPARQL queries.
|
|
50
|
+
:load_ic_dict: Load an RDF graph into Python dictionary from standalone power flow solution file.
|
|
51
|
+
:load_dyrfile: Load contents of a (PSSE) dyrfile into a Pandas dataframe.
|
|
52
|
+
:load_raw_meta: Load (PSSE) rawfile metadata into a Python dictionary.
|
|
53
|
+
:load_rawfile: Load contents of a (PSSE) rawfile into a Python dictionary.
|
|
54
|
+
:load_system_graph: Load the networkx layout (graph) from a JSON file.
|
|
55
|
+
:match_dyr_generators: Collect dyrfile models from Pandas dataframe into a Python dictionary by generator name/id.
|
|
56
|
+
:plot_system_graph: Plot the transmission system topology from a networkx layout (graph).
|
|
57
|
+
:print_cim_summaries: Print class names and counts found in list of Turtle files.
|
|
58
|
+
:print_raw_table: Print a named dictionary loaded from section of a (PSSE) rawfile.
|
|
59
|
+
:print_solution_summary: Prints a summary of solved MATPOWER case to console.
|
|
60
|
+
:read_matpower_casefile: Read a MATPOWER m file into Python dictionary.
|
|
61
|
+
:run_matpower_and_wait: Runs MATPOWER from command line on Windows, Mac, or Linux.
|
|
62
|
+
:save_system_graph: Save the networkx layout (graph) to a JSON file.
|
|
63
|
+
:summarize_casefile: Report the table sizes, total load, and total generation in a loaded MATPOWER case file.
|
|
64
|
+
:summarize_graph: Count the class instances by namespace in an RDF graph.
|
|
65
|
+
:summarize_dyrfile: Enumerate contents of a loaded (PSSE) dyrfile dataframe.
|
|
66
|
+
:write_atp_dll_interface: Netlists an ATP module that calls an IEEE/Cigre DLL.
|
|
67
|
+
:write_cim_rdf: Serialize the (possibly modified) return value from create_cim_rdf.
|
|
68
|
+
:write_most_table_indices: Writes some array column indices not included in the base MATPOWER set.
|
|
69
|
+
:CASES: Configuration data for five examples that come with this package.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
from .buslists import build_bus_lists
|
|
73
|
+
from .buslists import get_swingbus_id
|
|
74
|
+
|
|
75
|
+
from .cim_examples import CASES
|
|
76
|
+
from .cim_examples import list_cases
|
|
77
|
+
from .cim_examples import extract_case
|
|
78
|
+
|
|
79
|
+
from .cim_sparql import adhoc_sparql_dict
|
|
80
|
+
from .cim_sparql import list_dict_table
|
|
81
|
+
from .cim_sparql import load_emt_dict
|
|
82
|
+
from .cim_sparql import load_ic_dict
|
|
83
|
+
from .cim_sparql import summarize_graph
|
|
84
|
+
|
|
85
|
+
from .cim_summary import print_cim_summaries
|
|
86
|
+
|
|
87
|
+
from .cim_support import load_detailed_model_types
|
|
88
|
+
from .cim_support import load_dynamics_defaults
|
|
89
|
+
from .cim_support import load_dynamics_mapping
|
|
90
|
+
from .cim_support import load_dyrfile
|
|
91
|
+
from .cim_support import load_raw_meta
|
|
92
|
+
from .cim_support import load_rawfile
|
|
93
|
+
from .cim_support import match_dyr_generators
|
|
94
|
+
from .cim_support import print_raw_table
|
|
95
|
+
from .cim_support import summarize_dyrfile
|
|
96
|
+
|
|
97
|
+
from .create_atp import create_atp
|
|
98
|
+
|
|
99
|
+
from .create_mpow import create_matpower
|
|
100
|
+
|
|
101
|
+
from .create_rdf import add_ibr_plant
|
|
102
|
+
from .create_rdf import create_cim_rdf
|
|
103
|
+
from .create_rdf import write_cim_rdf
|
|
104
|
+
|
|
105
|
+
from .create_sql import create_cim_sql
|
|
106
|
+
|
|
107
|
+
from .dll_config import get_dll_interface
|
|
108
|
+
from .dll_config import write_atp_dll_interface
|
|
109
|
+
|
|
110
|
+
from .mpow_utilities import print_solution_summary
|
|
111
|
+
from .mpow_utilities import read_matpower_casefile
|
|
112
|
+
from .mpow_utilities import run_matpower_and_wait
|
|
113
|
+
from .mpow_utilities import summarize_casefile
|
|
114
|
+
from .mpow_utilities import write_most_table_indices
|
|
115
|
+
|
|
116
|
+
from .plot_utils import build_system_graph
|
|
117
|
+
from .plot_utils import load_system_graph
|
|
118
|
+
from .plot_utils import plot_system_graph
|
|
119
|
+
from .plot_utils import save_system_graph
|
|
120
|
+
|
|
121
|
+
from .version import __version__
|
|
122
|
+
from .version import __cim_schema__
|
|
123
|
+
from .version import __emtiop_schema__
|
|
124
|
+
from .version import __urn_cim__
|
|
125
|
+
from .version import __urn_emtiop__
|
|
126
|
+
|
emtiop/buslists.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2026 3743 Authors
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
See the LICENSE file distributed with this work for copyright and
|
|
17
|
+
licensing information, the AUTHORS file for a list of copyright
|
|
18
|
+
holders, and the CONTRIBUTORS file for the list of contributors.
|
|
19
|
+
|
|
20
|
+
SPDX-License-Identifier: Apache-2.0"""
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
Helper functions that navigate sequential bus numbers and CIM ConnectivityNode IDs.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def build_bus_lists (d:dict)->tuple[dict,dict]:
|
|
27
|
+
"""Order the buses (connectivity nodes) sequentially.
|
|
28
|
+
|
|
29
|
+
Creates two efficient dictionaries to access buses by their
|
|
30
|
+
sequential number, or by ConnectivityNode ID.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
d (dict): The dictionary of CIM query results for a network.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
ordered_buses(dict): The *EMTBus* dictionary, re-ordered by bus number.
|
|
37
|
+
bus_numbers(dict): Reverse lookup the ConnectivityNode ID by bus number.
|
|
38
|
+
"""
|
|
39
|
+
bNumeric = True
|
|
40
|
+
for key, data in d['EMTBus']['vals'].items():
|
|
41
|
+
if not data['name'].isdigit():
|
|
42
|
+
bNumeric = False
|
|
43
|
+
break
|
|
44
|
+
if bNumeric:
|
|
45
|
+
ordered_buses = dict(sorted(d['EMTBus']['vals'].items(), key=lambda x:int(x[1]['name'])))
|
|
46
|
+
print ('Numeric ordered buses')
|
|
47
|
+
else:
|
|
48
|
+
ordered_buses = d['EMTBus']['vals']
|
|
49
|
+
print ('Non-numeric ordered buses')
|
|
50
|
+
bus_numbers = {}
|
|
51
|
+
busnum = 1
|
|
52
|
+
for key, data in ordered_buses.items(): # data has name and nomv
|
|
53
|
+
bus_numbers[key] = busnum
|
|
54
|
+
busnum += 1
|
|
55
|
+
return ordered_buses, bus_numbers
|
|
56
|
+
|
|
57
|
+
def get_swingbus_id (ordered_buses:dict, swingbus:str)->str:
|
|
58
|
+
"""Find the CIM ConnectivityNode ID by matching the Name/Number from the original raw file.
|
|
59
|
+
|
|
60
|
+
The swing bus is used in MATPOWER or another power flow, and the Thevenin equivalent
|
|
61
|
+
source in some EMT models.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
ordered_buses (dict): a dictionary of *EMTBus* sorted by bus number
|
|
65
|
+
swingbus (str): the swing bus number, as a string name
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
cnid (str): the ConnectivityNode ID corresponding to the swing bus.
|
|
69
|
+
"""
|
|
70
|
+
for cnid, data in ordered_buses.items():
|
|
71
|
+
if data['name'] == swingbus:
|
|
72
|
+
return cnid
|
|
73
|
+
|
|
74
|
+
|
emtiop/cim_examples.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2026 3743 Authors
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
See the LICENSE file distributed with this work for copyright and
|
|
17
|
+
licensing information, the AUTHORS file for a list of copyright
|
|
18
|
+
holders, and the CONTRIBUTORS file for the list of contributors.
|
|
19
|
+
|
|
20
|
+
SPDX-License-Identifier: Apache-2.0"""
|
|
21
|
+
|
|
22
|
+
import importlib.resources
|
|
23
|
+
import shutil
|
|
24
|
+
import sys
|
|
25
|
+
import os
|
|
26
|
+
|
|
27
|
+
RDIR = 'emtiop.examples'
|
|
28
|
+
|
|
29
|
+
CASES = [
|
|
30
|
+
{'id': '6477751A-0472-4FD6-B3C3-3AD4945CBE56',
|
|
31
|
+
'name': 'IEEE39',
|
|
32
|
+
'desc': '39 buses, 9 machines, 1 IBR',
|
|
33
|
+
'legend_loc': 'lower right',
|
|
34
|
+
'wind_units': [],
|
|
35
|
+
'solar_units': ['30_1'],
|
|
36
|
+
'hydro_units': [],
|
|
37
|
+
'nuclear_units': [],
|
|
38
|
+
'swingbus': '31',
|
|
39
|
+
'load': 1.0,
|
|
40
|
+
'UseXfmrSaturation': False,
|
|
41
|
+
'UseMATPOWER': True},
|
|
42
|
+
{'id': '1783D2A8-1204-4781-A0B4-7A73A2FA6038',
|
|
43
|
+
'name': 'IEEE118',
|
|
44
|
+
'desc': '193 buses, 56 machines, 19 IBR',
|
|
45
|
+
'legend_loc': 'best',
|
|
46
|
+
'wind_units': ['132_W', '136_W', '138_W', '168_W', '180_W'],
|
|
47
|
+
'solar_units': ['126_S', '128_S', '130_S', '140_S', '149_S',
|
|
48
|
+
'151_S', '159_S', '165_S', '175_S', '179_S',
|
|
49
|
+
'183_S', '185_S', '188_S', '191_S'],
|
|
50
|
+
'hydro_units': [],
|
|
51
|
+
'nuclear_units': [],
|
|
52
|
+
'swingbus':'131',
|
|
53
|
+
'load': 0.6748,
|
|
54
|
+
'UseXfmrSaturation': False,
|
|
55
|
+
'UseMATPOWER': True},
|
|
56
|
+
{'id': '2540AF5C-4F83-4C0F-9577-DEE8CC73BBB3',
|
|
57
|
+
'name': 'WECC240',
|
|
58
|
+
'desc': '333 buses, 105 machines, 35 IBR',
|
|
59
|
+
'legend_loc': 'best',
|
|
60
|
+
'wind_units': ['10322_S', '10342_W', '13332_S', '21301_G', '2332_S',
|
|
61
|
+
'2431_S', '2434_S', '24382_RG', '24386_SW', '2439_S'],
|
|
62
|
+
'solar_units': ['2533_S', '2631_S', '32343_NW', '34331_S', '38351_NG',
|
|
63
|
+
'3932_S', '3933_CG', '39331_NB', '40311_H', '40312_S',
|
|
64
|
+
'4035_C', '40392_W', '41312_W', '42321_H', '5032_C',
|
|
65
|
+
'50324_W', '61322_H', '61324_W', '62351_H', '63331_W',
|
|
66
|
+
'64333_W', '6533_C', '65331_G', '70312_P', '70321_G'],
|
|
67
|
+
'hydro_units': ['12321_H', '13311_H', '21302_H', '2637_H', '2638_H',
|
|
68
|
+
'40352_H', '40391_H', '41311_H', '41321_H', '42312_H',
|
|
69
|
+
'50311_H', '63352_H', '65332_H', '70322_H', '8033_H',
|
|
70
|
+
'80341_H'],
|
|
71
|
+
'nuclear_units': ['14311_N', '41322_N'],
|
|
72
|
+
'swingbus':'3831',
|
|
73
|
+
'old_swingbus':'2438',
|
|
74
|
+
'load': 1.02,
|
|
75
|
+
'UseXfmrSaturation': False,
|
|
76
|
+
'UseMATPOWER': True},
|
|
77
|
+
{'id': '93EA6BF1-A569-4190-9590-98A62780489E',
|
|
78
|
+
'name':'XfmrSat',
|
|
79
|
+
'desc': '5 buses, load rejection with transformer saturation',
|
|
80
|
+
'legend_loc': 'best',
|
|
81
|
+
'wind_units':[],
|
|
82
|
+
'solar_units':[],
|
|
83
|
+
'hydro_units':[],
|
|
84
|
+
'nuclear_units':[],
|
|
85
|
+
'swingbus': '1',
|
|
86
|
+
'load':1.0,
|
|
87
|
+
'emergency_ratings': True,
|
|
88
|
+
'UseXfmrSaturation': True,
|
|
89
|
+
'UseMATPOWER': True},
|
|
90
|
+
{'id': '62CB0930-211D-4762-B5C1-27BF73EAC7C4',
|
|
91
|
+
'name': 'SMIBDLL',
|
|
92
|
+
'desc': '12 buses in a test harness, 1 IBR in a DLL',
|
|
93
|
+
'legend_loc': 'best',
|
|
94
|
+
'wind_units': [],
|
|
95
|
+
'solar_units': ['1_1'],
|
|
96
|
+
'hydro_units': [],
|
|
97
|
+
'nuclear_units': [],
|
|
98
|
+
'swingbus': '5',
|
|
99
|
+
'load': 1.0,
|
|
100
|
+
'UseXfmrSaturation': True,
|
|
101
|
+
'UseMATPOWER': False,
|
|
102
|
+
'ExtraFiles': ['create_smib_dll.py']} # 'gfm_gfl_ibr2.dll', 'gfm_gfl_ibr2.dll32' now curled from test.bat
|
|
103
|
+
]
|
|
104
|
+
"""A list containing example case configurations as dictionaries."""
|
|
105
|
+
|
|
106
|
+
def list_cases()->None:
|
|
107
|
+
"""List the names and descriptions of examples provided with the package.
|
|
108
|
+
|
|
109
|
+
Prints information to the console. Use the *Idx* as an argument to *extract_case*
|
|
110
|
+
to copy the example files for one case into the current directory.
|
|
111
|
+
"""
|
|
112
|
+
print ('Idx {:8s} {:36s} {:s}'.format ('Name', 'mRID', 'Description'))
|
|
113
|
+
for i in range(len(CASES)):
|
|
114
|
+
print ('{:3d} {:8s} {:36s} {:s}'.format (i, CASES[i]['name'], CASES[i]['id'], CASES[i]['desc']))
|
|
115
|
+
|
|
116
|
+
def extract_case ()->None:
|
|
117
|
+
"""Copy example files for one case into the current directory.
|
|
118
|
+
|
|
119
|
+
Select the case by index, as obtained from *list_cases*. The
|
|
120
|
+
index must be passed as an argument on the command line.
|
|
121
|
+
"""
|
|
122
|
+
idx = int(sys.argv[1])
|
|
123
|
+
case = CASES[idx]
|
|
124
|
+
root = case['name']
|
|
125
|
+
|
|
126
|
+
# case-specific example files
|
|
127
|
+
for ext in ['.raw', '.dyr', '_Network.json', '_mRIDs.dat', '.atp', '.prm']:
|
|
128
|
+
src = importlib.resources.files(RDIR).joinpath('{:s}{:s}'.format(root, ext))
|
|
129
|
+
with importlib.resources.as_file(src) as fpath:
|
|
130
|
+
if os.path.isfile(fpath):
|
|
131
|
+
shutil.copy(fpath, '.')
|
|
132
|
+
|
|
133
|
+
if 'ExtraFiles' in case:
|
|
134
|
+
for fname in case['ExtraFiles']:
|
|
135
|
+
if not os.path.isfile('./{:s}'.format(fname)):
|
|
136
|
+
src = importlib.resources.files(RDIR).joinpath(fname)
|
|
137
|
+
with importlib.resources.as_file(src) as fpath:
|
|
138
|
+
if os.path.isfile(fpath):
|
|
139
|
+
shutil.copy(fpath, '.')
|
|
140
|
+
|
|
141
|
+
# MATPOWER and ATP support files
|
|
142
|
+
for fname in ['matpower_write_ic.m', 'ibr.pch', 'syncmach.pch', 'tacspv3.pch']:
|
|
143
|
+
if not os.path.isfile('./{:s}'.format(fname)):
|
|
144
|
+
src = importlib.resources.files(RDIR).joinpath(fname)
|
|
145
|
+
with importlib.resources.as_file(src) as fpath:
|
|
146
|
+
if os.path.isfile(fpath):
|
|
147
|
+
shutil.copy(fpath, '.')
|
|
148
|
+
|
|
149
|
+
# Python script files
|
|
150
|
+
for fname in ['raw_to_rdf.py', 'bps_make_mpow.py', 'mpow.py', 'ic_to_rdf.py',
|
|
151
|
+
'cim_to_atp.py', 'atp.py', 'plot_bps.py', 'cim_summary.py',
|
|
152
|
+
'test_cim_sparql.py']:
|
|
153
|
+
if not os.path.isfile('./{:s}'.format(fname)):
|
|
154
|
+
src = importlib.resources.files(RDIR).joinpath(fname)
|
|
155
|
+
with importlib.resources.as_file(src) as fpath:
|
|
156
|
+
if os.path.isfile(fpath):
|
|
157
|
+
shutil.copy(fpath, '.')
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
emtiop/cim_sparql.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2026 3743 Authors
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
See the LICENSE file distributed with this work for copyright and
|
|
17
|
+
licensing information, the AUTHORS file for a list of copyright
|
|
18
|
+
holders, and the CONTRIBUTORS file for the list of contributors.
|
|
19
|
+
|
|
20
|
+
SPDX-License-Identifier: Apache-2.0"""
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
Functions that read CIM into Python dictionaries using SPARQL queries.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import rdflib
|
|
27
|
+
import time
|
|
28
|
+
import xml.etree.ElementTree as ET
|
|
29
|
+
import importlib.resources
|
|
30
|
+
import sys
|
|
31
|
+
|
|
32
|
+
XML_QUERY_FILE = 'sparql_queries.xml'
|
|
33
|
+
PREFIX = None
|
|
34
|
+
DELIM = ':'
|
|
35
|
+
ROOT = None
|
|
36
|
+
|
|
37
|
+
def summarize_graph (g:rdflib.Graph)->None:
|
|
38
|
+
"""Count the class instances by namespace in an RDF graph.
|
|
39
|
+
|
|
40
|
+
Use this function for a printed summary of the graph size.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
g (Graph): the RDF Graph obtained from construction in memory, or loaded from a file.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
q = """
|
|
47
|
+
SELECT ?class (COUNT(?class) as ?cnt) WHERE {
|
|
48
|
+
?s a ?class .
|
|
49
|
+
} group by ?class order by ?class"""
|
|
50
|
+
|
|
51
|
+
print ('{:40s} {:40s} {:5s}'.format ('Namespace', 'Class Name', 'Count'))
|
|
52
|
+
for r in g.query(q):
|
|
53
|
+
nscls = r['class']
|
|
54
|
+
idx = nscls.find('#')
|
|
55
|
+
ns = nscls[:idx]
|
|
56
|
+
cls = nscls[idx+1:]
|
|
57
|
+
print('{:40s} {:40s} {:5d}'.format (ns, cls, int(r['cnt'])))
|
|
58
|
+
|
|
59
|
+
def list_dict_table(dict:str, tag:str=None)->None:
|
|
60
|
+
"""Print the fields and attributes of a Python dictionary loaded from SPARQL.
|
|
61
|
+
|
|
62
|
+
Use this function to explore the structure, size, and contents of a table loaded from SPARQL.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
dict (dict): result from *load_emt_dict*, *load_ic_dict*, or *adhoc_sparql_dict*
|
|
66
|
+
tag (str): should be a loaded table name, 'Adhoc Query'. If it includes \\* then the table is multi-keyed.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
bMultiKey = False
|
|
70
|
+
if tag is None:
|
|
71
|
+
tbl = dict
|
|
72
|
+
tag = 'Adhoc Query'
|
|
73
|
+
else:
|
|
74
|
+
if '*' in tag:
|
|
75
|
+
bMultiKey = True
|
|
76
|
+
tbl = dict[tag]
|
|
77
|
+
print ('\n{:s}: key,{:s}'.format(tag, str(tbl['columns'])))
|
|
78
|
+
if bMultiKey:
|
|
79
|
+
for key, ary in tbl['vals'].items():
|
|
80
|
+
for row in ary:
|
|
81
|
+
print ('{:s},{:s}'.format (key, ','.join(str(row[c]) for c in tbl['columns'])))
|
|
82
|
+
else:
|
|
83
|
+
for key, row in tbl['vals'].items():
|
|
84
|
+
print ('{:s},{:s}'.format (key, ','.join(str(row[c]) for c in tbl['columns'])))
|
|
85
|
+
|
|
86
|
+
def build_query (prefix:str, base:str, sysid:str)->str:
|
|
87
|
+
if sysid is not None:
|
|
88
|
+
idx = base.find('WHERE {') + 8
|
|
89
|
+
return prefix + '\n' + base[:idx] + """ VALUES ?EquipmentContainer_mRID {{"{:s}"^^c:String}}\n""".format (sysid) + base[idx:]
|
|
90
|
+
return prefix + '\n' + base
|
|
91
|
+
|
|
92
|
+
def query_for_values (g:rdflib.Graph, tbl:dict, sysid:str)->None:
|
|
93
|
+
bMultiKey = False
|
|
94
|
+
if '*' in tbl['keyfld']:
|
|
95
|
+
bMultiKey = True
|
|
96
|
+
keyflds = tbl['keyfld'].strip('*').split(':')
|
|
97
|
+
q = build_query (PREFIX, tbl['sparql'], sysid)
|
|
98
|
+
#print ('===================')
|
|
99
|
+
#print (q)
|
|
100
|
+
result = g.query(q)
|
|
101
|
+
#print (result)
|
|
102
|
+
vars = [str(item) for item in result.vars]
|
|
103
|
+
for akey in keyflds:
|
|
104
|
+
vars.remove (akey)
|
|
105
|
+
tbl['columns'] = vars
|
|
106
|
+
for b in result:
|
|
107
|
+
row = {}
|
|
108
|
+
key = str(b[keyflds[0]])
|
|
109
|
+
for i in range(1, len(keyflds)):
|
|
110
|
+
key = key + DELIM + str(b[keyflds[i]])
|
|
111
|
+
for fld in vars:
|
|
112
|
+
#print (' ', key, fld, b[fld])
|
|
113
|
+
if b[fld] is None:
|
|
114
|
+
row[fld] = None
|
|
115
|
+
elif 'mRID' in fld:
|
|
116
|
+
row[fld] = str(b[fld])
|
|
117
|
+
elif '_name' in fld:
|
|
118
|
+
row[fld] = str(b[fld])
|
|
119
|
+
elif '_type' in fld:
|
|
120
|
+
row[fld] = str(b[fld])
|
|
121
|
+
elif fld in ['name', 'connectionKind', 'vectorGroup', 'inputSignalType', 'kind', 'parameterKind', 'unit']:
|
|
122
|
+
row[fld] = str(b[fld])
|
|
123
|
+
else:
|
|
124
|
+
try:
|
|
125
|
+
row[fld] = int(b[fld])
|
|
126
|
+
except ValueError:
|
|
127
|
+
try:
|
|
128
|
+
row[fld] = float(b[fld])
|
|
129
|
+
except ValueError:
|
|
130
|
+
row[fld] = str(b[fld])
|
|
131
|
+
if bMultiKey:
|
|
132
|
+
if key not in tbl['vals']:
|
|
133
|
+
tbl['vals'][key] = []
|
|
134
|
+
tbl['vals'][key].append(row)
|
|
135
|
+
else:
|
|
136
|
+
tbl['vals'][key] = row
|
|
137
|
+
|
|
138
|
+
def load_root_queries(bPrint:bool=False)->None:
|
|
139
|
+
global ROOT, PREFIX
|
|
140
|
+
if ROOT is None:
|
|
141
|
+
# read the queries into dict
|
|
142
|
+
xfile = importlib.resources.files('emtiop.queries').joinpath(XML_QUERY_FILE)
|
|
143
|
+
with open (xfile, 'r', encoding='utf-8', errors='strict') as fp:
|
|
144
|
+
s = fp.read()
|
|
145
|
+
ROOT = ET.fromstring(s)
|
|
146
|
+
nsCIM = ROOT.find('nsCIM').text.strip()
|
|
147
|
+
nsRDF = ROOT.find('nsRDF').text.strip()
|
|
148
|
+
nsEMT = ROOT.find('nsEMT').text.strip()
|
|
149
|
+
PREFIX = """PREFIX r: <{:s}>\nPREFIX c: <{:s}>\nPREFIX e: <{:s}>""".format (nsRDF, nsCIM, nsEMT)
|
|
150
|
+
if bPrint:
|
|
151
|
+
print (PREFIX)
|
|
152
|
+
|
|
153
|
+
def load_emt_dict (g:rdflib.Graph, sysid:str, bTiming:bool=False)->dict:
|
|
154
|
+
"""Load an RDF graph into Python dictionary using packaged SPARQL queries.
|
|
155
|
+
|
|
156
|
+
This function works on a graph that might contain several transmission system datasets,
|
|
157
|
+
differentiated by *sysid*. The correct *sysid* must be supplied, even if the graph
|
|
158
|
+
contains data for only one transmission system, because *sysid* filters all queries.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
g (Graph): an RDF graph loaded from *XML* or *TTL* file
|
|
162
|
+
sysid (str): the ID of the transmission system's *EquipmentContainer*
|
|
163
|
+
bTiming (bool): print the execution time of each packaged query
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
dict(dict): dictionary of *EMT* tables loaded from SPARQL.
|
|
167
|
+
"""
|
|
168
|
+
global ROOT
|
|
169
|
+
|
|
170
|
+
current_recursion_limit = sys.getrecursionlimit()
|
|
171
|
+
new_recursion_limit = 10000
|
|
172
|
+
if new_recursion_limit > current_recursion_limit:
|
|
173
|
+
sys.setrecursionlimit(new_recursion_limit)
|
|
174
|
+
print ('For SPARQL, changing recursion limit from {:d} to {:d}'.format (current_recursion_limit, new_recursion_limit))
|
|
175
|
+
|
|
176
|
+
start_time = time.time()
|
|
177
|
+
load_root_queries()
|
|
178
|
+
dict = {}
|
|
179
|
+
for query in ROOT.findall('query'):
|
|
180
|
+
qid = query.find('id').text.strip()
|
|
181
|
+
dict[qid] = {}
|
|
182
|
+
dict[qid]['keyfld'] = query.find('keyfld').text
|
|
183
|
+
dict[qid]['sparql'] = query.find('value').text.strip()
|
|
184
|
+
dict[qid]['columns'] = []
|
|
185
|
+
dict[qid]['vals'] = {}
|
|
186
|
+
#print (' ', qid, dict[qid]['keyfld'])
|
|
187
|
+
|
|
188
|
+
for key in ['EMTContainer', 'EMTBus', 'EMTBusXY', 'EMTBaseVoltage', 'EMTLine', 'EMTLoad',
|
|
189
|
+
'EMTCountPowerXfmrWindings', 'EMTPowerXfmrWinding', 'EMTPowerXfmrCore',
|
|
190
|
+
'EMTPowerXfmrMesh', 'EMTXfmrTap', 'EMTXfmrSaturation', 'EMTCompShunt', 'EMTCompSeries',
|
|
191
|
+
'EMTSyncMachine', 'EMTSolar', 'EMTWind',
|
|
192
|
+
'EMTDynamicsModel', 'EMTDynamicsModelType', 'EMTDynamicsParameterDescriptor', 'EMTDynamicsParameter',
|
|
193
|
+
# 'EMTGovGAST', 'EMTGovHydro1', 'EMTGovSteam0',
|
|
194
|
+
# 'EMTGovSteamSGO', 'EMTExcST1A', 'EMTExcIEEEDC1A', 'EMTExcSEXS', 'EMTPss1A',
|
|
195
|
+
# 'EMTPssIEEE1A', 'EMTWeccREGCA', 'EMTWeccREECA', 'EMTWeccREPCA',
|
|
196
|
+
# 'EMTWeccWTGTA', 'EMTWeccWTGARA',
|
|
197
|
+
'EMTEnergySource', 'EMTDisconnectingCircuitBreaker',
|
|
198
|
+
'EMTXfmrLimit', 'EMTBranchLimit', 'EMTBusVoltage', 'EMTBranchFlow', 'EMTXfmrFlow',
|
|
199
|
+
'EMTIBRPlant*', 'EMTRotatingMachinePlant*', 'EMTIEEECigreAPIParameters*',
|
|
200
|
+
'EMTIBRPlantAttributes', 'EMTCountAPIParameters', 'EMTIEEECigreAPI',
|
|
201
|
+
'EMTCountAPIInputs', 'EMTIEEECigreAPIInputs*', 'EMTCountAPIOutputs', 'EMTIEEECigreAPIOutputs*',
|
|
202
|
+
'EMTEquipmentContainer', 'EMTDCEquipmentContainer', 'EMTDCNode', 'EMTDCShunt', 'EMTDCEnergySource',
|
|
203
|
+
'EMTIEEECigreAPIInfo', 'EMTIEEECigreAPIParameterInfos*', 'EMTIEEECigreAPIInputSignalInfos*',
|
|
204
|
+
'EMTIEEECigreAPIOutputSignalInfos*']:
|
|
205
|
+
if bTiming:
|
|
206
|
+
query_start_time = time.time()
|
|
207
|
+
query_for_values (g, dict[key], sysid)
|
|
208
|
+
if bTiming:
|
|
209
|
+
print (' Running {:40s} took {:6.3f} s for {:5d} rows'.format (key, time.time() - query_start_time, len(dict[key]['vals'])))
|
|
210
|
+
if bTiming:
|
|
211
|
+
print ('Total query time {:6.3f} s'.format (time.time() - start_time))
|
|
212
|
+
return dict
|
|
213
|
+
|
|
214
|
+
def load_ic_dict (g:rdflib.Graph, bPrint:bool=False)->dict:
|
|
215
|
+
"""Load an RDF graph into Python dictionary from standalone power flow solution file.
|
|
216
|
+
|
|
217
|
+
This function only runs the *EMTBusVoltageIC* and *EMTBranchFlowIC* queries
|
|
218
|
+
on data that usually comes from a power flow solver.
|
|
219
|
+
|
|
220
|
+
The *EMTXfmrFlowIC* no longer works in a standalone power flow solution file. To
|
|
221
|
+
obtain this filtering, merge the power flow solution and network model into a single
|
|
222
|
+
CIM RDF file, and then use *EMTXfmrFlow*.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
g (Graph): an RDF graph loaded from *XML* or *TTL* file
|
|
226
|
+
bPrint (bool): print the total query time
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
dict(dict): dictionary of *EMT* tables loaded from SPARQL.
|
|
230
|
+
"""
|
|
231
|
+
global ROOT
|
|
232
|
+
start_time = time.time()
|
|
233
|
+
# read the queries into dict
|
|
234
|
+
load_root_queries()
|
|
235
|
+
dict = {}
|
|
236
|
+
for query in ROOT.findall('query'):
|
|
237
|
+
qid = query.find('id').text.strip()
|
|
238
|
+
dict[qid] = {}
|
|
239
|
+
dict[qid]['keyfld'] = query.find('keyfld').text
|
|
240
|
+
dict[qid]['sparql'] = query.find('value').text.strip()
|
|
241
|
+
dict[qid]['columns'] = []
|
|
242
|
+
dict[qid]['vals'] = {}
|
|
243
|
+
#print (' ', qid, dict[qid]['keyfld'])
|
|
244
|
+
|
|
245
|
+
for key in ['EMTBusVoltageIC', 'EMTBranchFlowIC']: # , 'EMTXfmrFlowIC']:
|
|
246
|
+
query_for_values (g, dict[key], sysid=None)
|
|
247
|
+
|
|
248
|
+
if bPrint:
|
|
249
|
+
print ('Total query time {:6.3f} s'.format (time.time() - start_time))
|
|
250
|
+
return dict
|
|
251
|
+
|
|
252
|
+
def adhoc_sparql_dict (g:rdflib.Graph, q:str, key_field:str)->dict:
|
|
253
|
+
"""Load the result of a user-written SPARQL query into a Python dictionary.
|
|
254
|
+
|
|
255
|
+
Use this function to run SPARQL that has been constructed at runtime. It is used
|
|
256
|
+
to work with IBR and conventional generating plants that are not rooted in raw files,
|
|
257
|
+
i.e., the plant data is constructed at runtime.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
g (Graph): an RDF graph constructed in memory, or loaded from *XML* or *TTL* file
|
|
261
|
+
q (str): the SPARQL query text
|
|
262
|
+
key_field (str): the field to index query results on, usually a CIM *mRID*
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
dict(dict): dictionary results from SPARQL, corresponding to a single un-named table.
|
|
266
|
+
"""
|
|
267
|
+
load_root_queries()
|
|
268
|
+
dict = {}
|
|
269
|
+
dict['keyfld'] = key_field
|
|
270
|
+
dict['sparql'] = q
|
|
271
|
+
dict['columns'] = []
|
|
272
|
+
dict['vals'] = {}
|
|
273
|
+
query_for_values (g, dict, sysid=None)
|
|
274
|
+
return dict
|
|
275
|
+
|