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.
Files changed (68) hide show
  1. emtiop/__init__.py +21 -0
  2. emtiop/api.py +126 -0
  3. emtiop/buslists.py +74 -0
  4. emtiop/cim_examples.py +161 -0
  5. emtiop/cim_sparql.py +275 -0
  6. emtiop/cim_summary.py +100 -0
  7. emtiop/cim_support.py +404 -0
  8. emtiop/create_atp.py +1768 -0
  9. emtiop/create_mpow.py +398 -0
  10. emtiop/create_rdf.py +1699 -0
  11. emtiop/create_sql.py +222 -0
  12. emtiop/dll_config.py +517 -0
  13. emtiop/examples/IEEE118.atp +369 -0
  14. emtiop/examples/IEEE118.dyr +397 -0
  15. emtiop/examples/IEEE118.prm +33 -0
  16. emtiop/examples/IEEE118.raw +940 -0
  17. emtiop/examples/IEEE118_Network.json +4903 -0
  18. emtiop/examples/IEEE118_mRIDs.dat +2870 -0
  19. emtiop/examples/IEEE39.atp +97 -0
  20. emtiop/examples/IEEE39.dyr +65 -0
  21. emtiop/examples/IEEE39.prm +33 -0
  22. emtiop/examples/IEEE39.raw +252 -0
  23. emtiop/examples/IEEE39_Network.json +939 -0
  24. emtiop/examples/IEEE39_mRIDs.dat +552 -0
  25. emtiop/examples/SMIBDLL.atp +53 -0
  26. emtiop/examples/SMIBDLL.prm +47 -0
  27. emtiop/examples/SMIBDLL_Network.json +375 -0
  28. emtiop/examples/SMIBDLL_mRIDs.dat +109 -0
  29. emtiop/examples/WECC240.atp +605 -0
  30. emtiop/examples/WECC240.dyr +959 -0
  31. emtiop/examples/WECC240.prm +33 -0
  32. emtiop/examples/WECC240.raw +1907 -0
  33. emtiop/examples/WECC240_Network.json +8438 -0
  34. emtiop/examples/WECC240_mRIDs.dat +5505 -0
  35. emtiop/examples/XfmrSat.atp +41 -0
  36. emtiop/examples/XfmrSat.prm +28 -0
  37. emtiop/examples/XfmrSat.raw +97 -0
  38. emtiop/examples/XfmrSat_Network.json +106 -0
  39. emtiop/examples/XfmrSat_mRIDs.dat +48 -0
  40. emtiop/examples/__init__.py +1 -0
  41. emtiop/examples/atp.py +537 -0
  42. emtiop/examples/bps_make_mpow.py +55 -0
  43. emtiop/examples/cim_summary.py +31 -0
  44. emtiop/examples/cim_to_atp.py +59 -0
  45. emtiop/examples/create_smib_dll.py +148 -0
  46. emtiop/examples/ibr.pch +294 -0
  47. emtiop/examples/ic_to_rdf.py +146 -0
  48. emtiop/examples/matpower_write_ic.m +50 -0
  49. emtiop/examples/mpow.py +325 -0
  50. emtiop/examples/plot_bps.py +46 -0
  51. emtiop/examples/raw_to_rdf.py +45 -0
  52. emtiop/examples/syncmach.pch +156 -0
  53. emtiop/examples/tacspv3.pch +144 -0
  54. emtiop/examples/test_cim_sparql.py +65 -0
  55. emtiop/mpow_utilities.py +911 -0
  56. emtiop/plot_utils.py +347 -0
  57. emtiop/queries/detailed_model_types.json +2322 -0
  58. emtiop/queries/dynamics_defaults.json +1213 -0
  59. emtiop/queries/dyr_mapping.json +1107 -0
  60. emtiop/queries/raw_mapping.json +859 -0
  61. emtiop/queries/sparql_queries.xml +1994 -0
  62. emtiop/version.py +37 -0
  63. emtiop-1.0.0.dist-info/METADATA +126 -0
  64. emtiop-1.0.0.dist-info/RECORD +68 -0
  65. emtiop-1.0.0.dist-info/WHEEL +5 -0
  66. emtiop-1.0.0.dist-info/entry_points.txt +3 -0
  67. emtiop-1.0.0.dist-info/licenses/LICENSE +201 -0
  68. 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
+