├── .devcontainer
└── devcontainer.json
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ ├── dev-pre-release.yml
│ └── dispatch-pypi.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CIMTool_plugin
├── builder.md
├── cimantic-graphs-init.xsl
├── cimantic-graphs.xsl
├── cimantic_graphs.png
├── cimantic_graphs_init.png
├── init.png
└── profile.png
├── LICENSE
├── README.md
├── cim_graph_structure.png
├── cimgraph
├── __init__.py
├── data_profile
│ ├── __init__.py
│ ├── cim17v40
│ │ ├── __init__.py
│ │ └── cim17v40.py
│ ├── cimhub_2023
│ │ ├── __init__.py
│ │ └── cimhub_2023.py
│ ├── cimhub_ufls
│ │ ├── __init__.py
│ │ └── cimhub_ufls.py
│ ├── identity.py
│ ├── known_problem_classes.py
│ ├── rc4_2021
│ │ ├── __init__.py
│ │ └── rc4_2021.py
│ ├── ufls
│ │ ├── __init__.py
│ │ └── ufls.py
│ ├── units
│ │ ├── __init__.py
│ │ ├── cim_units
│ │ │ └── units.txt
│ │ └── units.py
│ └── xsd_config.xml
├── databases
│ ├── __init__.py
│ ├── blazegraph
│ │ ├── __init__.py
│ │ └── blazegraph.py
│ ├── fileparsers
│ │ ├── __init__.py
│ │ └── xml_parser.py
│ ├── graphdb
│ │ ├── __init__.py
│ │ └── graphdb.py
│ ├── gridappsd
│ │ ├── __init__.py
│ │ └── gridappsd.py
│ ├── mysql
│ │ ├── __init__.py
│ │ └── mysql.py
│ ├── neo4j
│ │ ├── __init__.py
│ │ └── neo4j.py
│ ├── piwebapi
│ │ └── __init__.py
│ └── rdflib
│ │ ├── __init__.py
│ │ └── rdflib.py
├── models
│ ├── .ipynb_checkpoints
│ │ └── Untitled-checkpoint.ipynb
│ ├── __init__.py
│ ├── bus_branch_model.py
│ ├── distributed_area.py
│ ├── feeder_model.py
│ ├── graph_model.py
│ └── node_breaker_model.py
├── queries
│ ├── __init__.py
│ ├── cypher
│ │ ├── __init__.py
│ │ ├── get_all_edges.py
│ │ ├── get_all_nodes.py
│ │ ├── get_object.py
│ │ └── get_triple.py
│ ├── jsonld_sql
│ │ ├── __init__.py
│ │ ├── get_all_edges.py
│ │ └── get_all_nodes.py
│ ├── ontotext
│ │ ├── __init__.py
│ │ ├── get_all_edges.py
│ │ └── get_all_nodes.py
│ ├── rdflib
│ │ ├── __init__.py
│ │ ├── get_all_attributes.py
│ │ ├── get_all_edges.py
│ │ └── get_all_nodes.py
│ └── sparql
│ │ ├── __init__.py
│ │ ├── get_all_attributes.py
│ │ ├── get_all_edges.py
│ │ ├── get_all_nodes.py
│ │ ├── get_object.py
│ │ ├── get_triple.py
│ │ └── upload_triples.py
└── utils
│ ├── __init__.py
│ ├── get_all_data.py
│ ├── mermaid.py
│ ├── object_utils.py
│ ├── readme.md
│ ├── write_csv.py
│ ├── write_json.py
│ └── write_xml.py
├── docker-compose.yml
├── example.env
├── examples
├── bus_branch_example.ipynb
├── debug notebooks
│ ├── der_demo.ipynb
│ ├── glm.ipynb
│ ├── ieee118_bus_branch_example.ipynb
│ ├── ieee13_example.ipynb
│ └── locations.ipynb
├── ditto_examples.ipynb
├── feeder_example.ipynb
└── node_breaker_example.ipynb
├── mermaid.md
├── pyproject.toml
└── tests
├── node_breaker_topo_msg.json
├── test_CIM_PROFILE.py
├── test_blazegraph_seto.py
├── test_get_all_edges.py
├── test_gridappsd_connection.py
├── test_gridappsd_datasource.py
├── test_mermaid.py
├── test_models
├── IEEE118_CIM.xml
├── IEEE123.xml
├── IEEE13.ttl
├── ieee123_pv.xml
├── ieee13.jsonld
├── ieee13.xml
├── ieee13_assets.xml
├── ieee9500bal.xml
└── maple10nodebreaker.xml
├── test_neo4j.py
├── test_rdflib.py
├── test_uuid.py
├── test_xmlfile.py
├── timing.py
├── topo_message.json
└── uuid_test.ipynb
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "image": "mcr.microsoft.com/devcontainers/universal:2",
3 | "features": {
4 | "ghcr.io/devcontainers-contrib/features/poetry:2": {}
5 | },
6 | "postStartCommand": "docker compose up -d",
7 | "hostRequirements": {
8 | "cpus": 4,
9 | "memory": "8gb",
10 | "storage": "32gb"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set the default behavior, in case people don't have core.autocrlf set.
2 | * text=auto
3 |
4 | # Explicitly declare text files you want to always be normalized and converted
5 | # to native line endings on checkout.
6 | *.c text
7 | *.h text
8 | *.py text
9 |
10 | # Declare files that will always have CRLF line endings on checkout.
11 | *.sln text eol=crlf
12 |
13 | # Denote all files that are truly binary and should not be modified.
14 | *.png binary
15 | *.jpg binary
16 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Operating System (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Volttron Version [develop, releases/8.2, main]
29 |
30 | **Additional context**
31 | Add any other context about the problem here.
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/dev-pre-release.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Pre-Release Package
3 |
4 | on:
5 | push:
6 | branches:
7 | - develop
8 | workflow_dispatch:
9 | inputs:
10 | force:
11 | description: 'Force deploy'
12 | required: false
13 | default: false
14 | type: boolean
15 | jobs:
16 | check-changes:
17 | runs-on: ubuntu-latest
18 | outputs:
19 | any_changed: ${{ steps.changed-files.outputs.any_changed }}
20 | steps:
21 | - name: Checkout repository
22 | uses: actions/checkout@v2
23 | with:
24 | fetch-depth: 0
25 |
26 | - name: Get changed files
27 | id: changed-files
28 | uses: tj-actions/changed-files@v45
29 | with:
30 | files: |
31 | **.py
32 | build:
33 | runs-on: ubuntu-latest
34 | needs: check-changes
35 | if: needs.check-changes.outputs.any_changed == 'true' || github.event.inputs.force == 'true'
36 | steps:
37 | - name: Checkout repository
38 | uses: actions/checkout@v2
39 | with:
40 | fetch-depth: 0
41 |
42 | - name: Set up Python
43 | uses: actions/setup-python@v4
44 | with:
45 | python-version: '3.10'
46 |
47 | - name: Install Poetry
48 | uses: snok/install-poetry@v1.4.1
49 | with:
50 | virtualenvs-in-project: true
51 | installer-parallel: true
52 |
53 | - name: Install Poetry Plugins
54 | run: |
55 | poetry self add poetry-plugin-export
56 |
57 | - name: Bump version
58 | id: bump-version
59 | run: |
60 | echo "Bumping version..."
61 | echo "From: $(poetry version -s)"
62 | poetry version prerelease
63 | NEW_TAG=v$(poetry version --short)
64 | echo "To: $(poetry version -s)"
65 | echo "NEW_TAG=$(echo ${NEW_TAG})" >> $GITHUB_ENV
66 |
67 | - name: Install dependencies
68 | run: |
69 | poetry install --without dev
70 | poetry build
71 |
72 | - name: Commit bumped version
73 | run: |
74 | git config --global user.name 'gridappsd[bot]'
75 | git config --global user.email 'gridappsd[bot]@users.noreply.github.com'
76 | git commit -am "Bump version to $(poetry version -s)"
77 | git push origin develop
78 |
79 | - name: Create Release
80 | uses: ncipollo/release-action@v1.15.0
81 | env:
82 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
83 | with:
84 | artifacts: "dist/*.gz,dist/*.whl"
85 | artifactErrorsFailBuild: true
86 | generateReleaseNotes: true
87 | commit: ${{ github.ref }}
88 | prerelease: true
89 | tag: ${{ env.NEW_TAG }}
90 | token: ${{ secrets.GITHUB_TOKEN }}
91 | - name: Publish to PyPI
92 | id: publish-to-pypi
93 | run: |
94 | # This is needed, because the poetry publish will fail at the top level of the project
95 | # so ./scripts/run_on_each.sh fails for that.
96 | echo "POETRY_PUBLISH_OPTIONS=''" >> $GITHUB_ENV
97 |
98 | poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }}
99 | poetry publish
100 |
--------------------------------------------------------------------------------
/.github/workflows/dispatch-pypi.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # Documentation located
3 | # https://github.com/marketplace/actions/publish-python-poetry-package
4 | name: Dispatch to PyPi
5 |
6 | on:
7 | workflow_dispatch:
8 |
9 | defaults:
10 | run:
11 | shell: bash
12 |
13 | env:
14 | LANG: en_US.utf-8
15 | LC_ALL: en_US.utf-8
16 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
17 |
18 | jobs:
19 | publish_to_pypi:
20 | runs-on: ubuntu-latest
21 |
22 | steps:
23 | - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
24 | - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
25 | - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
26 |
27 | - name: Checkout code
28 | uses: actions/checkout@v3
29 |
30 | - name: Build and publish to pypi
31 | uses: JRubics/poetry-publish@v1.16
32 | with:
33 | # These are only needed when using test.pypi
34 | #repository_name: testpypi
35 | #repository_url: https://test.pypi.org/legacy/
36 | pypi_token: ${{ secrets.PYPI_TOKEN }}
37 | ignore_dev_requirements: "yes"
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .venv/
2 | .ipynb_checkpoints/
3 | __pycache__/
4 |
5 | # C extensions
6 | *.so
7 |
8 | # Distribution / packaging
9 | .Python
10 | /docker/
11 | /neo4j/
12 | poetry.lock
13 | env/
14 | build/
15 | develop-eggs/
16 | dist/
17 | downloads/
18 | eggs/
19 | .eggs/
20 | lib/
21 | lib64/
22 | parts/
23 | sdist/
24 | var/
25 | wheels/
26 | *.egg-info/
27 | .installed.cfg
28 | *.egg
29 | *.py[cod]
30 | *$py.class
31 |
32 | # PyInstaller
33 | # Usually these files are written by a python script from a template
34 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
35 | *.manifest
36 | *.spec
37 |
38 | # Installer logs
39 | pip-log.txt
40 | pip-delete-this-directory.txt
41 |
42 | # Unit test / coverage reports
43 | htmlcov/
44 | .tox/
45 | .coverage
46 | .coverage.*
47 | .cache
48 | nosetests.xml
49 | coverage.xml
50 | *.cover
51 | .hypothesis/
52 | /tests/test_output/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 |
62 | # Flask stuff:
63 | instance/
64 | .webassets-cache
65 |
66 | # Scrapy stuff:
67 | .scrapy
68 |
69 | # Sphinx documentation
70 | docs/_build/
71 |
72 | # PyBuilder
73 | target/
74 |
75 | # Jupyter Notebook
76 | .ipynb_checkpoints
77 |
78 | # pyenv
79 | .python-version
80 |
81 | # celery beat schedule file
82 | celerybeat-schedule
83 |
84 | # SageMath parsed files
85 | *.sage.py
86 |
87 | # dotenv
88 | .env
89 |
90 | # virtualenv
91 | .venv
92 | venv/
93 | ENV/
94 |
95 | # Spyder project settings
96 | .spyderproject
97 | .spyproject
98 |
99 | # Rope project settings
100 | .ropeproject
101 |
102 | # mkdocs documentation
103 | /site
104 |
105 | # mypy
106 | .mypy_cache/
107 |
108 | # idea project
109 | .idea/
110 |
111 | # visual studio code
112 | .vscode/
113 |
114 | # pytest cache
115 | .pytest_cache
116 |
117 | generated/
118 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v5.0.0
4 | hooks:
5 | - id: check-yaml
6 | - id: double-quote-string-fixer
7 | - id: check-json
8 | - id: check-toml
9 | - id: check-xml
10 | - id: forbid-new-submodules
11 | - id: end-of-file-fixer
12 | - id: trailing-whitespace
13 | - id: check-merge-conflict
14 | - id: no-commit-to-branch # blocks main commits. To bypass do git commit --allow-empty
15 |
16 | - repo: https://github.com/PyCQA/isort
17 | rev: 6.0.1
18 | hooks:
19 | - id: isort
20 | name: isort
21 | description: "A Python utility / library to sort imports."
22 | entry: isort
23 | language: python
24 | types: [ python ]
25 |
--------------------------------------------------------------------------------
/CIMTool_plugin/builder.md:
--------------------------------------------------------------------------------
1 | # cimantic-graphs.xsl
2 |
3 | A custom builder shared with UCAIug by Pacific Northwest National Laboratory.
4 |
5 | ## Builder Description
6 |
7 | The **[cimantic-graphs.xsl](cimantic-graphs.xsl)** builder produces a specialized python dataclass schema to be used as input into the [CIMantic Graphs](https://github.com/PNNL-CIM-Tools/CIM-Graph/tree/develop) open source library for creating, parsing, and editing CIM power system models using in-memory knowledge graphs.
8 |
9 | This builder creates a hierarchical tree of CIM classes starting with top-level classes (such as `IdentifiedObject`) and then listing all classes inheriting from each level of dataclass. The dataclasses are used as the "single-source-of-truth" for auto-generated database queries and graph traversal using CIMantic Graphs.
10 |
11 | This builder is intended to be used in conjunction the [cimantic-graphs-init.xsl](cimantic-graphs-init.xsl) builder, which generates the python `__init__.py` file required for library imports to work correctly. The files generated by both builders should be placed in a new folder within the CIMantic Graphs library `./cimgraph/data_profile` subdirectory.
12 |
13 | Below is an example of a specialized python dataclass schema generated by this builder.
14 |
15 | ```PYTHON
16 | @dataclass
17 | class IdentifiedObject(Identity):
18 | '''
19 | This is a root class to provide common identification for all classes needing
20 | identification and naming attributes.
21 | '''
22 | mRID: Optional[ str ] = field(
23 | default = None,
24 | metadata = {
25 | 'type': 'Attribute',
26 | 'minOccurs': '0',
27 | 'maxOccurs': '1'
28 | })
29 | '''
30 | Master resource identifier issued by a model authority. The mRID is unique
31 | within an exchange context. Global uniqueness is easily achieved by using
32 | a UUID, as specified in RFC 4122, for the mRID. The use of UUID is strongly
33 | recommended.
34 | For CIMXML data files in RDF syntax conforming to IEC 61970-552, the mRID
35 | is mapped to rdf:ID or rdf:about attributes that identify CIM object elements.
36 | '''
37 |
38 | @dataclass
39 | class ACDCTerminal(IdentifiedObject):
40 | '''
41 | An electrical connection point (AC or DC) to a piece of conducting equipment.
42 | Terminals are connected at physical connection points called connectivity
43 | nodes.
44 | '''
45 | BusNameMarker: Optional[ str | BusNameMarker ] = field(
46 | default = None,
47 | metadata = {
48 | 'type': 'Association',
49 | 'minOccurs': '0',
50 | 'maxOccurs': '1',
51 | 'inverse': 'BusNameMarker.Terminal'
52 | })
53 | '''
54 | The bus name marker used to name the bus (topological node).
55 | '''
56 |
57 | Measurements: list[ str | Measurement ] = field(
58 | default_factory = list,
59 | metadata = {
60 | 'type': 'Association',
61 | 'minOccurs': '0',
62 | 'maxOccurs': 'unbounded',
63 | 'inverse': 'Measurement.Terminal'
64 | })
65 | '''
66 | Measurements associated with this terminal defining where the measurement
67 | is placed in the network topology. It may be used, for instance, to capture
68 | the sensor position, such as a voltage transformer (PT) at a busbar or
69 | a current transformer (CT) at the bar between a breaker and an isolator.
70 | '''
71 |
72 | @dataclass
73 | class Terminal(ACDCTerminal):
74 | '''
75 | An AC electrical connection point to a piece of conducting equipment. Terminals
76 | are connected at physical connection points called connectivity nodes.
77 | '''
78 | phases: Optional[ str | PhaseCode ] = field(
79 | default = None,
80 | metadata = {
81 | 'type': 'enumeration',
82 | 'minOccurs': '0',
83 | 'maxOccurs': '1'
84 | })
85 |
86 | ```
87 | The accompanying __init__.py file is used for library imports and contains a list of all classes within the profile:
88 |
89 | ```PYTHON
90 | from cimgraph.data_profile.cim17v40.cim17v40 import (
91 | ACDCTerminal,
92 | ACLineSegment,
93 | ACLineSegmentPhase,
94 | * * *
95 | )
96 | __all__ = [
97 | ACDCTerminal,
98 | ACLineSegment,
99 | ACLineSegmentPhase,
100 | * * *
101 | ]
102 |
103 | ```
104 |
105 |
106 | ## XSLT Version
107 |
108 | This builder is XSLT 1.0 compliant.
109 |
110 | ## Author
111 |
112 | Alex A. Anderson [@aandersn] on behalf of UCAIug.
113 |
114 | ## Submission Date
115 |
116 | 01-May-2024
117 |
118 | ## Builder NTE Configuration
119 |
120 |
121 |
122 | The below screenshot highlights the NTE (Name/Type/Extension) settings for the builder.
123 |
124 | The cimantic-graphs.xsl builder should be configured as a `TEXT` transform with the extension of `py`. The cimantic-graphs-init.xsl builder should be configured as a `TEXT` transform with the extensions of `__init__.py`.
125 |
126 | 
127 |
128 | 
129 |
130 | To generate the profile artifacts for use with CIMantic Graphs, select both builders in the Profile Summary tab and click Save. Next, create a new folder in the cimgraph/data_profile subdirectory with the same name as the new profile. Copy the generated .py files to the new folder and rename the second file to just `__init__.py`. It may also be necessary to rename the main python file to match the profile/namespace name in line 5 of the init file.
131 |
132 | 
133 |
134 | 
135 |
136 | The dataclasses can then be accessed by calling from
137 | ```PYTHON
138 | import cimgraph.data_profile.EndDeviceControls as cim
139 |
140 | new_device = cim.EndDevice()
141 | ```
142 |
143 |
144 | >*NOTE: CIMTool requires that file extensions be unique and will prevent you from entering an extension already assigned to a builder. This is because an artifact's name is derived by concatenating the base name of the CIMTool ```.owl``` profile with the file extension assigned to the builder. Therefore, a unique file extension must be assigned to each builder when imported. The file extension for a builder can be modified later from within the "Maintain XSLT Transform Builders" screen.*
145 |
146 |
147 | ## License
148 |
149 | This builder is released under a [BSD-3](https://github.com/PNNL-CIM-Tools/CIM-Graph/blob/main/LICENSE) license as part of the CIMantic Graphs library developed by PNNL.
150 |
151 | This software was created under a project sponsored by the U.S. Department of Energy’s Office of Electricity, an agency of the United States Government. Neither the United States Government nor the United States Department of Energy, nor Battelle, nor any of their employees, nor any jurisdiction or organization that has cooperated in the development of these materials, makes any warranty, express or implied, or assumes any legal liability or responsibility for the accuracy, completeness, or usefulness or any information, apparatus, product, software, or process disclosed, or represents that its use would not infringe privately owned rights.
152 |
153 | Reference herein to any specific commercial product, process, or service by trade name, trademark, manufacturer, or otherwise does not necessarily constitute or imply its endorsement, recommendation, or favoring by the United States Government or any agency thereof, or Battelle Memorial Institute. The views and opinions of authors expressed herein do not necessarily state or reflect those of the United States Government or any agency thereof.
154 |
155 | PACIFIC NORTHWEST NATIONAL LABORATORY
156 |
157 | operated by BATTELLE for the
158 |
159 | UNITED STATES DEPARTMENT OF ENERGY
160 |
161 | under Contract DE-AC05-76RL01830
162 |
--------------------------------------------------------------------------------
/CIMTool_plugin/cimantic-graphs-init.xsl:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 | Profile
13 | au.com.langdale.cimtool.generated
14 |
15 |
16 |
17 |
18 |
19 |
20 | - Annotated CIMantic Graphs data profile init file for
21 |
22 |
23 | - Generated by CIMTool http://cimtool.org
24 |
25 |
26 | - from cimgraph.data_profile.. import (
27 |
28 | - Identity,
29 |
30 |
31 |
32 |
33 |
34 |
35 | ,
36 |
37 |
38 | - )
39 |
40 |
41 | - __all__ = [
42 |
43 | - 'Identity',
44 |
45 |
46 |
47 |
48 | ''
49 | ,
50 |
51 |
52 | - ]
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/CIMTool_plugin/cimantic_graphs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PNNL-CIM-Tools/CIM-Graph/e448f4088b11b5a2d2f8317769bd939da7ea2f11/CIMTool_plugin/cimantic_graphs.png
--------------------------------------------------------------------------------
/CIMTool_plugin/cimantic_graphs_init.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PNNL-CIM-Tools/CIM-Graph/e448f4088b11b5a2d2f8317769bd939da7ea2f11/CIMTool_plugin/cimantic_graphs_init.png
--------------------------------------------------------------------------------
/CIMTool_plugin/init.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PNNL-CIM-Tools/CIM-Graph/e448f4088b11b5a2d2f8317769bd939da7ea2f11/CIMTool_plugin/init.png
--------------------------------------------------------------------------------
/CIMTool_plugin/profile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PNNL-CIM-Tools/CIM-Graph/e448f4088b11b5a2d2f8317769bd939da7ea2f11/CIMTool_plugin/profile.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2024, Battelle Memorial Institute; All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | 1. Redistributions of source code must retain the above copyright notice,
9 | this list of conditions and the following disclaimer.
10 |
11 | 2. Redistributions in binary form must reproduce the above copyright notice,
12 | this list of conditions and the following disclaimer in the documentation
13 | and/or other materials provided with the distribution.
14 |
15 | 3. Neither the name of the copyright holder nor the names of its contributors
16 | may be used to endorse or promote products derived from this software without
17 | specific prior written permission.
18 |
19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
23 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
29 | THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/cim_graph_structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PNNL-CIM-Tools/CIM-Graph/e448f4088b11b5a2d2f8317769bd939da7ea2f11/cim_graph_structure.png
--------------------------------------------------------------------------------
/cimgraph/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PNNL-CIM-Tools/CIM-Graph/e448f4088b11b5a2d2f8317769bd939da7ea2f11/cimgraph/__init__.py
--------------------------------------------------------------------------------
/cimgraph/data_profile/__init__.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 |
4 | class CIM_PROFILE(Enum):
5 | RC4_2021 = 'rc4_2021'
6 | CIMHUB_2023 = 'cimhub_2023'
7 | CIM17V40 = 'cim17v40'
8 | CIMHUB_UFLS = 'cimhub_ufls'
9 | UFLS = 'ufls'
10 |
--------------------------------------------------------------------------------
/cimgraph/data_profile/identity.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | from dataclasses import dataclass, field, is_dataclass
4 | from random import Random
5 | from typing import Optional
6 | from uuid import UUID, uuid4
7 |
8 | # from cimgraph.utils.timing import timing as time_func
9 |
10 | ARCHIVE_JSON_LD = True
11 |
12 | _log = logging.getLogger(__name__)
13 |
14 |
15 | class UUID_Meta():
16 | uuid:UUID = None
17 | uuid_str:str = ''
18 | uri_has_underscore:bool = False
19 | uri_is_capitalized:bool = False
20 | mrid_has_underscore:bool = False
21 | mrid_is_capitalized:bool = False
22 |
23 | # Create UUID from inconsistent mRIDs
24 | def generate_uuid(self, mRID:str = None, uri:str = None, name:str = None, seed:str = None) -> UUID:
25 | if seed is None:
26 | seed = ''
27 | invalid_mrid = True
28 |
29 | # If URI is specified, try creating from UUID from URI
30 | if uri is not None:
31 | # Handle inconsistent capitalization / underscores
32 | if uri.strip('_') != uri:
33 | self.uri_has_underscore = True
34 | if uri.lower() != uri:
35 | self.uri_is_capitalized = True
36 | try:
37 | self.uuid = UUID(uri.strip('_').lower())
38 | identifier = self.uuid
39 | invalid_mrid = False
40 | except:
41 | seed = seed + uri
42 | _log.warning(f'URI {uri} not a valid UUID, generating new UUID')
43 | mRID = str(uri)
44 | else:
45 | self.uuid = identifier
46 | invalid_mrid = False
47 |
48 | # If URI is specified, try creating from UUID from URI
49 | if mRID is not None:
50 | # Handle inconsistent capitalization / underscores
51 | if mRID.strip('_') != mRID:
52 | self.mrid_has_underscore = True
53 | if uri is None:
54 | self.uri_has_underscore = True
55 | if mRID.lower() != mRID:
56 | self.mrid_is_capitalized = True
57 | if uri is None:
58 | self.uri_is_capitalized = True
59 | # Create a new UUID based on the mRID if it does not exist
60 | if self.uuid is None:
61 | try:
62 | identifier = UUID(mRID.strip('_').lower())
63 | invalid_mrid = False
64 | except:
65 | seed = seed + mRID
66 | _log.warning(f'mRID {mRID} not a valid UUID, generating new UUID')
67 |
68 | # Otherwise, build UUID using unique name as a seed
69 | if invalid_mrid:
70 |
71 | if seed:
72 | randomGenerator = Random(seed)
73 | self.uuid = UUID(int=randomGenerator.getrandbits(128), version=4)
74 | else:
75 | self.uuid = uuid4()
76 |
77 | identifier = self.uuid
78 |
79 | self.uuid_str = str(identifier)
80 | return identifier
81 |
82 | @dataclass
83 | class Identity():
84 | '''
85 | This is the new root class from CIM 18 to provide common identification
86 | for all classes needing identification and naming attributes.
87 | IdentifiedObject is now a child class of Identity.
88 | mRID is superseded by Identity.identifier, which is typed to be a UUID.
89 | '''
90 | identifier: Optional[UUID] = field(
91 | default = None,
92 | metadata = {
93 | 'type': 'Attribute',
94 | 'minOccurs': '1',
95 | 'maxOccurs': '1'
96 | })
97 |
98 | # Backwards support for objects created with mRID
99 | # @time_func
100 | def __post_init__(self) -> None:
101 | # Validate if pre-specified
102 | if self.identifier is not None:
103 | id = str(self.identifier)
104 | # Check if Identity.identifier is an invalid UUID
105 | if not isinstance(self.identifier,UUID):
106 | _log.warning(f'Identifier {self.identifier} must be a UUID object, generating new UUID')
107 | self.identifier = None
108 | # If object inherits from IdentifiedObject, create uuid from mRID
109 | if 'mRID' in self.__dataclass_fields__:
110 | self.uuid(uri=id, mRID=self.mRID, name=self.name)
111 | else:
112 | self.uuid(uri=id)
113 | # Otherwise, create a new UUID
114 | else:
115 | if 'mRID' in self.__dataclass_fields__:
116 | self.uuid(mRID=self.mRID, name=self.name)
117 | else:
118 | self.uuid()
119 |
120 |
121 |
122 | # Override python string for printing with JSON representation
123 | # @time_func
124 | def __str__(self, show_mRID:bool = False, show_empty:bool = False,
125 | use_names:bool = False) -> json:
126 | # Create JSON-LD dump with repr and all attributes
127 | dump = dict(json.loads(self.__repr__()) | self.__dict__)
128 | del dump['__uuid__']
129 | del dump['__json_ld__']
130 | attribute_list = list(self.__dataclass_fields__.keys())
131 | for attribute in attribute_list:
132 | # Delete attributes from print that are empty
133 | if dump[attribute] is None or dump[attribute] == []:
134 | if not show_empty:
135 | del dump[attribute]
136 | # If a dataclass, replace with custom repr
137 | elif is_dataclass(dump[attribute]):
138 | if use_names and 'name' in dump[attribute].__dataclass_fields__:
139 | dump[attribute] = dump[attribute].name
140 | else:
141 | dump[attribute] = dump[attribute].__repr__()
142 | # Reformat all attributes as string for JSON
143 | elif not isinstance(dump[attribute], str):
144 | dump[attribute] = str(dump[attribute])
145 | # Remove duplicate identifier and mRID from JSON-LD
146 | if not show_mRID:
147 | del dump['identifier']
148 | if 'mRID' in dump:
149 | del dump['mRID']
150 | # Fix python ' vs JSON "
151 | dump = json.dumps(dump)
152 | dump = str(dump).replace('\\\"','\"' )
153 | dump = str(dump).replace('\"[','[' )
154 | dump = str(dump).replace(']\"',']' )
155 | dump = str(dump).replace('\"{','{' )
156 | dump = str(dump).replace('}\"','}' )
157 | return dump
158 |
159 | # Override python __repr__ method with JSON-LD representation
160 | # This is needed to avoid infinite loops in object previews
161 | # @time_func
162 | def __repr__(self) -> str:
163 | if ARCHIVE_JSON_LD:
164 | return self.__json_ld__
165 | else:
166 | return json.dumps({'@id': f'{str(self.identifier)}', '@type': f'{self.__class__.__name__}'})
167 |
168 | # Add indentation of json for pretty print
169 | def pprint(self, print_mRID:bool=False) -> None:
170 | print(json.dumps(json.loads(self.__str__(print_mRID)), indent=4))
171 |
172 | # Create UUID from inconsistent mRIDs
173 | # @time_func
174 | def uuid(self, mRID:str = None, uri:str = None, name:str = None, seed:str = None) -> UUID:
175 | self.__uuid__ = UUID_Meta()
176 | if seed is None:
177 | seed = ''
178 | if name is not None:
179 | seed = seed + f'{self.__class__.__name__}:{name}'
180 |
181 | self.identifier = self.__uuid__.generate_uuid(mRID=mRID, uri=uri, name=name, seed=seed)
182 |
183 | # Write mRID string for backwards compatibility
184 | if 'mRID' in self.__dataclass_fields__:
185 | if mRID is not None:
186 | self.mRID = mRID
187 | else:
188 | self.mRID = str(self.identifier)
189 | if name is not None:
190 | self.name = name
191 |
192 | if ARCHIVE_JSON_LD:
193 | self.__json_ld__ = json.dumps({'@id': f'{str(self.identifier)}', '@type': f'{self.__class__.__name__}'})
194 |
195 |
196 | # Method to reconstitute URI from UUID
197 | def uri(self) -> str:
198 | uri = str(self.identifier)
199 | try:
200 | if self.__uuid__.uri_is_capitalized:
201 | uri = uri.upper()
202 | if self.__uuid__.uri_has_underscore:
203 | uri = '_' + uri
204 | except:
205 | pass
206 | return uri
207 |
--------------------------------------------------------------------------------
/cimgraph/data_profile/known_problem_classes.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from dataclasses import dataclass, field
4 |
5 |
6 | @dataclass
7 | class ClassesWithoutMRID:
8 | def __init__(self):
9 | self.classes=[
10 | 'IEC61968CIMVersion',
11 | 'WirePhaseInfo',
12 | 'CalculationMethodOrder',
13 | 'FieldDispatchStep',
14 | 'PositionPoint',
15 | 'ScheduledEventData',
16 | 'UserAttribute',
17 | 'AccountNotification',
18 | 'CustomerNotification',
19 | 'DERCurveData',
20 | 'DERFunction',
21 | 'DERMonitorableParameter',
22 | 'DispatchablePowerCapability',
23 | 'DispatchSchedule',
24 | 'EndDeviceAction',
25 | 'EndDeviceEventDetail',
26 | 'IntervalBlock',
27 | 'PanDemandResponse',
28 | 'PanDisplay',
29 | 'PanPricing',
30 | 'PanPricingDetail',
31 | 'PendingCalculation',
32 | 'ReadingQuality',
33 | 'EstimatedRestorationTime',
34 | 'OutageArea',
35 | 'SwitchingStep',
36 | 'SwitchingStepGroup',
37 | 'Card',
38 | 'Cheque',
39 | 'ConsumptionTariffInterval',
40 | 'TimeTariffInterval',
41 | 'RepairItem',
42 | 'IEC61970CIMVersion',
43 | 'CurveData',
44 | 'IrregularTimePoint',
45 | 'Name',
46 | 'NameType',
47 | 'NameTypeAuthority',
48 | 'OperatingShare',
49 | 'RegularTimePoint',
50 | 'DiagramObjectGluePoint',
51 | 'DiagramObjectPoint',
52 | 'PublicX509Certificate',
53 | 'MeasurementValueQuality',
54 | 'Quality61850',
55 | 'BranchGroupTerminal',
56 | 'StateVariable',
57 | 'SvInjection',
58 | 'SvPowerFlow',
59 | 'SvShuntCompensatorSections',
60 | 'SvStatus',
61 | 'SvSwitch',
62 | 'SvTapStep',
63 | 'SvVoltage',
64 | 'NonlinearShuntCompensatorPhasePoint',
65 | 'NonlinearShuntCompensatorPoint',
66 | 'PhaseImpedanceData',
67 | 'PhaseTapChangerTablePoint',
68 | 'RatioTapChangerTablePoint',
69 | 'TapChangerTablePoint',
70 | 'ProprietaryParameterDynamics',
71 | 'IEC62325CIMVersion',
72 | 'AtmosphericPhenomenon',
73 | 'CloudCondition',
74 | 'Cyclone',
75 | 'Earthquake',
76 | 'EnvironmentalLocationType',
77 | 'EnvironmentalPhenomenon',
78 | 'Fire',
79 | 'Flood',
80 | 'GeosphericPhenomenon',
81 | 'Hurricane',
82 | 'HydrosphericPhenomenon',
83 | 'Landslide',
84 | 'LightningStrike',
85 | 'MagneticStorm',
86 | 'ReportingCapability',
87 | 'SpacePhenomenon',
88 | 'Tornado',
89 | 'TropicalCycloneAustralia',
90 | 'Tsunami',
91 | 'VolcanicAshCloud',
92 | 'Whirlpool',
93 | 'ResourceCapacity',
94 | 'AceTariffType',
95 | 'AttributeInstanceComponent',
96 | 'ConstraintDuration',
97 | 'DateAndOrTime',
98 | 'FlowDirection',
99 | 'MarketObjectStatus',
100 | 'Period',
101 | 'Point',
102 | 'Price',
103 | 'Quantity',
104 | 'Reason',
105 | 'Unit',
106 | 'MarketInvoice',
107 | 'MarketInvoiceLineItem',
108 | 'MarketLedger',
109 | 'MarketLedgerEntry',
110 | 'MktUserAttribute',
111 | 'PlannedMarket',
112 | 'AllocationResult',
113 | 'AllocationResultValues',
114 | 'AuxiliaryCost',
115 | 'AuxiliaryObject',
116 | 'AuxiliaryValues',
117 | 'ExpectedEnergy',
118 | 'ExpectedEnergyValues',
119 | 'FiveMinAuxiliaryData',
120 | 'TenMinAuxiliaryData',
121 | 'TradingHubPrice',
122 | 'TradingHubValues',
123 | 'AnalogMeasurementValueQuality',
124 | 'ASRequirements',
125 | 'BranchEndFlow',
126 | 'ControlAreaSolutionData',
127 | 'DefaultBidCurveData',
128 | 'DiscreteMeasurementValueQuality',
129 | 'DistributionFactorSet',
130 | 'GenDistributionFactor',
131 | 'GeneratingUnitDynamicValues',
132 | 'InterchangeETCData',
133 | 'LoadDistributionFactor',
134 | 'MWLimitSchedule',
135 | 'ProfileData',
136 | 'SCADAInformation',
137 | 'ShuntCompensatorDynamicData',
138 | 'SwitchStatus',
139 | 'SysLoadDistributionFactor',
140 | 'TapChangerDynamicData',
141 | 'TransferInterfaceSolution',
142 | 'TransmissionCapacity',
143 | 'TransmissionInterfaceRightEntitlement',
144 | 'TransmissionReservation',
145 | 'TREntitlement',
146 | 'ChargeProfileData',
147 | 'Commitments',
148 | 'CommodityPrice',
149 | 'DopInstruction',
150 | 'DotInstruction',
151 | 'ExPostLossResults',
152 | 'ExPostMarketRegionResults',
153 | 'ExPostPricingResults',
154 | 'ExPostResourceResults',
155 | 'GeneralClearingResults',
156 | 'Instructions',
157 | 'LoadFollowingOperatorInput',
158 | 'LossClearingResults',
159 | 'MarketRegionResults',
160 | 'MarketResults',
161 | 'MitigatedBidSegment',
162 | 'MPMResourceStatus',
163 | 'MPMTestResults',
164 | 'PnodeResults',
165 | 'PriceDescriptor',
166 | 'ResourceAwardInstruction',
167 | 'ResourceDeploymentStatus',
168 | 'ResourceDispatchResults',
169 | 'ResourceLoadFollowingInst',
170 | 'RMRDetermination',
171 | 'RUCAwardInstruction',
172 | 'SelfScheduleBreakdown',
173 | 'ActionRequest',
174 | 'AttributeProperty',
175 | 'BidDistributionFactor',
176 | 'EnergyPriceCurve',
177 | 'InterTieDispatchResponse',
178 | 'LoadFollowingInst',
179 | 'TradeProduct',
180 | 'BidPriceCap',
181 | 'CombinedCycleTransitionState',
182 | 'ContractDistributionFactor',
183 | 'ControlAreaDesignation',
184 | 'FlowgateRelief',
185 | 'FlowgateValue',
186 | 'GasPrice',
187 | 'LoadRatio',
188 | 'MPMTestThreshold',
189 | 'OilPrice',
190 | 'PnodeDistributionFactor',
191 | 'ResourceCertification',
192 | 'ResourceStartupCost',
193 | 'ResourceVerifiableCosts',
194 | 'SchedulingCoordinatorUser',
195 | 'SubstitutionResourceList',
196 | 'PackageDependenciesCIMVersion',
197 | 'WorkTimeScheduleKind' ]
198 | self.units = [
199 | 'WPerHz',
200 | 'g'
201 | ]
202 |
203 | @dataclass
204 | class ClassesWithManytoMany:
205 | def __init__(self):
206 | self.attributes=[
207 | 'ConnectivityNode.OperationalLimitSet',
208 | 'ACDCTerminal.OperationalLimitSet',
209 | 'ShortCircuitTest.GroundedEnds',
210 | 'TransformerMeshImpedance.ToTransformerEnd',
211 | 'Equipment.AdditionalEquipmentContainer'
212 | ]
213 |
--------------------------------------------------------------------------------
/cimgraph/data_profile/ufls/__init__.py:
--------------------------------------------------------------------------------
1 | '''
2 | Annotated CIMantic Graphs data profile init file for ufls
3 | Generated by CIMTool http://cimtool.org
4 | '''
5 | from cimgraph.data_profile.ufls.ufls import (PU, ACDCTerminal, ActivePower, Analog, ApparentPower,
6 | BaseVoltage, BatteryStateKind, BatteryUnit, Breaker,
7 | ConductingEquipment, ConnectivityNode,
8 | ConnectivityNodeContainer, ConverterControlModeKind,
9 | CurrentFlow, Discrete, DistributionArea,
10 | EnergyConnection, EnergyConsumer, Equipment,
11 | EquipmentContainer, Feeder, FeederArea, Frequency,
12 | FrequencyProtectionFunctionBlock, FunctionBlock,
13 | FunctionInputVariable, FunctionOutputVariable,
14 | GeographicalRegion, IdentifiedObject, Identity,
15 | Measurement, Microgrid, PerCent, PhaseCode,
16 | PhotovoltaicUnit, PowerCutZone,
17 | PowerElectronicsConnection, PowerElectronicsUnit,
18 | PowerSystemResource, ProtectedSwitch,
19 | ProtectionEquipment, ProtectionFunctionBlock,
20 | ProtectionSettingsGroup, ReactivePower, RealEnergy,
21 | RegulatingCondEq, SchedulingArea, SecondaryArea,
22 | Seconds, SubGeographicalRegion, SubSchedulingArea,
23 | Substation, Switch, SwitchArea, Terminal,
24 | TopologicalNode,
25 | UnderFrequencyProtectionFunctionBlock, UnitMultiplier,
26 | UnitSymbol, Voltage, WideAreaProtectionFunctionBlock)
27 |
28 | __all__ = [
29 | 'Identity',
30 | 'ACDCTerminal' ,
31 | 'Analog' ,
32 | 'AreaConfiguration' ,
33 | 'BaseVoltage' ,
34 | 'BatteryStateKind' ,
35 | 'BatteryUnit' ,
36 | 'Breaker' ,
37 | 'ConductingEquipment' ,
38 | 'ConnectivityNode' ,
39 | 'ConnectivityNodeContainer' ,
40 | 'ConverterControlModeKind' ,
41 | 'Discrete' ,
42 | 'DistributionArea' ,
43 | 'EnergyConnection' ,
44 | 'EnergyConsumer' ,
45 | 'Equipment' ,
46 | 'EquipmentContainer' ,
47 | 'Feeder' ,
48 | 'FeederArea' ,
49 | 'FrequencyProtectionFunctionBlock' ,
50 | 'FunctionBlock' ,
51 | 'FunctionInputVariable' ,
52 | 'FunctionOutputVariable' ,
53 | 'GeographicalRegion' ,
54 | 'IdentifiedObject' ,
55 | 'Measurement' ,
56 | 'Microgrid' ,
57 | 'PhaseCode' ,
58 | 'PhotovoltaicUnit' ,
59 | 'PowerCutZone' ,
60 | 'PowerElectronicsConnection' ,
61 | 'PowerElectronicsUnit' ,
62 | 'PowerSystemResource' ,
63 | 'ProtectedSwitch' ,
64 | 'ProtectionEquipment' ,
65 | 'ProtectionFunctionBlock' ,
66 | 'ProtectionSettingsGroup' ,
67 | 'RegulatingCondEq' ,
68 | 'SchedulingArea' ,
69 | 'SecondaryArea' ,
70 | 'SubGeographicalRegion' ,
71 | 'SubSchedulingArea' ,
72 | 'Substation' ,
73 | 'Switch' ,
74 | 'SwitchArea' ,
75 | 'Terminal' ,
76 | 'TopologicalNode' ,
77 | 'UnderFrequencyProtectionFunctionBlock' ,
78 | 'UnitMultiplier' ,
79 | 'UnitSymbol' ,
80 | 'WideAreaProtectionFunctionBlock' ,
81 | 'ReactivePower' ,
82 | 'Seconds' ,
83 | 'Voltage' ,
84 | 'PerCent' ,
85 | 'ApparentPower' ,
86 | 'RealEnergy' ,
87 | 'CurrentFlow' ,
88 | 'PU' ,
89 | 'Frequency' ,
90 | 'ActivePower'
91 | ]
92 |
--------------------------------------------------------------------------------
/cimgraph/data_profile/units/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PNNL-CIM-Tools/CIM-Graph/e448f4088b11b5a2d2f8317769bd939da7ea2f11/cimgraph/data_profile/units/__init__.py
--------------------------------------------------------------------------------
/cimgraph/data_profile/units/cim_units/units.txt:
--------------------------------------------------------------------------------
1 | imaginary = [] = i = reactive
2 | A2 = amp * amp = A²
3 | A2h = amp * amp * hour = A²h
4 | A2s = amp * amp * second = A²s
5 | APerA = amp / amp = A/A
6 | APerm = amp / meter = A/m
7 | As = amp * second = A·s
8 | CPerkg = coulomb / kilogram = C/m
9 | CPerm2 = coulomb / meter / meter = C/m²
10 | CPerm3 = coulomb / meter / meter / meter = C/m³
11 | FPerm = farad / meter = F/m
12 | GyPers = gray / second = Gy/s
13 | HPerm = henry / meter = H/m
14 | HzPerHz = hertz / hertz = Hz/Hz
15 | HzPers = herts / second = Hz/s
16 | JPerK = joule / kelvin = J/K
17 | JPerkg = joule / kilogram = J/kg
18 | JPerkgK = joule / kilogram / kelvin = J/kg·K
19 | JPerm2 = joule / meter / meter = J/m²
20 | JPerm3 = joule / meter / meter = J/m³
21 | JPermol = joule / mol = J/mol
22 | JPermolK = joule / mol / kelvin = J/mol·K
23 | JPers = joule / second = J/s
24 | KPers = kelvin / second = K/s
25 | NPerm = newton / meter = N/m
26 | Nm = newton * meter = N·m
27 | PaPers = pascal / second = Pa/s
28 | Pas = pascal * second = Pas
29 | Q = imaginary * watt = Q
30 | Qh = imaginary * watt * hour = Qh
31 | SPerm = siemens / meter = S/m
32 | V2 = volt * volt = V²
33 | V2h = volt * volt * hour = V²h
34 | VAh = volt * amp * hour = VA·h
35 | VAr = watt * imaginary = VAr
36 | VArh = watt * hour * imaginary = VAr·h
37 | VPerHz = volt / hertz = V/Hz
38 | VPerV = volt / volt = V/V
39 | VPerVA = volt / volt / amp = V/VA
40 | VPerVAr = volt / volt / amp / imaginary = V/VAr
41 | VPerm = volt / meter = V/m
42 | Vh = volt * hour = Vh
43 | Vs = volt * second = Vs
44 | WPerA = watt / amp = W/A
45 | WPerHz = watt / hertz = W/Hz
46 | WPerW = watt / watt = W/W
47 | WPerm2 = watt / meter / meter = W/m²
48 | WPerm2sr = watt / meter / meter / steradian = W/m²sr
49 | WPermK = watt / meter / kelvin = W/m·K
50 | WPers = watt / second = W/s
51 | WPersr = watt / steradian = W/sr
52 | anglemin = arcminute = arcmin
53 | anglesec = arcsecond = arcsec
54 | charPers = baud = bps
55 | character = bit = bit
56 | cosPhi = [] = cosφ
57 | ft3 = feet * feet * feet = ft³
58 | gPerg = gram / gram = g/g
59 | katPerm3 = katal / meter / meter / meter = kat/m³
60 | kgPerJ = kilogram / joule = kg/J
61 | kgPerm3 = kilogram / meter / meter / meter = kg/m³
62 | kgm = kilogram * meter = kg·m
63 | kgm2 = kilogram * meter * meter = kg·m²
64 | kn = knot = kt
65 | lPerh = litre / hour = L/h
66 | lPerl = litre / litre = L/L
67 | lPers = litre / meter = L/m
68 | m2 = meter * meter = m²
69 | m2Pers = meter * meter / second = m²/s
70 | m3 = meter * meter * meter = m³
71 | m3Compensated = meter * meter * meter = m³_comp
72 | m3Perh = meter * meter * meter / hour = m³/h
73 | m3Perkg = meter * meter * meter / kilogram = m³/kg
74 | m3Pers = meter * meter * meter / second = m³/s
75 | m3Uncompensated = meter * meter * meter = m³_uncomp
76 | mPerm3 = meter / meter / meter / meter = m/m³
77 | mPers = meter / second = m/s
78 | mPers2 = meter / second / second = m/s²
79 | molPerkg = mol / kilogram = mol/kg
80 | molPerm3 = mol / meter / meter / meter = mol/m³
81 | molPermol = mol / mol = mol/mol
82 | none = [] = _
83 | ohmPerm = ohm / meter = ohm/m
84 | ohmm = ohm * meter = ohm·m
85 | onePerHz = 1 / hertz = Hz_1
86 | onePerm = 1 / meter = m_1
87 | radPers = radian / second = rad/s
88 | radPers2 = radian / second / second = rad/s²
89 | rev = revolution
90 | rotPers = hertz = Hz
91 | sPers = second / second = s/s
92 |
--------------------------------------------------------------------------------
/cimgraph/data_profile/xsd_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/cimgraph/databases/blazegraph/__init__.py:
--------------------------------------------------------------------------------
1 | from cimgraph.databases.blazegraph.blazegraph import BlazegraphConnection
2 |
--------------------------------------------------------------------------------
/cimgraph/databases/fileparsers/__init__.py:
--------------------------------------------------------------------------------
1 | from cimgraph.databases.fileparsers.xml_parser import XMLFile
2 |
--------------------------------------------------------------------------------
/cimgraph/databases/graphdb/__init__.py:
--------------------------------------------------------------------------------
1 | from cimgraph.databases.graphdb.graphdb import GraphDBConnection
2 |
--------------------------------------------------------------------------------
/cimgraph/databases/gridappsd/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | This module provides functionality to interact with the GridAPPSD platform.
3 |
4 | It attempts to import the GridAPPSD package and sets an environment variable
5 | 'CIMGRAPH_HAS_GRIDAPPSD' to '1' and HAS_GRIDAPPSD flag set to True, if the import is
6 | successful. If the import fails, it sets a flag 'HAS_GRIDAPPSD' to False.
7 |
8 | The module also provides a function 'get_topology_response' which sends a
9 | request to the GridAPPSD platform to get the topology of a specific feeder.
10 |
11 | Functions:
12 | ----------
13 | get_topology_response(feeder_mrid: str, gapps: GridAPPSD) -> Dict:
14 | Sends a request to the GridAPPSD platform to get the topology of a specific feeder.
15 |
16 | Parameters:
17 | -----------
18 | feeder_mrid : str
19 | The mRID of the feeder for which the topology is requested.
20 | gapps : GridAPPSD
21 | An instance of the GridAPPSD class.
22 |
23 | Returns:
24 | --------
25 | dict
26 | A dictionary containing the response from the GridAPPSD platform.
27 | """
28 | from __future__ import annotations
29 |
30 | from gridappsd import GridAPPSD
31 |
32 | from cimgraph.databases.gridappsd.gridappsd import GridappsdConnection
33 |
34 |
35 | def get_topology_response(feeder_mrid: str, gapps: GridAPPSD) -> dict:
36 | assert feeder_mrid is not None
37 |
38 | topic = 'goss.gridappsd.request.data.topology'
39 | message = {'requestType': 'GET_SWITCH_AREAS', 'modelID': feeder_mrid, 'resultFormat': 'JSON'}
40 |
41 | topo_response = gapps.get_response(topic=topic, message=message, timeout=30)
42 | return topo_response
43 |
--------------------------------------------------------------------------------
/cimgraph/databases/mysql/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PNNL-CIM-Tools/CIM-Graph/e448f4088b11b5a2d2f8317769bd939da7ea2f11/cimgraph/databases/mysql/__init__.py
--------------------------------------------------------------------------------
/cimgraph/databases/neo4j/__init__.py:
--------------------------------------------------------------------------------
1 | from cimgraph.databases.neo4j.neo4j import Neo4jConnection
2 |
--------------------------------------------------------------------------------
/cimgraph/databases/piwebapi/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PNNL-CIM-Tools/CIM-Graph/e448f4088b11b5a2d2f8317769bd939da7ea2f11/cimgraph/databases/piwebapi/__init__.py
--------------------------------------------------------------------------------
/cimgraph/databases/rdflib/__init__.py:
--------------------------------------------------------------------------------
1 | from cimgraph.databases.rdflib.rdflib import RDFlibConnection
2 |
--------------------------------------------------------------------------------
/cimgraph/databases/rdflib/rdflib.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import importlib
4 | import json
5 | import logging
6 | import math
7 | import os
8 | from uuid import UUID
9 |
10 | from rdflib import Graph, Namespace, URIRef
11 | from rdflib.namespace import RDF
12 |
13 | import cimgraph.queries.sparql as sparql
14 | from cimgraph.databases import (ConnectionInterface, QueryResponse, get_cim_profile,
15 | get_iec61970_301, get_namespace)
16 |
17 | _log = logging.getLogger(__name__)
18 |
19 |
20 | class RDFlibConnection(ConnectionInterface):
21 |
22 | def __init__(self, filename:str=None, use_oxigraph:bool=True):
23 | self.cim_profile, self.cim = get_cim_profile()
24 | self.namespace = get_namespace()
25 | self.iec61970_301 = get_iec61970_301()
26 | self.filename = filename
27 | self.libgraph = None
28 | self.use_oxigraph = use_oxigraph
29 |
30 | def connect(self):
31 | if not self.libgraph:
32 | if self.use_oxigraph:
33 | self.libgraph = Graph(store='Oxigraph')
34 | else:
35 | self.libgraph = Graph()
36 |
37 | if self.filename is not None:
38 | try:
39 | self.libgraph.parse(self.filename)
40 | self.libgraph.bind('cim', Namespace(self.namespace))
41 | self.libgraph.bind('rdf', RDF)
42 | except:
43 | _log.warning(f'File {self.filename} not found. Defaulting to empty network graph')
44 | self.filename = None
45 |
46 | def disconnect(self):
47 | self.libgraph = None
48 |
49 | def execute(self, query_message: str) -> QueryResponse:
50 | self.connect()
51 | query_output = self.libgraph.query(query_message)
52 | return query_output
53 |
54 | def get_object(self, mRID:str, graph = None) -> object:
55 | if graph is None:
56 | graph = {}
57 | sparql_message = sparql.get_object_sparql(mRID)
58 | query_output = self.execute(sparql_message)
59 | obj = None
60 | for result in query_output:
61 | uri = result['identifier']['value']
62 | obj_class = result['obj_class']['value']
63 | class_type = getattr(self.cim, obj_class)
64 | obj = self.create_object(graph, class_type, uri)
65 | return obj
66 |
67 | def create_new_graph(self, container: object) -> dict[type, dict[UUID, object]]:
68 | graph = {}
69 | self.add_to_graph(graph=graph, obj=container)
70 | # Get all nodes, terminal, and equipment by
71 | sparql_message = sparql.get_all_nodes_from_container(container)
72 | query_output = self.execute(sparql_message)
73 | graph = self.parse_node_query(graph, query_output)
74 | return graph
75 |
76 | def parse_node_query(self, graph: dict, query_output: dict) -> dict[type, dict[UUID, object]]:
77 |
78 | for result in query_output:
79 | # Parse query results
80 | node_mrid = str(result.ConnectivityNode.value)
81 | term_mrid = str(result.Terminal.value)
82 | eq = json.loads(result.Equipment.value)
83 | eq_id = eq['@id']
84 | eq_class = eq['@type']
85 |
86 | if eq_class in self.cim.__all__:
87 | eq_class = getattr(self.cim, eq_class)
88 | equipment = self.create_object(graph, eq_class, eq_id)
89 |
90 | else:
91 | _log.warning(f'object class missing from data profile: {eq_class}')
92 | continue
93 |
94 | if node_mrid != 'None':
95 | # Add each object to graph
96 | node = self.create_object(graph, self.cim.ConnectivityNode, node_mrid)
97 | terminal = self.create_object(graph, self.cim.Terminal, term_mrid)
98 | # Associate the node and equipment with the terminal
99 | if terminal not in equipment.Terminals:
100 | equipment.Terminals.append(terminal)
101 | if terminal not in node.Terminals:
102 | node.Terminals.append(terminal)
103 | # Associate the terminal with the equipment and node
104 | setattr(terminal, 'ConnectivityNode', node)
105 | setattr(terminal, 'ConductingEquipment', equipment)
106 |
107 | return graph
108 |
109 | def get_edges_query(self, graph: dict[type, dict[UUID, object]],
110 | cim_class: type) -> str:
111 |
112 | eq_mrids = list(graph[cim_class].keys())[0:100]
113 | sparql_message = sparql.get_all_edges_sparql(graph, cim_class, eq_mrids)
114 |
115 | return sparql_message
116 |
117 | def get_all_edges(self, graph: dict[type, dict[UUID, object]], cim_class: type) -> None:
118 | uuid_list = list(graph[cim_class].keys())
119 | for index in range(math.ceil(len(uuid_list) / 100)):
120 | eq_mrids = uuid_list[index * 100:(index + 1) * 100]
121 | #generate SPARQL message from correct queries>sparql python script based on class name
122 | sparql_message = sparql.get_all_edges_sparql(graph, cim_class, eq_mrids)
123 | #execute sparql query
124 | query_output = self.execute(sparql_message)
125 | self.edge_query_parser(query_output, graph, cim_class)
126 |
127 | def get_all_attributes(self, graph: dict[type, dict[UUID, object]], cim_class: type) -> None:
128 | mrid_list = list(graph[cim_class].keys())
129 |
130 | for index in range(math.ceil(len(mrid_list) / 100)):
131 | eq_mrids = mrid_list[index * 100:(index + 1) * 100]
132 | #generate SPARQL message from correct loaders>sparql python script based on class name
133 | sparql_message = sparql.get_all_attributes_sparql(graph, cim_class, eq_mrids)
134 | #execute sparql query
135 | query_output = self.execute(sparql_message)
136 | self.edge_query_parser(query_output, graph, cim_class, expand_graph = False)
137 |
138 | def edge_query_parser(self, query_output: QueryResponse,
139 | graph: dict[type, dict[UUID, object]],
140 | cim_class: type, expand_graph = True) -> None:
141 |
142 | for result in query_output:
143 | if result.attribute is not None: #skip 'type' and other single attributes
144 | if 'type' not in result.attribute.value:
145 | is_association = False
146 | is_enumeration = False
147 | if result.uri() is not None: #get mRID
148 | mRID = str(result.uri())
149 | else:
150 | iri = str(result.eq)
151 | if self.iec61970_301 > 7:
152 | mRID = iri.split('uuid:')[1]
153 | else:
154 | mRID = iri.split('rdf:id:')[1]
155 | identifier = UUID(mRID.strip('_').lower())
156 | attr_uri = result.attr
157 | attr = str(result.attr).split(self.namespace)[1]
158 | attribute = attr.split('.') #split edge attribute
159 | value = str(result.val) #get edge value
160 |
161 | if self.namespace in value: #check if enumeration
162 | enum_text = value.split(self.namespace)[1]
163 | enum_text = enum_text.split('>')[0]
164 | enum_class = enum_text.split('.')[0]
165 | enum_value = enum_text.split('.')[1]
166 | is_enumeration = True
167 |
168 | if result.edge_class is not None: #check if association
169 | is_association = True
170 | # edge = json.loads(result.edge)
171 | # edge_mRID = edge['@id']
172 | # edge_class = edge['@type']
173 | edge_class = str(result.edge_class)
174 | if result.edge_mRID is not None:
175 | edge_mRID = str(result.edge_mRID)
176 | else:
177 | if self.iec61970_301 > 7:
178 | edge_mRID = value.split('uuid:')[1]
179 | else:
180 | edge_mRID = value.split('#')[1]
181 | if edge_class in self.cim.__all__:
182 | edge_class = getattr(self.cim, edge_class)
183 | else:
184 | _log.warning(f'unknown class {edge_class}')
185 | continue
186 |
187 | if expand_graph:
188 | self.create_edge(graph, cim_class, identifier, attribute, edge_class, edge_mRID)
189 | else:
190 | self.create_value(graph, cim_class, identifier, attribute, value)
191 |
192 |
193 | elif is_enumeration:
194 | edge_enum = getattr(self.cim, enum_class)(enum_value)
195 | association = self.check_attribute(cim_class, attribute)
196 | if association is not None:
197 | setattr(graph[cim_class][identifier], association, edge_enum)
198 | else:
199 | association = self.check_attribute(cim_class, attribute)
200 | if association is not None:
201 | self.create_value(graph, cim_class, identifier, attribute, value)
202 |
203 |
204 | # return obj
205 |
206 | def upload(self, graph):
207 | pass
208 |
209 |
210 | def get_from_triple(self, subject, predicate, graph = ...):
211 | pass
212 |
213 | def create_distributed_graph(self, area, graph = ...):
214 | pass
215 |
--------------------------------------------------------------------------------
/cimgraph/models/.ipynb_checkpoints/Untitled-checkpoint.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [],
3 | "metadata": {},
4 | "nbformat": 4,
5 | "nbformat_minor": 5
6 | }
7 |
--------------------------------------------------------------------------------
/cimgraph/models/__init__.py:
--------------------------------------------------------------------------------
1 | from cimgraph.models.bus_branch_model import BusBranchModel
2 | from cimgraph.models.distributed_area import DistributedArea
3 | from cimgraph.models.feeder_model import FeederModel
4 | from cimgraph.models.graph_model import GraphModel
5 | from cimgraph.models.node_breaker_model import NodeBreakerModel
6 |
--------------------------------------------------------------------------------
/cimgraph/models/bus_branch_model.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import importlib
4 | import json
5 | import logging
6 | from dataclasses import dataclass, field
7 |
8 | from cimgraph.models.graph_model import GraphModel
9 |
10 | _log = logging.getLogger(__name__)
11 |
12 |
13 | @dataclass
14 | class BusBranchModel(GraphModel):
15 |
16 | distributed_hierarchy: list[type] = field(default_factory=list)
17 |
18 | def __post_init__(self):
19 | self.cim_profile = self.connection.cim_profile
20 | self.cim = importlib.import_module('cimgraph.data_profile.' + self.cim_profile)
21 |
22 | if self.connection is not None:
23 | if self.distributed:
24 | self.initialize_distributed_model(self.container)
25 | else:
26 | self.initialize_centralized_model(self.container)
27 | else:
28 | _log.error('A ConnectionInterface must be specified')
29 |
30 | def initialize_centralized_model(self, container) -> None:
31 | self.graph = self.connection.create_new_graph(container)
32 |
33 | def initialize_distributed_model(self, container) -> None:
34 | pass
35 | # if len(self.distributed_hierarchy) > 0:
36 | # for container_class in self.distributed_hierarchy:
37 | # container_type = container_class.__class__.__name__
38 | # setattr(self, container_type + 's', [])
39 | # #TODO: create subclasses based on pre-defined topology
40 | # else:
41 | # centralized_graph = self.connection.create_new_graph(container)
42 | # self.get_all_edges(self.cim.PowerTransformer, centralized_graph)
43 | # self.get_all_edges(self.cim.TransformerTank, centralized_graph)
44 | # self.get_all_edges(self.cim.BaseVoltage, centralized_graph)
45 |
46 |
47 | # self.linknet = LinkNet(self.cim_profile, centralized_graph)
48 | # self.linknet.build_linknet([self.cim.ACLineSegment])
49 |
--------------------------------------------------------------------------------
/cimgraph/models/distributed_area.py:
--------------------------------------------------------------------------------
1 | import importlib
2 | import logging
3 | from dataclasses import dataclass, field
4 | from uuid import UUID
5 |
6 | from cimgraph.models.graph_model import GraphModel
7 |
8 | _log = logging.getLogger(__name__)
9 |
10 |
11 | @dataclass
12 | class DistributedArea(GraphModel):
13 |
14 | def __post_init__(self):
15 | self.cim_profile = self.connection.cim_profile
16 | self.cim = self.connection.cim
17 | self.graph = {}
18 | self.add_to_graph(self.container)
19 | self.distributed_areas = []
20 | self.boundaries = []
21 |
22 | def build_from_area(self) -> None:
23 | self.get_all_edges(self.container.__class__)
24 | self.connection.create_distributed_graph(area=self.container, graph=self.graph)
25 |
26 |
27 | # Build the distributed model from a topology message from GridAPPS-D
28 | def build_from_topo_message(self, topology_dict:dict):
29 | # Set base as
30 | self.container = self.add_jsonld_to_graph(topology_dict)
31 |
32 | # builder for Feeder, Substation, VoltageLevel, and Bay
33 | if isinstance(self.container, self.cim.EquipmentContainer):
34 | for addr_eq in topology_dict['AddressableEquipment']:
35 | equipment = self.add_jsonld_to_graph(addr_eq)
36 | equipment.EquipmentContainer = self.container
37 |
38 | for unaddr_eq in topology_dict['AddressableEquipment']:
39 | equipment = self.add_jsonld_to_graph(unaddr_eq)
40 | equipment.EquipmentContainer = self.container
41 |
42 | for meas in topology_dict['Measurements']:
43 | self.add_jsonld_to_graph(meas)
44 |
45 |
46 | elif isinstance(self.container, self.cim.SubSchedulingArea):
47 | for boundary in topology_dict['BoundaryTerminals']:
48 | terminal = self.add_jsonld_to_graph(boundary)
49 | self.container.BoundaryTerminals.append(terminal)
50 |
51 | for addr_eq in topology_dict['AddressableEquipment']:
52 | equipment = self.add_jsonld_to_graph(addr_eq)
53 | equipment.SubSchedulingArea = self.container
54 |
55 | for unaddr_eq in topology_dict['AddressableEquipment']:
56 | equipment = self.add_jsonld_to_graph(unaddr_eq)
57 | equipment.SubSchedulingArea = self.container
58 |
59 | for meas in topology_dict['Measurements']:
60 | self.add_jsonld_to_graph(meas)
61 |
--------------------------------------------------------------------------------
/cimgraph/models/feeder_model.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | from dataclasses import dataclass, field
4 |
5 | from cimgraph.models.distributed_area import DistributedArea
6 | from cimgraph.models.graph_model import GraphModel
7 |
8 | # from cimgraph.utils.timing import timing as time_func
9 |
10 | _log = logging.getLogger(__name__)
11 |
12 |
13 | @dataclass
14 | class FeederModel(GraphModel):
15 | """
16 | Knowledge graph class for distribution feeder objects. This should class
17 | should be used for all feeder models and distribution data.
18 | Args:
19 | container: a CIM Feeder object with specified mRID
20 | connection: a ConnectionInterface object, such as BlazegraphConnection
21 | distributed: a boolean to indicate if the graph is distributed
22 | Optional Args:
23 | topology_message: JSON message from GridAPPS-D Topology Proccessor Service
24 | Returns:
25 | none
26 | Methods:
27 | add_to_graph(object): adds a new CIM object to the knowledge graph
28 | get_all_edges(cim.ClassName): universal database query to expand graph by one edge
29 | graph[cim.ClassName]: access to graph dictionary sorted by class and mRID
30 | pprint(cim.ClassName): pretty-print method for showing graph of a class type
31 | get_edges_query(cim.ClassName): returns query text for debugging
32 | """
33 | topology_message: dict = field(default_factory=dict)
34 | distributed_areas: list[DistributedArea] | None = None
35 |
36 | def __post_init__(self):
37 |
38 | if self.connection is not None: # Check if connection has been specified
39 | self.cim = self.connection.cim # Set CIM data profile
40 | if self.distributed: # Check if distributed flag is true
41 | # Build distributed network model
42 | self.__initialize_distributed_model()
43 | else:
44 | # Otherwise build centralized network model
45 | self.__initialize_centralized_model()
46 | else: # Log error thant no connection was specified
47 | _log.error('A ConnectionInterface must be specified')
48 |
49 | def __initialize_centralized_model(self) -> None:
50 | # Build graph model using database-specific routine
51 | self.graph = self.connection.create_new_graph(self.container)
52 |
53 | def __initialize_distributed_model(self) -> None:
54 | self.distributed_areas = []
55 |
56 | # If topology message is provided, build the distributed model from the message
57 | if self.topology_message:
58 | if isinstance(self.topology_message, str):
59 | self.topology_message = json.loads(self.topology_message)
60 |
61 | if 'DistributionArea' in self.topology_message:
62 | # Identify cim.Feeder container from topo message
63 | feeder_topo_dict = self.topology_message['DistributionArea']['Substations'][0]['NormalEnergizedFeeder'][0]
64 | elif 'FeederArea' in self.topology_message:
65 | feeder_topo_dict = self.topology_message
66 | else:
67 | error_message = 'Invalid topology message. Must be a JSON message from GridAPPS-D Topology Processor'
68 | _log.error(error_message)
69 | raise ValueError(error_message)
70 |
71 | # try:
72 | self.container = self.add_jsonld_to_graph(feeder_topo_dict)
73 | # Identify cim.FeederArea container from topo message
74 | feeder_area_dict = feeder_topo_dict['FeederArea']
75 | feeder_area = self.add_jsonld_to_graph(feeder_area_dict)
76 |
77 | # Create a new DistributedArea GraphModel for the feeder area
78 | feeder_area_model = DistributedArea(container=feeder_area,
79 | connection=self.connection,
80 | distributed=True)
81 | feeder_area_model.build_from_topo_message(topology_dict=feeder_area_dict)
82 | # Append to distributed_areas field of FeederModel GraphModel.
83 | self.distributed_areas.append(feeder_area_model)
84 |
85 | for switch_area_dict in feeder_area_dict['SwitchAreas']:
86 | # Identify cim.SwitchArea container from topo message
87 | switch_area = feeder_area_model.add_jsonld_to_graph(switch_area_dict)
88 |
89 | # Create a new DistributedArea GraphModel for the switch area
90 | switch_area_model = DistributedArea(container=switch_area,
91 | connection=self.connection,
92 | distributed=True)
93 | switch_area_model.build_from_topo_message(switch_area_dict)
94 | # Append to distributed_areas field of FeederModel GraphModel.
95 | feeder_area_model.distributed_areas.append(switch_area_model)
96 |
97 | for sec_area_dict in switch_area_dict['SecondaryAreas']:
98 | # Identify cim.SecondaryArea container from topo message
99 | sec_area = switch_area_model.add_jsonld_to_graph(sec_area_dict)
100 |
101 | # Create a new DistributedArea GraphModel for the switch area
102 | sec_area_model = DistributedArea(container=sec_area,
103 | connection=self.connection,
104 | distributed=True)
105 | sec_area_model.build_from_topo_message(sec_area_dict)
106 | # Append to distributed_areas field of FeederModel GraphModel.
107 | switch_area_model.distributed_areas.append(sec_area_model)
108 |
109 | # except:
110 | # error_message = 'Invalid topology message. Must be a JSON message from GridAPPS-D Topology Processor'
111 | # _log.error(error_message)
112 | # raise ValueError(error_message)
113 |
114 | # If no topology message is provided, build the distributed model from the database
115 | else:
116 |
117 | if not isinstance(self.container, self.cim.Feeder):
118 | error_message = 'Invalid argument: container must be an instance of cim.Feeder'
119 | _log.error(error_message)
120 | raise TypeError(error_message)
121 |
122 | self.add_to_graph(self.container)
123 |
124 | new_edges = self.get_from_triple(self.container, 'Feeder.FeederArea')
125 |
126 | if not new_edges:
127 | error_message = f'No FeederArea defined for Feeder {self.container.uri()}. '
128 | error_message += 'Rebuild the model with the create_distributed_feeder() method'
129 | error_message += 'from the CIM-Graph-Topology-Processor library.'
130 | _log.error(error_message)
131 | raise ValueError(error_message)
132 |
133 | feeder_area = new_edges[0]
134 | # Create a new DistributedArea GraphModel for the feeder area
135 | feeder_area_model = DistributedArea(container=feeder_area,
136 | connection=self.connection,
137 | distributed=True)
138 |
139 | # Initialize DistributedArea with equipment, nodes, terminals, and measurements
140 | feeder_area_model.get_all_edges(self.cim.FeederArea)
141 | feeder_area_model.build_from_area()
142 | # Append to distributed_areas field of FeederModel GraphModel.
143 | self.distributed_areas.append(feeder_area_model)
144 |
145 | for switch_area in feeder_area.SwitchAreas:
146 | # Create a new DistributedArea GraphModel for each switch area
147 | switch_area_model = DistributedArea(container=switch_area,
148 | connection=self.connection,
149 | distributed=True)
150 | # Initialize DistributedArea with equipment, nodes, terminals, and measurements
151 | switch_area_model.get_all_edges(self.cim.SwitchArea)
152 | switch_area_model.build_from_area()
153 | # Add switch area context object to list of dist areas for feeder area context
154 | feeder_area_model.distributed_areas.append(switch_area_model)
155 |
156 | for secondary_area in switch_area.SecondaryAreas:
157 | # Create new DistributedArea GraphModel for each secondary area
158 | secondary_area_model = DistributedArea(container=secondary_area,
159 | connection=self.connection,
160 | distributed=True)
161 | # Initialize DistributedArea with equipment, nodes, terminals, and measurements
162 | secondary_area_model.build_from_area()
163 | # Add secondary area context to list of dist areas for switch area context
164 | switch_area_model.distributed_areas.append(secondary_area_model)
165 |
--------------------------------------------------------------------------------
/cimgraph/models/graph_model.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import json
4 | import logging
5 | from abc import ABC, abstractmethod
6 | from dataclasses import dataclass, field
7 | from uuid import UUID
8 |
9 | from cimgraph.data_profile.identity import Identity
10 | from cimgraph.databases import ConnectionInterface
11 |
12 | _log = logging.getLogger(__name__)
13 |
14 | jsonld = dict['@id':str(UUID),'@type':str(type)]
15 | Graph = dict[type, dict[UUID, object]]
16 |
17 | @dataclass
18 | class GraphModel(ABC):
19 | container: object
20 | connection: ConnectionInterface
21 | distributed: bool = field(default=False)
22 | graph: dict[type, dict[str, object]] = field(default_factory=dict)
23 | """
24 | Underlying root class for all knowledge graph models, inlcuding
25 | FeederModel, BusBranchModel, and NodeBreakerModel
26 | Required Args:
27 | container: a CIM container object inheriting from ConnectivityNodeContainer
28 | connection: a ConnectionInterface object, such as BlazegraphConnection
29 | distributed: a boolean to indicate if the graph is distributed
30 | Returns:
31 | none
32 | Methods:
33 | add_to_graph(object): adds a new CIM object to the knowledge graph
34 | get_all_edges(cim.ClassName): universal database query to expand graph by one edge
35 | graph[cim.ClassName]: access to graph dictionary sorted by class and mRID
36 | pprint(cim.ClassName): pretty-print method for showing graph of a class type
37 | get_edges_query(cim.ClassName): returns query text for debugging
38 | """
39 |
40 | def add_to_graph(self, obj: object, graph: dict[type, dict[UUID, object]] = None) -> None:
41 | if graph is None:
42 | graph = self.graph
43 | if type(obj) not in graph:
44 | graph[type(obj)] = {}
45 | if obj.identifier not in graph[type(obj)]:
46 | graph[type(obj)][obj.identifier] = obj
47 |
48 | def add_jsonld_to_graph(self, json_ld: jsonld, graph = None) -> object:
49 | if type(json_ld) == str:
50 | json_ld = json.loads(json_ld)
51 | elif type(json_ld) == dict:
52 | pass
53 | else:
54 | raise TypeError('json_ld input must be string or dict')
55 |
56 | if graph is None:
57 | graph = self.graph
58 |
59 | obj_id = json_ld['@id']
60 | obj_class = json_ld['@type']
61 |
62 | # If equipment class is in data profile, add it to the graph also
63 | if obj_class in self.cim.__all__:
64 | obj_class = getattr(self.cim, obj_class)
65 | obj = self.connection.create_object(class_type=obj_class, uri=obj_id, graph=graph)
66 | return obj
67 | else:
68 | # If it is not in the profile, log it as a missing class
69 | _log.warning(
70 | f'object class missing from data profile: {obj_class}')
71 |
72 |
73 |
74 | def get_all_edges(self, cim_class: type,
75 | graph: dict[type, dict[str, object]] = None) -> None:
76 | if graph is None:
77 | graph = self.graph
78 | if cim_class in graph:
79 | self.connection.get_all_edges(graph, cim_class)
80 | else:
81 | _log.info('no instances of ' + str(cim_class.__name__) +
82 | ' found in graph.')
83 |
84 | def get_edges_query(self, cim_class: type) -> str:
85 | if cim_class in self.graph:
86 | sparql_message = self.connection.get_edges_query(
87 | self.graph, cim_class)
88 | else:
89 | _log.info('no instances of ' + str(cim_class.__name__) +
90 | ' found in catalog.')
91 | sparql_message = ''
92 | return sparql_message
93 |
94 | def get_all_attributes(self, cim_class: type,
95 | graph: dict[type, dict[str, object]] = None) -> None:
96 | if graph is None:
97 | graph = self.graph
98 | if cim_class in graph:
99 | self.connection.get_all_attributes(graph, cim_class)
100 | else:
101 | _log.info('no instances of ' + str(cim_class.__name__) +
102 | ' found in graph.')
103 |
104 | def get_object(self, mRID:str|UUID) -> object:
105 | if type(mRID) != str:
106 | mRID = str(mRID)
107 | obj = self.connection.get_object(mRID, self.graph)
108 | if obj is None:
109 | obj = self.connection.get_object(mRID.upper(), self.graph)
110 | if obj is None:
111 | obj = self.connection.get_object('_' + mRID, self.graph)
112 | if obj is None:
113 | obj = self.connection.get_object('_' + mRID.upper(), self.graph)
114 | if obj is None:
115 | _log.warning(f'Could not find any objects matching {mRID}')
116 | return obj
117 |
118 | def get_from_triple(self, subject:object, predicate:str, add_to_graph = True) -> list[object|str]:
119 | if add_to_graph:
120 | new_edges = self.connection.get_from_triple(subject, predicate, self.graph)
121 | else:
122 | new_edges = self.connection.get_from_triple(subject, predicate)
123 | return new_edges
124 |
125 | def pprint(self, cim_class: type, show_empty: bool = False,
126 | json_ld: bool = False, use_names: bool = False) -> None:
127 | if cim_class in self.graph:
128 | json_dump = self.__dumps__(cim_class, show_empty, use_names)
129 | else:
130 | json_dump = {}
131 | _log.info(f'no instances of {cim_class.__name__} found in graph.')
132 | print(json_dump)
133 |
134 | def upload(self) -> None:
135 | self.connection.upload(self.graph)
136 |
137 | def __dumps__(self, cim_class: type, show_empty: bool = False,
138 | use_names=False) -> json:
139 | dump = []
140 | for obj in self.graph.get(cim_class, {}).values():
141 | if isinstance(obj, Identity):
142 | dump.append(json.loads(obj.__str__(
143 | show_empty=show_empty,
144 | use_names=use_names)))
145 | else:
146 | _log.warning(f'Unknown object of type {type(obj)}')
147 | dump = json.dumps(dump, indent=4)
148 | return dump
149 |
--------------------------------------------------------------------------------
/cimgraph/models/node_breaker_model.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import logging
4 | from dataclasses import dataclass, field
5 |
6 | from cimgraph.databases import ConnectionInterface
7 | from cimgraph.models.distributed_area import DistributedArea
8 | from cimgraph.models.graph_model import GraphModel
9 |
10 | _log = logging.getLogger(__name__)
11 |
12 |
13 | @dataclass
14 | class NodeBreakerModel(GraphModel):
15 | """
16 | Knowledge graph class for transmission node-breaker models. This should class
17 | should be used for all models with detailed substation representations.
18 | Args:
19 | container: a CIM object inheriting from EquipmentContainer with specified mRID
20 | connection: a ConnectionInterface object, such as BlazegraphConnection
21 | distributed: a boolean to indicate if the graph is distributed
22 | Optional Args:
23 | distributed_hierarchy: Custom inheritance structure for defining distributed areas
24 | topology_message: JSON message from GridAPPS-D Topology Proccessor Service
25 | Returns:
26 | none
27 | Methods:
28 | graph[cim.ClassName]: access to graph dictionary sorted by class and mRID
29 | add_to_graph(object): adds a new CIM object to the knowledge graph
30 | get_all_edges(cim.ClassName): universal database query to expand graph by one edge
31 | get_all_attributes(cim.ClassName): universal query to get attributes without expanding graph
32 | get_edges_query(cim.ClassName): returns query text for debugging
33 | pprint(cim.ClassName): pretty-print method for showing graph of a class type
34 |
35 | """
36 | # topology_message: dict = field(default_factory=dict)
37 | # distributed_hierarchy: list[type] = field(default_factory=list)
38 | aggregate_lower_areas: bool = field(default=True)
39 |
40 | def __post_init__(self):
41 | if self.connection is not None: # Check if connection has been specified
42 | self.cim = self.connection.cim # Set CIM data profile
43 | if self.distributed: # Check if distributed flag is true
44 | # Build distributed network model
45 | self.initialize_distributed_model(self.container)
46 | else:
47 | # Otherwise build centralized network model
48 | self.initialize_centralized_model(self.container)
49 | else: # Log error thant no connection was specified
50 | _log.error('A ConnectionInterface must be specified')
51 |
52 | def initialize_centralized_model(self, container: object) -> None:
53 | self.graph = self.connection.create_new_graph(container)
54 | self.add_to_graph(container)
55 |
56 | def initialize_distributed_model(self, container: object) -> None:
57 | self.graph = {}
58 | self.add_to_graph(self.container)
59 | self.distributed_areas = {}
60 |
61 | # if container.__class__ == self.cim.GeographicalRegion:
62 | # self.get_all_edges(self.cim.GeographicalRegion)
63 | # self.distributed_areas[self.cim.SubGeographicalRegion] = {}
64 | # self.distributed_areas[self.cim.Substation] = {}
65 | # sub_geo_list = list(
66 | # self.graph[self.cim.SubGeographicalRegion].keys())
67 | # for sub_geo_mrid in sub_geo_list:
68 | # sub_geo = self.graph[
69 | # self.cim.SubGeographicalRegion][sub_geo_mrid]
70 | # SubGeographicalArea = create_subgeographical_area(
71 | # self.connection, sub_geo)
72 | # self.distributed_areas[self.cim.SubGeographicalRegion][
73 | # sub_geo_mrid] = SubGeographicalArea
74 | # if self.aggregate_lower_areas:
75 | # aggregate_equipment(self.graph, SubGeographicalArea.graph)
76 | # self.distributed_areas.update(
77 | # SubGeographicalArea.distributed_areas)
78 |
79 | # elif container.__class__ == self.cim.SubGeographicalRegion:
80 | # self.get_all_edges(self.cim.SubGeographicalRegion)
81 |
82 | # for substation in self.graph[self.cim.Substation].values():
83 | # SubstationArea = create_substation_area(
84 | # self.connection, substation)
85 | # self.distributed_areas.append(SubstationArea)
86 | # if self.aggregate_lower_areas:
87 | # aggregate_equipment(self.graph, SubstationArea.graph)
88 |
89 | # self.distristributed_areas = create_hierarchy_level(self, self.distributed_hierarchy, top_level=True)
90 |
--------------------------------------------------------------------------------
/cimgraph/queries/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PNNL-CIM-Tools/CIM-Graph/e448f4088b11b5a2d2f8317769bd939da7ea2f11/cimgraph/queries/__init__.py
--------------------------------------------------------------------------------
/cimgraph/queries/cypher/__init__.py:
--------------------------------------------------------------------------------
1 | from cimgraph.queries.cypher.get_all_edges import get_all_edges_cypher, get_all_properties_cypher
2 | from cimgraph.queries.cypher.get_all_nodes import (get_all_nodes_from_area,
3 | get_all_nodes_from_container)
4 | from cimgraph.queries.cypher.get_object import get_object_cypher
5 | from cimgraph.queries.cypher.get_triple import get_triple_cypher
6 |
--------------------------------------------------------------------------------
/cimgraph/queries/cypher/get_all_edges.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from uuid import UUID
4 |
5 | from cimgraph.databases import get_iec61970_301, get_namespace, get_url
6 |
7 |
8 | def get_all_edges_cypher(graph:dict[type, dict[UUID, object]], cim_class: type, uuid_list: list[UUID]) -> str:
9 | """
10 | Generates SPARQL query string for a given catalog of objects and feeder id
11 | Args:
12 | graph (dict[type, dict[UUID, object]]): The graph of CIM objects organized by
13 | class type and UUID object identifier
14 | cim_class (type): The CIM class type to query
15 | uuid_list (list[UUID]): List of UUIDs to query for
16 |
17 | Returns:
18 | query_message: query string that can be used in Neo4J database
19 | """
20 | class_name = cim_class.__name__
21 |
22 | if int(get_iec61970_301()) > 7:
23 | split = 'urn:uuid:'
24 | else:
25 | split = f'{get_url()}#'
26 |
27 | query_message = f"""
28 | MATCH (n:{class_name})
29 | WHERE n.uri IN ["""
30 |
31 | for uuid in uuid_list:
32 | query_message += f'"{split+graph[cim_class][uuid].uri()}", \n'
33 |
34 | query_message = query_message.rstrip(', \n')
35 | query_message += f''']
36 | OPTIONAL MATCH (n) - [r] - (m)
37 | RETURN DISTINCT n.uri as identifier,
38 | type(r) as attribute,
39 | REPLACE(m.uri, "{split}", "") as edge_id,
40 | labels(m)[1] as edge_class'''
41 | # '{"@id":"' + COALESCE(m.uri ,"") + '","@type":"' + COALESCE((labels(m))[1],"") + '"}' as edge
42 | return query_message
43 |
44 | def get_all_properties_cypher(graph:dict[type, dict[UUID, object]], cim_class: type, uuid_list: list[UUID]) -> str:
45 | """
46 | Generates SPARQL query string for a given catalog of objects and feeder id
47 | Args:
48 | graph (dict[type, dict[UUID, object]]): The graph of CIM objects organized by
49 | class type and UUID object identifier
50 | cim_class (type): The CIM class type to query
51 | uuid_list (list[UUID]): List of UUIDs to query for
52 |
53 | Returns:
54 | query_message: query string that can be used in Neo4J database
55 | """
56 | class_name = cim_class.__name__
57 |
58 | if int(get_iec61970_301()) > 7:
59 | split = 'urn:uuid:'
60 | else:
61 | split = f'{get_url()}#'
62 |
63 | query_message = f"""
64 | MATCH (n:{class_name})
65 | WHERE n.uri IN [
66 | """
67 |
68 | for uuid in uuid_list:
69 | query_message += f'"{split+graph[cim_class][uuid].uri()}", \n'
70 |
71 | query_message = query_message.rstrip(', \n')
72 | query_message += ''']
73 | RETURN DISTINCT n.uri as identifier,
74 | properties(n) as attributes'''
75 | return query_message
76 |
--------------------------------------------------------------------------------
/cimgraph/queries/cypher/get_all_nodes.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from cimgraph.databases import get_iec61970_301, get_namespace, get_url
4 |
5 |
6 | def get_all_nodes_from_container(container: object):
7 | """
8 | Generates SPARQL query string for all nodes, terminals, and conducting equipment
9 | Args:
10 |
11 | Returns:
12 | query_message: query string that can be used in blazegraph connection or STOMP client
13 | """
14 | container_class = container.__class__.__name__
15 | uri = container.uri()
16 |
17 | if get_iec61970_301() > 7:
18 | split = 'urn:uuid:'
19 | else:
20 | split = f'{get_url()}#'
21 |
22 | query_message = f"""MATCH (container:{container_class})
23 | WHERE container.uri = "{split}{uri}"
24 | MATCH (eq) - [:`Equipment.EquipmentContainer`] - (container)
25 | OPTIONAL MATCH (cnode) - [:`Terminal.ConnectivityNode`] - (term:Terminal) - [:`Terminal.ConductingEquipment`] -> (eq)
26 | RETURN DISTINCT
27 | REPLACE(cnode.uri, "{split}", "") as ConnectivityNode,
28 | REPLACE(term.uri, "{split}", "") as Terminal,
29 | REPLACE(eq.uri, "{split}", "") as eq_id,
30 | LABELS(eq)[1] as eq_class"""
31 |
32 | return query_message
33 |
34 |
35 | def get_all_nodes_from_area(container: object):
36 | """
37 | Generates SPARQL query string for all nodes, terminals, and conducting equipment
38 | Args:
39 |
40 | Returns:
41 | query_message: query string that can be used in blazegraph connection or STOMP client
42 | """
43 | container_class = container.__class__.__name__
44 | uri = container.uri()
45 |
46 | query_message = f"""MATCH (container:{container_class})
47 | WHERE container.uri = "{uri}"
48 | MATCH (eq) - [:`Equipment.SubSchedulingArea`] - (container)
49 | OPTIONAL MATCH (cnode) - [:`Terminal.ConnectivityNode`] - (term:Terminal) - [:`Terminal.ConductingEquipment`] -> (eq)
50 | RETURN DISTINCT
51 | cnode.`IdentifiedObject.mRID` as ConnectivityNode,
52 | term.`IdentifiedObject.mRID` as Terminal,
53 | eq.`IdentifiedObject.mRID` as eq_id,
54 | LABELS(eq)[1] as eq_class"""
55 |
56 | return query_message
57 |
--------------------------------------------------------------------------------
/cimgraph/queries/cypher/get_object.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from uuid import UUID
4 |
5 | from cimgraph.databases import get_iec61970_301, get_namespace, get_url
6 |
7 |
8 | def get_object_cypher(mRID: str) -> str:
9 | """
10 | Generates cypher query string to find the type of an object from its uri
11 | Args:
12 | mrid (str): The mRID or uri of the object
13 | Returns:
14 | query_message: query string that can be used in Neo4J
15 | """
16 |
17 |
18 | if int(get_iec61970_301()) > 7:
19 | split = 'urn:uuid:'
20 | else:
21 | split = f'{get_url()}#'
22 |
23 | query_message = f'''
24 | MATCH(n)
25 | WHERE n.uri = "{split+mRID}"
26 | RETURN DISTINCT n.uri as identifier,
27 | labels(n)[1] as class'''
28 | # '{"@id":"' + COALESCE(m.uri ,"") + '","@type":"' + COALESCE((labels(m))[1],"") + '"}' as edge
29 | return query_message
30 |
--------------------------------------------------------------------------------
/cimgraph/queries/cypher/get_triple.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from uuid import UUID
4 |
5 | from cimgraph.databases import get_iec61970_301, get_url
6 |
7 |
8 | def get_triple_cypher(subject:object, attribute:str) -> str:
9 |
10 | """
11 | Generates cypher query string to find the type of an object from its uri
12 | Args:
13 | subject (object): The subject of the RDF triple
14 | attribute (str): The attribute / association to be queried
15 | Returns:
16 | query_message: query string that can be used in Neo4J
17 | """
18 |
19 | if int(get_iec61970_301()) > 7:
20 | split = 'urn:uuid:'
21 | else:
22 | split = f'{get_url()}#'
23 |
24 | query_message = f'''MATCH (n:{subject.__class__.__name__})
25 | WHERE n.uri = "{split+subject.uri()}"
26 | OPTIONAL MATCH (n) - [:`{attribute}`] - (m)
27 | RETURN DISTINCT
28 | n.uri as identifier,
29 | "{attribute}" as attribute,
30 | COALESCE(n.`{attribute}`,m.uri) as edge_id,
31 | labels(m)[1] as edge_class'''
32 | # '{"@id":"' + COALESCE(m.uri ,"") + '","@type":"' + COALESCE((labels(m))[1],"") + '"}' as edge
33 | return query_message
34 |
--------------------------------------------------------------------------------
/cimgraph/queries/jsonld_sql/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PNNL-CIM-Tools/CIM-Graph/e448f4088b11b5a2d2f8317769bd939da7ea2f11/cimgraph/queries/jsonld_sql/__init__.py
--------------------------------------------------------------------------------
/cimgraph/queries/jsonld_sql/get_all_edges.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PNNL-CIM-Tools/CIM-Graph/e448f4088b11b5a2d2f8317769bd939da7ea2f11/cimgraph/queries/jsonld_sql/get_all_edges.py
--------------------------------------------------------------------------------
/cimgraph/queries/jsonld_sql/get_all_nodes.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PNNL-CIM-Tools/CIM-Graph/e448f4088b11b5a2d2f8317769bd939da7ea2f11/cimgraph/queries/jsonld_sql/get_all_nodes.py
--------------------------------------------------------------------------------
/cimgraph/queries/ontotext/__init__.py:
--------------------------------------------------------------------------------
1 | from cimgraph.queries.ontotext.get_all_edges import get_all_edges_ontotext
2 | from cimgraph.queries.ontotext.get_all_nodes import get_all_nodes_ontotext
3 |
--------------------------------------------------------------------------------
/cimgraph/queries/ontotext/get_all_edges.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from uuid import UUID
4 |
5 | from cimgraph.databases import get_iec61970_301, get_namespace, get_url
6 |
7 |
8 | def get_all_edges_ontotext(graph:dict[type, dict[UUID, object]], cim_class: type, uuid_list: list[UUID]) -> str:
9 | """
10 | Generates SPARQL query string for a given catalog of objects and feeder id
11 | Args:
12 | graph (dict[type, dict[UUID, object]]): The graph of CIM objects organized by
13 | class type and UUID object identifier
14 | cim_class (type): The CIM class type to query
15 | uuid_list (list[UUID]): List of UUIDs to query for
16 |
17 | Returns:
18 | query_message: query string that can be used in GraphDB connection or STOMP client
19 | """
20 | class_name = cim_class.__name__
21 | namespace = get_namespace()
22 |
23 | if int(get_iec61970_301()) > 7:
24 | split = 'urn:uuid:'
25 | else:
26 | split = '#'
27 |
28 | query_message = """
29 | PREFIX r:
30 | PREFIX cim: <%s>""" % namespace
31 |
32 | query_message += """
33 | SELECT DISTINCT ?mRID ?attribute ?value ?edge
34 | WHERE {
35 | ?eq r:type cim:%s.""" % class_name
36 | # query_message += """
37 | # VALUES ?fdrid {"%s"}
38 | # {?fdr cim:IdentifiedObject.mRID ?fdrid.
39 | # {?eq (cim:|!cim:)? [ cim:Equipment.EquipmentContainer ?fdr]}
40 | # UNION
41 | # {[cim:Equipment.EquipmentContainer ?fdr] (cim:|!cim:)? ?eq}}.
42 | # """ %feeder_mrid
43 |
44 | query_message += """
45 | VALUES ?identifier {"""
46 | # add all equipment mRID
47 | for uuid in uuid_list:
48 | query_message += ' "%s" \n' % graph[cim_class][uuid].uri()
49 | query_message += ' }'
50 | query_message += f'''
51 | bind(iri(concat("{split}", ?identifier)) as ?eq)'''
52 |
53 | # add all attributes
54 | query_message += """
55 | SERVICE {
56 | path:findPath path:allPaths ;
57 | path:sourceNode ?eq ;
58 | path:destinationNode ?dst ;
59 | path:minPathLength 1 ;
60 | path:maxPathLength 1 ;
61 | path:endNode ?value ;
62 | path:propertyBinding ?attr ;
63 | path:bidirectional true ;
64 | path:pathIndex ?path .
65 | }
66 |
67 | {bind(strafter(str(?attr),"#") as ?attribute)}
68 | {bind(strafter(str(?val),"%s") as ?uri)}
69 | {bind(if(?uri = "", ?val, ?uri) as ?value)}
70 |
71 | OPTIONAL {?val a ?classraw.
72 | bind(strafter(str(?classraw),"%s") as ?edge_class)
73 | {bind(strafter(str(?val),"%s") as ?uri)}
74 |
75 | bind(concat("{\\"@id\\":\\"", ?uri,"\\",\\"@type\\":\\"", ?edge_class, "\\"}") as ?edge)}
76 | }
77 |
78 | ORDER by ?identifier ?attribute
79 | """ % (split, namespace, split)
80 | return query_message
81 |
--------------------------------------------------------------------------------
/cimgraph/queries/ontotext/get_all_nodes.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from cimgraph.databases import get_iec61970_301, get_namespace, get_url
4 |
5 |
6 | def get_all_nodes_ontotext(container: object) -> str:
7 | """
8 | Generates SPARQL query string for all nodes, terminals, and conducting equipment
9 | Args:
10 |
11 | Returns:
12 | query_message: query string that can be used in blazegraph connection or STOMP client
13 | """
14 | container_uri = container.uri()
15 | namespace = get_namespace()
16 |
17 | if get_iec61970_301() > 7:
18 | split = 'urn:uuid:'
19 | else:
20 | split = f'{get_url()}#'
21 |
22 | query_message = """
23 | PREFIX r:
24 | PREFIX cim: <%s>""" % namespace
25 | query_message += """
26 | SELECT DISTINCT ?ConnectivityNode ?Terminal ?Equipment
27 | WHERE {
28 | """ #
29 | query_message += """
30 | VALUES ?identifier {"%s"}
31 | bind(iri(concat("%s", ?identifier)) as ?c).
32 | """ % (container_uri, split)
33 |
34 | # get ConnectivityNode objects associated with Container
35 | query_message += ''' {
36 |
37 | ?node cim:ConnectivityNode.ConnectivityNodeContainer ?c.
38 | ?t cim:Terminal.ConnectivityNode ?node.
39 | ?t cim:Terminal.ConductingEquipment ?eq.
40 | ?eq a ?eq_cls.
41 |
42 | bind(strafter(str(?node),"%s") as ?ConnectivityNode).
43 | bind(strafter(str(?t),"%s") as ?Terminal).
44 | bind(strafter(str(?eq),"%s") as ?eq_id).
45 |
46 | bind(concat("{\\"@id\\":\\"", str(?eq_id),"\\",\\"@type\\":\\"",strafter(str(?eq_cls),"%s"), "\\"}") as ?Equipment)
47 | }
48 | ''' % (split, split, split, get_namespace())
49 | # get Equipment objects associated with Container
50 | query_message += '''
51 | UNION
52 | {
53 | {?eq cim:Equipment.EquipmentContainer ?c.}
54 | UNION
55 | {?eq cim:Equipment.AdditionalEquipmentContainer ?c.}
56 | OPTIONAL {
57 | ?t cim:Terminal.ConductingEquipment ?eq.
58 | ?t cim:Terminal.ConnectivityNode ?node.
59 | }
60 | ?eq a ?eq_cls.
61 |
62 | bind(strafter(str(?node),"%s") as ?ConnectivityNode).
63 | bind(strafter(str(?t),"%s") as ?Terminal).
64 | bind(strafter(str(?eq),"%s") as ?eq_id).
65 |
66 | bind(concat("{\\"@id\\":\\"", str(?eq_id),"\\",\\"@type\\":\\"",strafter(str(?eq_cls),"%s"), "\\"}") as ?Equipment)
67 | }
68 | }
69 | ORDER by ?ConnectivityNode
70 | ''' % (split, split, split, get_namespace())
71 |
72 | return query_message
73 |
74 | return query_message
75 |
--------------------------------------------------------------------------------
/cimgraph/queries/rdflib/__init__.py:
--------------------------------------------------------------------------------
1 | from cimgraph.queries.rdflib.get_all_attributes import get_all_attributes_sparql
2 | from cimgraph.queries.rdflib.get_all_edges import get_all_edges_sparql
3 | from cimgraph.queries.rdflib.get_all_nodes import get_all_nodes_sparql
4 |
--------------------------------------------------------------------------------
/cimgraph/queries/rdflib/get_all_attributes.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from uuid import UUID
4 |
5 | from cimgraph.data_profile.known_problem_classes import ClassesWithoutMRID
6 | from cimgraph.databases import get_cim_profile, get_iec61970_301, get_namespace, get_url
7 |
8 |
9 | def get_all_attributes_sparql(graph:dict[type, dict[UUID, object]], cim_class: str, uuid_list: list[UUID]) -> str:
10 | """
11 | Generates SPARQL query string for a given catalog of objects and feeder id
12 | Args:
13 | feeder_mrid (str | Feeder object): The mRID of the feeder or feeder object
14 | graph (dict[type, dict[str, object]]): The typed catalog of CIM objects organized by
15 | class type and object mRID
16 | Returns:
17 | query_message: query string that can be used in blazegraph connection or STOMP client
18 | """
19 | namespace = get_namespace()
20 | iec61970_301 = get_iec61970_301()
21 | class_name = cim_class.__name__
22 | classes_without_mrid = ClassesWithoutMRID()
23 |
24 | if int(iec61970_301) > 7:
25 | split = 'urn:uuid:'
26 | else:
27 | split = 'rdf:id:'
28 |
29 | query_message = """
30 | PREFIX r:
31 | PREFIX cim: <%s>""" % namespace
32 |
33 | query_message += """
34 | SELECT DISTINCT ?mRID ?attr ?val ?edge_class ?edge_mRID ?eq
35 | WHERE {
36 | ?eq r:type cim:%s.""" % class_name
37 | # query_message += """
38 | # VALUES ?fdrid {"%s"}
39 | # {?fdr cim:IdentifiedObject.mRID ?fdrid.
40 | # {?eq (cim:|!cim:)? [ cim:Equipment.EquipmentContainer ?fdr]}
41 | # UNION
42 | # {[cim:Equipment.EquipmentContainer ?fdr] (cim:|!cim:)? ?eq}}.
43 | # """ %feeder_mrid
44 |
45 | query_message += """
46 | VALUES ?identifier {"""
47 | # add all equipment mRID
48 | for uuid in uuid_list:
49 | query_message += ' "%s" \n' % graph[cim_class][uuid].uri()
50 | query_message += ' }'
51 | query_message += f'''
52 | bind(iri(concat("{split}", ?identifier)) as ?eq)'''
53 |
54 | # add all attributes
55 | query_message += """
56 | {?eq (cim:|!cim:) ?val.
57 | ?eq ?attr ?val.}
58 | UNION
59 | {?val (cim:|!cim:) ?eq.
60 | ?val ?attr ?eq.}
61 |
62 | # {bind(strafter(str(?attr),"#") as ?attribute).}
63 | # {bind(strafter(str(?val),"%s") as ?uri).}
64 | # {bind(if(?uri = "", ?val, ?uri) as ?value).}
65 | }
66 |
67 |
68 | ORDER by ?identifier ?attribute
69 | """ % split
70 | return query_message
71 |
--------------------------------------------------------------------------------
/cimgraph/queries/rdflib/get_all_edges.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from uuid import UUID
4 |
5 | from cimgraph.databases import get_iec61970_301, get_namespace
6 |
7 |
8 | def get_all_edges_sparql(graph:dict[type, dict[UUID, object]], cim_class: type, uuid_list: list[UUID]) -> str:
9 | """
10 | Generates SPARQL query string for a given catalog of objects and feeder id
11 | Args:
12 | graph (dict[type, dict[UUID, object]]): The graph of CIM objects organized by
13 | class type and UUID object identifier
14 | cim_class (type): The CIM class type to query
15 | uuid_list (list[UUID]): List of UUIDs to query for
16 |
17 | Returns:
18 | query_message: query string that can be used in RDFLib connection or STOMP client
19 | """
20 | class_name = cim_class.__name__
21 | namespace = get_namespace()
22 |
23 |
24 | if int(get_iec61970_301()) > 7:
25 | split = 'urn:uuid:'
26 | else:
27 | split = 'rdf:id:'
28 |
29 | query_message = """
30 | PREFIX r:
31 | PREFIX cim: <%s>""" % namespace
32 |
33 | query_message += """
34 | SELECT DISTINCT ?mRID ?attr ?val ?edge_class ?edge_mRID ?eq
35 | WHERE {
36 | ?eq r:type cim:%s.""" % class_name
37 | # query_message += """
38 | # VALUES ?fdrid {"%s"}
39 | # {?fdr cim:IdentifiedObject.mRID ?fdrid.
40 | # {?eq (cim:|!cim:)? [ cim:Equipment.EquipmentContainer ?fdr]}
41 | # UNION
42 | # {[cim:Equipment.EquipmentContainer ?fdr] (cim:|!cim:)? ?eq}}.
43 | # """ %feeder_mrid
44 |
45 | query_message += """
46 | VALUES ?identifier {"""
47 | # add all equipment mRID
48 | for uuid in uuid_list:
49 | query_message += ' "%s" \n' % graph[cim_class][uuid].uri()
50 | query_message += ' }'
51 | query_message += f'''
52 | bind(iri(concat("{split}", ?identifier)) as ?eq)'''
53 |
54 | # add all attributes
55 | query_message += """
56 |
57 | {?eq (cim:|!cim:) ?val.
58 | ?eq ?attr ?val.}
59 | UNION
60 | {?val (cim:|!cim:) ?eq.
61 | ?val ?attr ?eq.}
62 |
63 | # {bind(strafter(str(?attr),"#") as ?attribute).}
64 | # {bind(strafter(str(?val),"%s") as ?uri).}
65 | # {bind(if(?uri = "", ?val, ?uri) as ?value).}
66 |
67 | OPTIONAL {?val a ?classraw.
68 | bind(strafter(str(?classraw),"%s") as ?edge_class).}
69 | OPTIONAL {?val cim:IdentifiedObject.mRID ?edge_mRID.
70 | # {bind(if(EXISTS(?edge_id), ?val, ?edge_id)) as ?edge_mRID)}.
71 | # bind(concat("{\\"@id\\":\\"", ?edge_mRID,"\\",\\"@type\\":\\"", ?edge_class, "\\"}") as ?edge)
72 | }
73 | }
74 |
75 | ORDER by ?identifier ?attribute
76 | """ % (split, namespace)
77 | return query_message
78 |
--------------------------------------------------------------------------------
/cimgraph/queries/rdflib/get_all_nodes.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from cimgraph.databases import get_iec61970_301, get_namespace, get_url
4 |
5 |
6 | def get_all_nodes_sparql(container: object) -> str:
7 | """
8 | Generates SPARQL query string for all nodes, terminals, and conducting equipment
9 | Args:
10 |
11 | Returns:
12 | query_message: query string that can be used in blazegraph connection or STOMP client
13 | """
14 | container_class = container.__class__.__name__
15 | container_mRID = container.mRID
16 | namespace = get_namespace()
17 |
18 | try:
19 | container_uri = container.uri()
20 | except:
21 | container_uri = container.mRID
22 |
23 | if get_iec61970_301() > 7:
24 | split = 'urn:uuid:'
25 | else:
26 | split = 'rdf:id:'
27 |
28 | query_message = """
29 | PREFIX r:
30 | PREFIX cim: <%s>""" % namespace
31 | query_message += """
32 | SELECT ?ConnectivityNode ?Terminal ?Equipment
33 | WHERE {
34 | ?c r:type cim:%s.""" % container_class
35 | query_message += """
36 | VALUES ?identifier {"%s"}
37 | bind(iri(concat("%s", ?identifier)) as ?c).
38 | """ % (container_uri, split)
39 |
40 | # add all attributes
41 | query_message += """
42 | {
43 | ?node cim:ConnectivityNode.ConnectivityNodeContainer ?c.
44 | ?t cim:Terminal.ConnectivityNode ?node.
45 | ?t cim:Terminal.ConductingEquipment ?eq.
46 | ?eq a ?eq_cls.
47 |
48 | bind(strafter(str(?node),"%s") as ?ConnectivityNode).
49 | bind(strafter(str(?t),"%s") as ?Terminal).
50 | bind(strafter(str(?eq),"%s") as ?eq_id).
51 |
52 | bind(concat("{\\"@id\\":\\"", str(?eq_id),"\\",\\"@type\\":\\"",strafter(str(?eq_cls),"%s"), "\\"}") as ?Equipment)
53 | }
54 | """ % (split, split, split, get_namespace())
55 | # get Equipment objects associated with Container
56 | query_message += """
57 | UNION
58 | {
59 | {?eq cim:Equipment.EquipmentContainer ?c.}
60 | UNION
61 | {?eq cim:Equipment.AdditionalEquipmentContainer ?c.}
62 | OPTIONAL {
63 | ?t cim:Terminal.ConductingEquipment ?eq.
64 | ?t cim:Terminal.ConnectivityNode ?node.
65 | }
66 | ?eq a ?eq_cls.
67 |
68 | bind(strafter(str(?node),"%s") as ?ConnectivityNode).
69 | bind(strafter(str(?t),"%s") as ?Terminal).
70 | bind(strafter(str(?eq),"%s") as ?eq_id).
71 |
72 | bind(concat("{\\"@id\\":\\"", str(?eq_id),"\\",\\"@type\\":\\"",strafter(str(?eq_cls),"%s"), "\\"}") as ?Equipment)
73 | }
74 | }
75 | ORDER by ?ConnectivityNode
76 | """ % (split, split, split, get_namespace())
77 |
78 | return query_message
79 |
--------------------------------------------------------------------------------
/cimgraph/queries/sparql/__init__.py:
--------------------------------------------------------------------------------
1 | from cimgraph.queries.sparql.get_all_attributes import get_all_attributes_sparql
2 | from cimgraph.queries.sparql.get_all_edges import get_all_edges_sparql
3 | from cimgraph.queries.sparql.get_all_nodes import (get_all_nodes_from_area,
4 | get_all_nodes_from_container,
5 | get_all_nodes_from_list)
6 | from cimgraph.queries.sparql.get_object import get_object_sparql
7 | from cimgraph.queries.sparql.get_triple import get_triple_sparql
8 | from cimgraph.queries.sparql.upload_triples import upload_triples_sparql
9 |
--------------------------------------------------------------------------------
/cimgraph/queries/sparql/get_all_attributes.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from uuid import UUID
4 |
5 | from cimgraph.databases import get_iec61970_301, get_namespace, get_url
6 |
7 |
8 | def get_all_attributes_sparql(graph:dict[type, dict[UUID, object]], cim_class: str, uuid_list: list[UUID]) -> str:
9 | """
10 | Generates SPARQL query string for a given catalog of objects and feeder id
11 | Args:
12 | feeder_mrid (str | Feeder object): The mRID of the feeder or feeder object
13 | graph (dict[type, dict[str, object]]): The typed catalog of CIM objects organized by
14 | class type and object mRID
15 | Returns:
16 | query_message: query string that can be used in blazegraph connection or STOMP client
17 | """
18 | class_name = cim_class.__name__
19 |
20 | if int(get_iec61970_301()) > 7:
21 | split = 'urn:uuid:'
22 | else:
23 | split = f'{get_url()}#'
24 |
25 | query_message = """
26 | PREFIX r:
27 | PREFIX cim: <%s>""" % get_namespace()
28 |
29 | query_message += """
30 | SELECT DISTINCT ?identifier ?attribute ?value ?edge
31 | WHERE {
32 | """
33 |
34 | query_message += """
35 | VALUES ?identifier {"""
36 | # add all equipment mRID
37 | for uuid in uuid_list:
38 | query_message += ' "%s" \n' % graph[cim_class][uuid].uri()
39 | query_message += ' }'
40 | query_message += f'''
41 | bind(iri(concat("{split}", ?identifier)) as ?eq)'''
42 |
43 | query_message += """
44 |
45 | ?eq r:type cim:%s.
46 |
47 | {?eq (cim:|!cim:) ?val.
48 | ?eq ?attr ?val.}
49 | UNION
50 | {?val (cim:|!cim:) ?eq.
51 | ?val ?attr ?eq.}
52 |
53 | {bind(strafter(str(?attr),"#") as ?attribute)}
54 | {bind(strafter(str(?val),"%s") as ?uri)}
55 | {bind(if(?uri = "", ?val, ?uri) as ?value)}
56 | }
57 |
58 | ORDER by ?identifier ?attribute
59 | """ % (class_name, split)
60 | return query_message
61 |
--------------------------------------------------------------------------------
/cimgraph/queries/sparql/get_all_edges.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from uuid import UUID
4 |
5 | from cimgraph.databases import get_iec61970_301, get_namespace, get_url
6 |
7 |
8 | def get_all_edges_sparql(graph:dict[type, dict[UUID, object]], cim_class: type, uuid_list: list[UUID]) -> str:
9 | """
10 | Generates SPARQL query string for a given catalog of objects and feeder id
11 | Args:
12 | graph (dict[type, dict[UUID, object]]): The graph of CIM objects organized by
13 | class type and UUID object identifier
14 | cim_class (type): The CIM class type to query
15 | uuid_list (list[UUID]): List of UUIDs to query for
16 | Returns:
17 | query_message: query string that can be used in blazegraph connection or STOMP client
18 | """
19 | class_name = cim_class.__name__
20 |
21 | if get_iec61970_301() > 7:
22 | split = 'urn:uuid:'
23 | else:
24 | split = f'{get_url()}#'
25 |
26 | query_message = """
27 | PREFIX r:
28 | PREFIX cim: <%s>""" % get_namespace()
29 |
30 | query_message += """
31 | SELECT DISTINCT ?identifier ?attribute ?value ?edge
32 | WHERE {
33 | """
34 |
35 | query_message += """
36 | VALUES ?identifier {"""
37 | # add all equipment mRID
38 | for uuid in uuid_list:
39 | query_message += ' "%s" \n' % graph[cim_class][uuid].uri()
40 | query_message += ' }'
41 | query_message += f'''
42 | bind(iri(concat("{split}", ?identifier)) as ?eq)'''
43 |
44 | query_message += """
45 |
46 | ?eq r:type cim:%s.
47 |
48 | {?eq (cim:|!cim:) ?val.
49 | ?eq ?attr ?val.}
50 | UNION
51 | {?val (cim:|!cim:) ?eq.
52 | ?val ?attr ?eq.}
53 |
54 | {bind(strafter(str(?attr),"#") as ?attribute)}
55 | {bind(strafter(str(?val),"%s") as ?uri)}
56 | {bind(if(?uri = "", ?val, ?uri) as ?value)}
57 |
58 | OPTIONAL {?val a ?classraw.
59 | bind(strafter(str(?classraw),"%s") as ?edge_class)
60 | {bind(strafter(str(?val),"%s") as ?uri)}
61 |
62 | bind(concat("{\\"@id\\":\\"", ?uri,"\\",\\"@type\\":\\"", ?edge_class, "\\"}") as ?edge)}
63 | }
64 |
65 | ORDER by ?identifier ?attribute
66 | """ % (class_name, split, get_namespace(), split)
67 | return query_message
68 |
--------------------------------------------------------------------------------
/cimgraph/queries/sparql/get_all_nodes.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import logging
4 |
5 | import cimgraph.data_profile.cim17v40 as cim
6 | from cimgraph.databases import get_iec61970_301, get_namespace, get_url
7 |
8 | _log = logging.getLogger(__name__)
9 |
10 |
11 |
12 | def get_all_nodes_from_container(container: cim.EquipmentContainer) -> str:
13 | """
14 | Generates SPARQL query string for all nodes, terminals, and conducting equipment
15 | Args:
16 | container: an object instance of cim:ConnectivityNodeContainer or child classes (e.g. cim:Feeder)
17 | Returns:
18 | query_message: query string that can be used in blazegraph connection or STOMP client
19 | """
20 |
21 | try:
22 | container_uri = container.uri()
23 | except:
24 | container_uri = container.mRID
25 |
26 | if get_iec61970_301() > 7:
27 | split = 'urn:uuid:'
28 | else:
29 | split = f'{get_url()}#'
30 |
31 |
32 | query_message = """
33 | PREFIX r:
34 | PREFIX cim: <%s>""" % get_namespace()
35 | query_message += """
36 | SELECT DISTINCT ?ConnectivityNode ?Terminal ?Equipment
37 | WHERE {
38 | """ #
39 | query_message += """
40 | VALUES ?identifier {"%s"}
41 | bind(iri(concat("%s", ?identifier)) as ?c).
42 | """ % (container_uri, split)
43 |
44 | # get ConnectivityNode objects associated with Container
45 | query_message += ''' {
46 |
47 | ?node cim:ConnectivityNode.ConnectivityNodeContainer ?c.
48 | ?t cim:Terminal.ConnectivityNode ?node.
49 | ?t cim:Terminal.ConductingEquipment ?eq.
50 | ?eq a ?eq_cls.
51 |
52 | bind(strafter(str(?node),"%s") as ?ConnectivityNode).
53 | bind(strafter(str(?t),"%s") as ?Terminal).
54 | bind(strafter(str(?eq),"%s") as ?eq_id).
55 |
56 | bind(concat("{\\"@id\\":\\"", str(?eq_id),"\\",\\"@type\\":\\"",strafter(str(?eq_cls),"%s"), "\\"}") as ?Equipment)
57 | }
58 | ''' % (split, split, split, get_namespace())
59 | # get Equipment objects associated with Container
60 | query_message += '''
61 | UNION
62 | {
63 | {?eq cim:Equipment.EquipmentContainer ?c.}
64 | UNION
65 | {?eq cim:Equipment.AdditionalEquipmentContainer ?c.}
66 | OPTIONAL {
67 | ?t cim:Terminal.ConductingEquipment ?eq.
68 | ?t cim:Terminal.ConnectivityNode ?node.
69 | }
70 | ?eq a ?eq_cls.
71 |
72 | bind(strafter(str(?node),"%s") as ?ConnectivityNode).
73 | bind(strafter(str(?t),"%s") as ?Terminal).
74 | bind(strafter(str(?eq),"%s") as ?eq_id).
75 |
76 | bind(concat("{\\"@id\\":\\"", str(?eq_id),"\\",\\"@type\\":\\"",strafter(str(?eq_cls),"%s"), "\\"}") as ?Equipment)
77 | }
78 | }
79 | ORDER by ?ConnectivityNode
80 | ''' % (split, split, split, get_namespace())
81 |
82 | return query_message
83 |
84 |
85 | def get_all_nodes_from_list(mrid_list: list[str], namespace: str) -> str:
86 | """
87 | Generates SPARQL query string for all nodes, terminals, and conducting equipment
88 | Args:
89 |
90 | Returns:
91 | query_message: query string that can be used in blazegraph connection or STOMP client
92 | """
93 | query_message = f"""
94 | PREFIX r:
95 | PREFIX cim: <{namespace}>"""
96 | query_message += """
97 | SELECT DISTINCT ?ConnectivityNode ?Terminal ?Equipment
98 | WHERE {
99 | ?c r:type cim:ConnectivityNode"""
100 | query_message += """
101 | VALUES ?ConnectivityNode {"""
102 | # add all equipment mRID
103 | for mrid in mrid_list:
104 | query_message += f' "{mrid}" \n'
105 | query_message += ' }'
106 |
107 | # add all attributes
108 | query_message += """
109 | {
110 | ?node cim:IdentifiedObject.mRID ?ConnectivityNode.
111 |
112 | ?t cim:Terminal.ConnectivityNode ?node.
113 | ?t cim:IdentifiedObject.mRID ?Terminal.
114 | ?t cim:Terminal.ConductingEquipment ?eq.
115 |
116 | ?eq cim:IdentifiedObject.mRID ?eq_id.
117 | ?eq a ?eq_cls.
118 | bind(concat("{\\"@id\\":\\"", str(?eq_id),"\\",\\"@type\\":\\"",strafter(str(?eq_cls),"%s"), "\\"}") as ?Equipment)
119 | }
120 | }
121 | """ % namespace
122 |
123 | return query_message
124 |
125 |
126 | def get_all_nodes_from_area(area: object) -> str:
127 | """
128 | Generates SPARQL query string for all nodes, terminals, and conducting equipment
129 | Args:
130 | area: object of type SubSchedulingArea
131 | Returns:
132 | query_message: query string that can be used in blazegraph connection or STOMP client
133 | """
134 |
135 | area_class = area.__class__.__name__
136 | try:
137 | container_uri = area.uri()
138 | except:
139 | container_uri = area.mRID
140 |
141 | if get_iec61970_301() > 7:
142 | split = 'urn:uuid:'
143 | else:
144 | split = f'{get_url()}#'
145 |
146 |
147 | query_message = """
148 | PREFIX r:
149 | PREFIX cim: <%s>""" % get_namespace()
150 | query_message += """
151 | SELECT DISTINCT ?ConnectivityNode ?Terminal ?Equipment ?Measurement
152 | WHERE {
153 | """ #
154 | query_message += """
155 | VALUES ?identifier {"%s"}
156 | bind(iri(concat("%s", ?identifier)) as ?c).
157 | """ % (container_uri, split)
158 |
159 |
160 | # get Equipment objects associated with Container
161 | query_message += '''
162 |
163 | ?eq cim:Equipment.SubSchedulingArea ?c.
164 | ?eq a ?eq_cls.
165 |
166 | ?t cim:Terminal.ConductingEquipment ?eq.
167 | ?t cim:Terminal.ConnectivityNode ?node.
168 |
169 | OPTIONAL {?meas cim:Measurement.Terminal ?t.
170 | ?meas a ?m_cls.
171 | bind(strafter(str(?meas),"%s") as ?meas_id).
172 | }
173 |
174 | bind(strafter(str(?node),"%s") as ?ConnectivityNode).
175 | bind(strafter(str(?t),"%s") as ?Terminal).
176 |
177 | bind(strafter(str(?eq),"%s") as ?eq_id).
178 |
179 | bind(concat("{\\"@id\\":\\"", str(?eq_id),"\\",\\"@type\\":\\"",strafter(str(?eq_cls),"%s"), "\\"}") as ?Equipment)
180 | bind(concat("{\\"@id\\":\\"", str(?meas_id),"\\",\\"@type\\":\\"",strafter(str(?m_cls),"%s"), "\\"}") as ?Measurement)
181 |
182 | }
183 | ORDER by ?Equipment
184 | ''' % (split, split, split, split, get_namespace(), get_namespace())
185 |
186 | return query_message
187 |
--------------------------------------------------------------------------------
/cimgraph/queries/sparql/get_object.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from uuid import UUID
4 |
5 | from cimgraph.databases import get_iec61970_301, get_namespace, get_url
6 |
7 |
8 | def get_object_sparql(mRID: str) -> str:
9 | """
10 | Generates SPARQL query string to find the type of an object from its uri
11 | Args:
12 | mRID (str): The mRID or uri of the object
13 | Returns:
14 | query_message: query string that can be used in blazegraph connection or STOMP client
15 | """
16 |
17 | if get_iec61970_301() > 7:
18 | split = 'urn:uuid:'
19 | else:
20 | split = f'{get_url()}#'
21 |
22 | query_message = """
23 | PREFIX r:
24 | PREFIX cim: <%s>""" % get_namespace()
25 |
26 | query_message += """
27 | SELECT DISTINCT ?identifier ?obj_class
28 | WHERE {
29 | """
30 |
31 | query_message += '''
32 | VALUES ?identifier {"%s"}''' %mRID
33 | # add all equipment mRID
34 |
35 | query_message += f'''
36 | bind(iri(concat("{split}", ?identifier)) as ?eq)'''
37 |
38 | query_message += """
39 |
40 | ?eq a ?classraw.
41 | bind(strafter(str(?classraw),"%s") as ?obj_class)
42 | }
43 | ORDER by ?identifier
44 | """ % (get_namespace())
45 | return query_message
46 |
--------------------------------------------------------------------------------
/cimgraph/queries/sparql/get_triple.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from cimgraph.databases import get_iec61970_301, get_namespace, get_url
4 |
5 |
6 | def get_triple_sparql(obj:object, attribute:str) -> str:
7 | """
8 | Generates SPARQL query string to find predicate for RDF triple
9 | Args:
10 |
11 | Returns:
12 | query_message: query string that can be used in blazegraph connection or STOMP client
13 | """
14 | mrid = obj.uri()
15 |
16 | if int(get_iec61970_301()) > 7:
17 | split = 'urn:uuid:'
18 | else:
19 | split = f'{get_url()}#'
20 |
21 | query_message = """
22 | PREFIX r:
23 | PREFIX cim: <%s>""" % get_namespace()
24 |
25 | query_message += """
26 | SELECT DISTINCT ?identifier ?attribute ?value ?edge
27 | WHERE {
28 | """
29 |
30 | query_message += '''
31 | VALUES ?identifier {"%s"}''' %mrid
32 | # add all equipment mRID
33 |
34 | query_message += f'''
35 | bind(iri(concat("{split}", ?identifier)) as ?eq)'''
36 |
37 | query_message += """
38 |
39 | ?eq cim:%s ?val.
40 |
41 | {bind("%s" as ?attribute)}
42 | {bind(strafter(str(?val),"%s") as ?uri)}
43 | {bind(if(?uri = "", ?val, ?uri) as ?value)}
44 |
45 | OPTIONAL {?val a ?classraw.
46 | bind(strafter(str(?classraw),"%s") as ?edge_class)
47 | {bind(strafter(str(?val),"%s") as ?uri)}
48 |
49 | bind(concat("{\\"@id\\":\\"", ?uri,"\\",\\"@type\\":\\"", ?edge_class, "\\"}") as ?edge)}
50 | }
51 |
52 | ORDER by ?identifier ?attribute
53 | """ % (attribute, attribute, split, get_namespace(), split)
54 | return query_message
55 |
--------------------------------------------------------------------------------
/cimgraph/queries/sparql/upload_triples.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import enum
4 | import importlib
5 | import logging
6 |
7 | from cimgraph.data_profile.known_problem_classes import ClassesWithManytoMany
8 | from cimgraph.databases import get_cim_profile, get_iec61970_301, get_namespace, get_url
9 |
10 | _log = logging.getLogger(__name__)
11 |
12 |
13 | def upload_triples_sparql(obj: object) -> str:
14 | """
15 | Generates SPARQL query string to upload graph model changes to database
16 | Args:
17 | obj: A valid cim object instance.
18 | params: ConnectionParameters object with namespace, iec61070-301 vers, etc.
19 | Returns:
20 | query_message: query string that can be used in blazegraph connection or STOMP client
21 | """
22 | cim = importlib.import_module('cimgraph.data_profile.' + get_cim_profile())
23 | many_to_many = ClassesWithManytoMany().attributes
24 |
25 | # Handling of formatting change between different 301 standard versions
26 | if int(get_iec61970_301()) > 7: # Now use rdf:about
27 | rdf_resource = 'urn:uuid:'
28 | else: # Older versions used rdf:ID
29 | rdf_resource = f"""{get_url()}#"""
30 | rdf_enum = f"""{get_namespace()}"""
31 |
32 | prefix = 'PREFIX rdf: \n'
33 | prefix += f'PREFIX cim: <{get_namespace()}>\n'
34 | prefix += 'PREFIX xsd: \n'
35 | triples = []
36 | cim_class = obj.__class__
37 | # Write object class description
38 | triple = f'\n\t<{rdf_resource}{obj.uri()}> a cim:{cim_class.__name__}.\n'
39 | triples.append(triple)
40 | # Get list of all classes from which current object inherits
41 | parent_classes = list(cim_class.__mro__)
42 | parent_classes.pop(len(parent_classes) - 1)
43 | # Iterate through parent classes
44 | for parent in parent_classes:
45 | # Iterate through attributes and associations inherited from each parent class
46 | for attribute in parent.__annotations__.keys():
47 | if attribute == 'identifier':
48 | continue
49 |
50 | try:
51 | # Check if attribute is in data profile
52 | attribute_type = cim_class.__dataclass_fields__[attribute].type
53 | rdf = f'{parent.__name__}.{attribute}'
54 | # Upload attributes that are many-to-one or are known problem classes
55 | if 'list' not in attribute_type or rdf in many_to_many:
56 | edge_class = attribute_type.split('[')[1].split(']')[0]
57 | edge = getattr(obj, attribute)
58 | # Check if typed to a class within CIM profile
59 | if edge_class in cim.__all__:
60 | if edge is not None and edge != []:
61 | # Check if edge is an enumeration
62 | if type(edge.__class__) is enum.EnumMeta:
63 | triple = f'\n\t<{rdf_resource}{obj.uri()}> cim:{parent.__name__}.{attribute} <{rdf_enum}{str(edge)}>.\n'
64 | else:
65 | if type(edge) == str:
66 | triple = f'\n\t<{rdf_resource}{obj.uri()}> cim:{parent.__name__}.{attribute} <{rdf_resource}{edge}>.\n'
67 | triples.append(triple)
68 | elif type(edge) == list:
69 | for value in edge:
70 | if type(edge) == str:
71 | triple = f'\n\t<{rdf_resource}{obj.uri()}> cim:{parent.__name__}.{attribute} <{rdf_resource}{value}>.\n'
72 | triples.append(triple)
73 | else:
74 | triple = f'\n\t<{rdf_resource}{obj.uri()}> cim:{parent.__name__}.{attribute} <{rdf_resource}{value.uri()}>.\n'
75 | triples.append(triple)
76 | else:
77 | triple = f'\n\t<{rdf_resource}{obj.uri()}> cim:{parent.__name__}.{attribute} <{rdf_resource}{edge.uri()}>.\n'
78 | triples.append(triple)
79 | else:
80 | if edge is not None and edge != [] and rdf != 'Identity.identifier':
81 | triple = f"\n\t<{rdf_resource}{obj.uri()}> cim:{parent.__name__}.{attribute} \"{str(edge)}\".\n"
82 | triples.append(triple)
83 | except:
84 | # Otherwise throw warning that attribute was invalid
85 | _log.warning(f'Unable to create rdf triple for {cim_class.__name__}.{attribute} = {str(edge)}')
86 | triples.append('}')
87 | query_message = prefix + 'INSERT DATA { ' + ''.join(triples)
88 | return query_message
89 |
--------------------------------------------------------------------------------
/cimgraph/utils/__init__.py:
--------------------------------------------------------------------------------
1 | from cimgraph.utils.get_all_data import (get_all_bus_data, get_all_data, get_all_inverter_data,
2 | get_all_limit_data, get_all_line_data, get_all_load_data,
3 | get_all_location_data, get_all_measurement_data,
4 | get_all_switch_data, get_all_transformer_data)
5 | from cimgraph.utils.mermaid import (add_mermaid_path, download_mermaid, get_mermaid,
6 | get_mermaid_path)
7 | from cimgraph.utils.object_utils import create_object
8 | from cimgraph.utils.write_csv import write_csv
9 | from cimgraph.utils.write_json import write_json_ld
10 | from cimgraph.utils.write_xml import write_xml
11 |
--------------------------------------------------------------------------------
/cimgraph/utils/get_all_data.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import logging
4 |
5 | from cimgraph.models.graph_model import GraphModel
6 |
7 | _log = logging.getLogger(__name__)
8 |
9 |
10 | def get_all_line_data(network: GraphModel) -> None:
11 | cim = network.connection.cim
12 | network.get_all_edges(cim.ACLineSegment)
13 | network.get_all_edges(cim.ACLineSegmentPhase)
14 | network.get_all_edges(cim.PerLengthPhaseImpedance)
15 | network.get_all_edges(cim.PhaseImpedanceData)
16 | network.get_all_edges(cim.WireSpacingInfo)
17 | network.get_all_edges(cim.WirePosition)
18 | network.get_all_edges(cim.OverheadWireInfo)
19 | network.get_all_edges(cim.ConcentricNeutralCableInfo)
20 | network.get_all_edges(cim.TapeShieldCableInfo)
21 |
22 |
23 | def get_all_transformer_data(network: GraphModel) -> None:
24 | cim = network.connection.cim
25 | network.get_all_edges(cim.PowerTransformer)
26 | network.get_all_edges(cim.TransformerTank)
27 | network.get_all_edges(cim.Asset)
28 | network.get_all_edges(cim.TransformerTankEnd)
29 | network.get_all_edges(cim.TransformerTankInfo)
30 | network.get_all_edges(cim.TransformerEndInfo)
31 | network.get_all_edges(cim.PowerTransformerEnd)
32 | network.get_all_edges(cim.PowerTransformerInfo)
33 | network.get_all_edges(cim.TransformerCoreAdmittance)
34 | network.get_all_edges(cim.TransformerMeshImpedance)
35 | network.get_all_edges(cim.TransformerStarImpedance)
36 | network.get_all_edges(cim.ShortCircuitTest)
37 | network.get_all_edges(cim.NoLoadTest)
38 | network.get_all_edges(cim.RatioTapChanger)
39 | network.get_all_edges(cim.TapChanger)
40 | network.get_all_edges(cim.TapChangerControl)
41 | network.get_all_edges(cim.TapChangerInfo)
42 |
43 |
44 | def get_all_load_data(network: GraphModel) -> None:
45 | cim = network.connection.cim
46 | network.get_all_edges(cim.EnergySource)
47 | network.get_all_edges(cim.EnergyConsumer)
48 | network.get_all_edges(cim.ConformLoad)
49 | network.get_all_edges(cim.NonConformLoad)
50 | network.get_all_edges(cim.EnergyConsumerPhase)
51 | network.get_all_attributes(cim.LoadResponseCharacteristic)
52 | network.get_all_attributes(cim.PowerCutZone)
53 | if 'House' in cim.__all__:
54 | network.get_all_edges(cim.House)
55 | network.get_all_edges(cim.ThermostatController)
56 |
57 |
58 | def get_all_inverter_data(network: GraphModel) -> None:
59 | cim = network.connection.cim
60 | network.get_all_edges(cim.PowerElectronicsConnection)
61 | network.get_all_edges(cim.PowerElectronicsConnectionPhase)
62 | network.get_all_edges(cim.BatteryUnit)
63 | network.get_all_edges(cim.PowerElectronicsWindUnit)
64 | if 'PhotoVoltaicUnit' in cim.__all__: # handling inconsistent case
65 | network.get_all_edges(cim.PhotoVoltaicUnit)
66 | elif 'PhotovoltaicUnit' in cim.__all__: # handling inconsistent case
67 | network.get_all_edges(cim.PhotovoltaicUnit)
68 |
69 |
70 | def get_all_switch_data(network: GraphModel) -> None:
71 | cim = network.connection.cim
72 | network.get_all_edges(cim.Switch)
73 | network.get_all_edges(cim.Sectionaliser)
74 | network.get_all_edges(cim.Jumper)
75 | network.get_all_edges(cim.Fuse)
76 | network.get_all_edges(cim.Disconnector)
77 | network.get_all_edges(cim.GroundDisconnector)
78 | network.get_all_edges(cim.ProtectedSwitch)
79 | network.get_all_edges(cim.Breaker)
80 | network.get_all_edges(cim.Recloser)
81 | network.get_all_edges(cim.LoadBreakSwitch)
82 | network.get_all_edges(cim.SwitchPhase)
83 |
84 |
85 | def get_all_bus_data(network: GraphModel) -> None:
86 | cim = network.connection.cim
87 | network.get_all_edges(cim.ConnectivityNode)
88 | network.get_all_edges(cim.Terminal)
89 | network.get_all_edges(cim.TopologicalNode)
90 | network.get_all_edges(cim.TopologicalIsland)
91 |
92 |
93 | def get_all_measurement_data(network: GraphModel) -> None:
94 | cim = network.connection.cim
95 | network.get_all_edges(cim.Terminal)
96 | network.get_all_edges(cim.Measurement)
97 | network.get_all_edges(cim.Analog)
98 | network.get_all_edges(cim.Discrete)
99 | # network.get_all_edges(cim.Accumlator)
100 | network.get_all_edges(cim.StringMeasurement)
101 |
102 |
103 | def get_all_limit_data(network: GraphModel) -> None:
104 | cim = network.connection.cim
105 | network.get_all_edges(cim.OperationalLimitSet)
106 | network.get_all_edges(cim.ActivePowerLimit)
107 | network.get_all_edges(cim.ApparentPowerLimit)
108 | network.get_all_edges(cim.VoltageLimit)
109 | network.get_all_edges(cim.CurrentLimit)
110 | network.get_all_edges(cim.OperationalLimitType)
111 |
112 |
113 | def get_all_location_data(network: GraphModel):
114 | cim = network.connection.cim
115 | network.get_all_edges(cim.CoordinateSystem)
116 | network.get_all_edges(cim.Location)
117 | network.get_all_edges(cim.PositionPoint)
118 |
119 | def get_all_data(network: GraphModel):
120 | cim = network.connection.cim
121 |
122 | # Get base data
123 | get_all_bus_data(network)
124 | get_all_line_data(network)
125 | get_all_transformer_data(network)
126 | get_all_load_data(network)
127 | get_all_inverter_data(network)
128 | get_all_limit_data(network)
129 | get_all_location_data(network)
130 | get_all_measurement_data(network)
131 |
132 | attr_only = [
133 | cim.BaseVoltage, cim.Substation, cim.SubGeographicalRegion,
134 | cim.GeographicalRegion, cim.LoadResponseCharacteristic
135 | ]
136 |
137 | # Do recursive search for missing data
138 | all_classes = []
139 | parsed_classes = []
140 | for class_type in list(network.graph.keys()):
141 | all_classes.append(class_type.__name__)
142 |
143 | while set(parsed_classes) != set(all_classes):
144 | for class_name in all_classes:
145 | if class_name not in parsed_classes:
146 | class_type = getattr(cim, class_name)
147 | if class_type not in attr_only:
148 | # print('edges', class_name)
149 | network.get_all_edges(class_type)
150 |
151 | else:
152 | # print('attributes', class_name)
153 | network.get_all_attributes(class_type)
154 |
155 | parsed_classes.append(class_name)
156 | all_classes = []
157 | for class_type in list(network.graph.keys()):
158 | all_classes.append(class_type.__name__)
159 |
--------------------------------------------------------------------------------
/cimgraph/utils/object_utils.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | from uuid import UUID
4 |
5 | _log = logging.getLogger(__name__)
6 |
7 | jsonld = dict['@id':str(UUID),'@type':str(type)]
8 | Graph = dict[type, dict[UUID, object]]
9 |
10 |
11 |
12 | # def jsonld_to_obj(cim:cim, json_ld:jsonld|str[jsonld]) -> object:
13 |
14 | # if type(json_ld) == str:
15 | # json_ld = json.loads(json_ld)
16 | # elif type(json_ld) == dict:
17 | # pass
18 | # else:
19 | # raise TypeError('json_ld input must be string or dict')
20 |
21 |
22 |
23 | def create_object(class_type:type, uri:str, graph:Graph = None) -> object:
24 | """
25 | Method for creating new objects and adding them to the graph
26 | Required Args:
27 | graph: an LPG graph from a GraphModel object
28 | class_type: a dataclass type, such as cim.ACLineSegment
29 | uri: the RDF ID or mRID of the object
30 | Returns:
31 | obj: a dataclass instance with the correct identifier
32 | """
33 | # Convert uri string to a uuid
34 | try:
35 | identifier = UUID(uri.strip('_').lower())
36 | except:
37 | _log.warning(f'URI {uri} for object {class_type.__name__} is not a valid UUID')
38 | identifier = uri
39 |
40 | if graph is None:
41 | graph = {}
42 |
43 | # Add class type to graph keys if not there
44 | if class_type not in graph:
45 | graph[class_type] = {}
46 |
47 | # Check if object exists in graph
48 | if identifier in graph[class_type]:
49 | obj = graph[class_type][identifier]
50 |
51 | # If not there, create a new object and add to graph
52 | else:
53 | obj = class_type()
54 | obj.uuid(uri = uri)
55 | graph[class_type][identifier] = obj
56 |
57 | return obj
58 |
--------------------------------------------------------------------------------
/cimgraph/utils/readme.md:
--------------------------------------------------------------------------------
1 | # CIM-Graph Utils
2 |
3 | ## Overview
4 |
5 | ## Query Shortcuts
6 |
7 | ### `get_all_line_data(network)`
8 |
9 | ```mermaid
10 |
11 | zenuml
12 | title get_all_line_data(network)
13 | @Actor User
14 | @AzureFunction utils
15 | @PubSub GraphModel
16 | @AzureBackup Database
17 |
18 |
19 | User -> utils.get_all_line_data(network) {
20 | GraphModel.get_all_edges(cim.ACLineSegment) {
21 | Database.query {
22 | return objects
23 | }
24 | }
25 | GraphModel.get_all_edges(cim.ACLineSegmentPhase) {
26 | Database.query {
27 | return objects
28 | }
29 | }
30 | GraphModel.get_all_edges(cim.PhaseImpedanceData) {
31 | Database.query {
32 | return objects
33 | }
34 | }
35 | GraphModel.get_all_edges(cim.WireSpacingInfo) {
36 | Database.query {
37 | return objects
38 | }
39 | }
40 | }
41 | ```
42 |
--------------------------------------------------------------------------------
/cimgraph/utils/write_csv.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import logging
4 | import os
5 | from zipfile import ZIP_DEFLATED, ZipFile
6 |
7 | from cimgraph.models.graph_model import GraphModel
8 |
9 | _log = logging.getLogger(__name__)
10 |
11 |
12 | def write_csv(network: GraphModel, destination: str) -> None:
13 |
14 | try:
15 | os.remove(f'{destination}/{network.container.mRID}.zip')
16 | except:
17 | pass
18 |
19 | for cim_class in list(network.graph.keys()):
20 | attributes = list(cim_class.__dataclass_fields__.keys())
21 |
22 | with open(f'{cim_class.__name__}.csv', 'w') as csv_file:
23 | header = ','.join(attributes)
24 | csv_file.write(f'{header}\n')
25 |
26 | with open(f'{cim_class.__name__}.csv', 'a') as csv_file:
27 | # Get JSON-LD representation of model
28 | table = network.__dumps__(cim_class, show_empty=True, json_ld=True)
29 | fields = cim_class.__dataclass_fields__
30 | # Insert each power system object in CIMantic Graphs model
31 | for obj in table.values():
32 | # Create SQL Query
33 | row = []
34 | # sql_params = [self.username, int(time.time())]
35 |
36 | # Iterate through each attribute
37 | for attr in list(obj.keys()):
38 |
39 | if 'List' in fields[attr].type:
40 | row.append(f'"{obj[attr]}"') # JSON-LD List of RDF links
41 | elif 'float' in fields[attr].type:
42 | row.append(obj[attr]) # float
43 | else:
44 | row.append(f'"{obj[attr]}"') # free text
45 | csv_row = ','.join(row)
46 | csv_file.write(f'{csv_row}\n')
47 |
48 | with ZipFile(f'{destination}/{network.container.mRID}.zip', 'a', ZIP_DEFLATED) as zip_file:
49 | zip_file.write(f'{cim_class.__name__}.csv')
50 | os.remove(f'{cim_class.__name__}.csv')
51 |
--------------------------------------------------------------------------------
/cimgraph/utils/write_json.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import enum
4 | import json
5 | import logging
6 | from dataclasses import dataclass, field, is_dataclass
7 |
8 | from cimgraph.data_profile.known_problem_classes import ClassesWithManytoMany
9 | from cimgraph.models.graph_model import GraphModel
10 |
11 | _log = logging.getLogger(__name__)
12 |
13 |
14 | def write_json_ld(network: GraphModel, filename: str, namespaces: dict=None, indent:int=4) -> None:
15 | """
16 | Write the network graph to an XML file.
17 |
18 | Args:
19 | network (GraphModel): The network graph to be written to an XML file.
20 | namespaces (dict): A dictionary of namespaces to be used in the XML file. The key is the namespace prefix and the value is the namespace URI. Defaults to CIM100.
21 |
22 | Returns:
23 | None
24 |
25 | """
26 | if namespaces is None:
27 | namespaces = {'cim': 'http://iec.ch/TC57/CIM100#',
28 | 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'}
29 |
30 | # Create reverse lookup for namespace
31 | reverse_ns_lookup = {v: k for k, v in namespaces.items()}
32 |
33 | classes_with_many_to_many = ClassesWithManytoMany()
34 | many_to_many = classes_with_many_to_many.attributes
35 |
36 | # Write XML header and namespace declarations
37 | f = open(filename, 'w', encoding='utf-8')
38 | f.write('{\n')
39 |
40 | context = {
41 | 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
42 | 'cim': 'http://ucaiug.org/ns/CIM#',
43 | 'eu': 'http://iec.ch/TC57/CIM100-European#',
44 | 'dcterms': 'http://purl.org/dc/terms/',
45 | 'dcat': 'http://www.w3.org/ns/dcat#',
46 | 'prov': 'http://www.w3.org/ns/prov#',
47 | 'xsd': 'http://www.w3.org/2001/XMLSchema#'
48 | }
49 | f.write(' '*indent + '"@context": ')
50 | f.write(json.dumps(context, indent=indent).replace('\n', '\n' + ' '*indent)+',\n')
51 |
52 | # TODO: Add support for PROV, DCAT, and timestamping of model data
53 |
54 | f.write(' '*indent + '"@graph": [\n')
55 |
56 | # Write each object in the network graph to the XML file
57 | for root_class in list(network.graph.keys()):
58 | counter = 0
59 | for obj in network.graph[root_class].values():
60 |
61 | cim_class = obj.__class__
62 | dump = {}
63 | dump['@id'] = 'urn:uuid:'+obj.uri()
64 | dump['@type'] = f'cim:{cim_class.__name__}'
65 | parent_classes = list(cim_class.__mro__)
66 | parent_classes.pop(len(parent_classes) - 1)
67 | for parent in parent_classes:
68 | for attribute in parent.__annotations__.keys():
69 | # Skip over Identity.identifier attribute
70 | if attribute == 'identifier':
71 | continue
72 | attribute_type = cim_class.__dataclass_fields__[attribute].type
73 | rdf = f'{parent.__name__}.{attribute}'
74 | attr_ns = cim_class.__dataclass_fields__[attribute].metadata['namespace']
75 | ns_prefix = reverse_ns_lookup[attr_ns]
76 |
77 | # Upload attributes that are many-to-one or are known problem classes
78 | if 'list' not in attribute_type or rdf in many_to_many:
79 | edge_class = attribute_type.split('[')[1].split(']')[0]
80 | edge = getattr(obj, attribute)
81 | # Check if attribute is association to a class object
82 | if edge_class in network.connection.cim.__all__:
83 | if edge is not None and edge != []:
84 | if type(edge.__class__) is enum.EnumMeta:
85 | dump[f'{ns_prefix}:{parent.__name__}.{attribute}'] = f'{attr_ns}:{str(edge)}'
86 | elif type(edge) is str or type(edge) is bool or type(edge) is float:
87 | dump[f'{ns_prefix}:{parent.__name__}.{attribute}'] = str(edge)
88 | elif type(edge) is list:
89 | for value in edge:
90 | if type(edge.__class__) is enum.EnumMeta:
91 | dump[f'{ns_prefix}:{parent.__name__}.{attribute}'] = f'{attr_ns}:{str(value)}'
92 | elif type(edge) is str or type(edge) is bool or type(edge) is float:
93 | dump[f'{ns_prefix}:{parent.__name__}.{attribute}'] = str(value)
94 | else:
95 | dump[f'{ns_prefix}:{parent.__name__}.{attribute}'] = json.loads(value.__repr__())
96 | else:
97 | dump[f'{ns_prefix}:{parent.__name__}.{attribute}'] = json.loads(edge.__repr__())
98 | else:
99 | if edge is not None and edge != [] and rdf != 'Identity.identifier':
100 | dump[f'{ns_prefix}:{parent.__name__}.{attribute}'] = str(edge)
101 |
102 | f.write(' '*indent*2 + json.dumps(dump, indent=indent).replace('\n', '\n' + ' '*indent*2)+',\n')
103 | counter = counter + 1
104 | _log.info(f'wrote {counter} {cim_class.__name__} objects')
105 | f.close()
106 | # Remove the last two characters before the closing bracket
107 | f = open(filename, 'rb+')
108 | f.seek(-2, 2)
109 | f.truncate()
110 | f = open(filename, 'a', encoding='utf-8')
111 | f.write('\n'+' '*indent + ']\n'+'}')
112 | f.close()
113 |
--------------------------------------------------------------------------------
/cimgraph/utils/write_xml.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import enum
4 | import logging
5 |
6 | from cimgraph.data_profile.known_problem_classes import ClassesWithManytoMany
7 | from cimgraph.models.graph_model import GraphModel
8 |
9 | _log = logging.getLogger(__name__)
10 |
11 |
12 | def write_xml(network: GraphModel, filename: str, namespaces: dict=None) -> None:
13 | """
14 | Write the network graph to an XML file.
15 |
16 | Args:
17 | network (GraphModel): The network graph to be written to an XML file.
18 | namespaces (dict): A dictionary of namespaces to be used in the XML file. The key is the namespace prefix and the value is the namespace URI. Defaults to CIM100.
19 |
20 | Returns:
21 | None
22 |
23 | """
24 | if namespaces is None:
25 | namespaces = {'cim': 'http://iec.ch/TC57/CIM100#',
26 | 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'}
27 |
28 | # Create reverse lookup for namespace
29 | reverse_ns_lookup = {v: k for k, v in namespaces.items()}
30 |
31 | iec61970_301 = network.connection.iec61970_301
32 | classes_with_many_to_many = ClassesWithManytoMany()
33 | many_to_many = classes_with_many_to_many.attributes
34 | # Handling of formatting change between different 301 standard versions
35 | if int(iec61970_301) > 7:
36 | rdf_header = 'rdf:about="urn:uuid:'
37 | rdf_resource = 'urn:uuid:'
38 | else:
39 | rdf_header = 'rdf:ID="'
40 | rdf_resource = '#'
41 |
42 | # Write XML header and namespace declarations
43 | f = open(filename, 'w', encoding='utf-8')
44 | header = '\n'
45 | header += '\n'
47 | header += f'\n'
51 | header += '\n'
53 | f.write(header)
54 |
55 | # Write each object in the network graph to the XML file
56 | for root_class in list(network.graph.keys()):
57 | counter = 0
58 | for obj in network.graph[root_class].values():
59 | cim_class = obj.__class__
60 | header = f'\n'
61 | f.write(header)
62 | parent_classes = list(cim_class.__mro__)
63 | parent_classes.pop(len(parent_classes) - 1)
64 | for parent in parent_classes:
65 | for attribute in parent.__annotations__.keys():
66 | # Skip over Identity.identifier attribute
67 | if attribute == 'identifier':
68 | continue
69 | attribute_type = cim_class.__dataclass_fields__[attribute].type
70 | rdf = f'{parent.__name__}.{attribute}'
71 | attr_ns = cim_class.__dataclass_fields__[attribute].metadata['namespace']
72 | ns_prefix = reverse_ns_lookup[attr_ns]
73 | # Upload attributes that are many-to-one or are known problem classes
74 | if 'list' not in attribute_type or rdf in many_to_many:
75 | edge_class = attribute_type.split('[')[1].split(']')[0]
76 | edge = getattr(obj, attribute)
77 | # Check if attribute is association to a class object
78 | if edge_class in network.connection.cim.__all__:
79 | if edge is not None and edge != []:
80 | if type(edge.__class__) is enum.EnumMeta:
81 | resource = f'rdf:resource="{attr_ns}{str(edge)}"'
82 | row = f' <{ns_prefix}:{parent.__name__}.{attribute} {resource}/>\n'
83 | f.write(row)
84 | elif type(edge) is str or type(edge) is bool or type(edge) is float:
85 | row = f' <{ns_prefix}:{parent.__name__}.{attribute}>{str(edge)}{ns_prefix}:{parent.__name__}.{attribute}>\n'
86 | f.write(row)
87 | elif type(edge) is list:
88 | for value in edge:
89 | #TODO: lookup how to handle multiple rows of same value
90 | if type(value.__class__) is enum.EnumMeta:
91 | resource = f'rdf:resource="{attr_ns}{str(edge)}"'
92 | row = f' <{ns_prefix}:{parent.__name__}.{attribute} {resource}/>\n'
93 | f.write(row)
94 | elif type(value) is str or type(value) is bool or type(value) is float:
95 | row = f' <{ns_prefix}:{parent.__name__}.{attribute}>{str(value)}{ns_prefix}:{parent.__name__}.{attribute}>\n'
96 | f.write(row)
97 | else:
98 | resource = f'rdf:resource="{rdf_resource}{value.uri()}"'
99 | row = f' <{ns_prefix}:{parent.__name__}.{attribute} {resource}/>\n'
100 | f.write(row)
101 | else:
102 | # try:
103 | resource = f'rdf:resource="{rdf_resource}{edge.uri()}"'
104 | row = f' <{ns_prefix}:{parent.__name__}.{attribute} {resource}/>\n'
105 | f.write(row)
106 | # except:
107 | # _log.warning(obj.__dict__)
108 | else:
109 | if edge is not None and edge != [] and rdf != 'Identity.identifier':
110 | row = f' <{ns_prefix}:{parent.__name__}.{attribute}>{str(edge)}{ns_prefix}:{parent.__name__}.{attribute}>\n'
111 | f.write(row)
112 | tail = f'\n'
113 | f.write(tail)
114 | counter = counter + 1
115 | _log.info(f'wrote {counter} {cim_class.__name__} objects')
116 | f.write('')
117 | f.close()
118 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 |
3 | services:
4 |
5 | neo4j-apoc:
6 | image: neo4j:5.22-community
7 | ports:
8 | - 7474:7474
9 | - 7687:7687
10 | environment:
11 | - NEO4J_PLUGINS= ["apoc", "n10s"]
12 | - NEO4J_AUTH=none
13 | - NEO4J_dbms_memory_transaction_total_max=10g
14 | - NEO4J_server_memory_heap_max__size=10g
15 | - NEO4J_apoc_export_file_enabled=true
16 | - NEO4J_apoc_import_file_enabled=true
17 | - NEO4J_apoc_import_file_use__neo4j__config=true
18 | volumes:
19 | - ./docker/neo4j/data:/data
20 | - ./docker/neo4j/plugins:/plugins
21 |
--------------------------------------------------------------------------------
/example.env:
--------------------------------------------------------------------------------
1 | CIMG_CIM_PROFILE = "cimhub_2023"
2 | CIMG_URL = "http://localhost:8889/bigdata/namespace/kb/sparql"
3 | CIMG_DATABASE = "powergridmodel"
4 | CIMG_HOST = "localhost"
5 | CIMG_PORT = "61613"
6 | CIMG_USERNAME = "system"
7 | CIMG_PASSWORD = "manager"
8 | CIMG_NAMESPACE = "http://iec.ch/TC57/CIM100#"
9 | CIMG_IEC61970_301 = "8"
10 | CIMG_USE_UNITS = "False"
11 |
--------------------------------------------------------------------------------
/examples/debug notebooks/glm.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "from cimgraph.databases import ConnectionParameters, RDFlibConnection, BlazegraphConnection, Neo4jConnection\n",
10 | "from cimgraph.models import NodeBreakerModel, BusBranchModel, FeederModel\n",
11 | "from cimgraph.models.graph_model import new_mrid\n",
12 | "import importlib"
13 | ]
14 | },
15 | {
16 | "cell_type": "code",
17 | "execution_count": 2,
18 | "metadata": {},
19 | "outputs": [],
20 | "source": [
21 | "data_profile = 'gridlabd'\n",
22 | "glm = importlib.import_module('cimgraph.data_profile.' + data_profile)\n",
23 | "\n",
24 | "cim_profile = 'cimhub_2023'\n",
25 | "cim = importlib.import_module('cimgraph.data_profile.' + cim_profile)"
26 | ]
27 | },
28 | {
29 | "cell_type": "code",
30 | "execution_count": 3,
31 | "metadata": {},
32 | "outputs": [],
33 | "source": [
34 | "# Neo4J Connection\n",
35 | "params = ConnectionParameters(url = \"neo4j://localhost:7687/neo4j\", database=\"neo4j\", cim_profile=data_profile)\n",
36 | "neo4j = Neo4jConnection(params)\n",
37 | "\n",
38 | "feeder_mrid = 'demo'\n",
39 | "feeder = cim.Feeder(mRID = feeder_mrid)\n",
40 | "network = FeederModel(connection=neo4j, container=feeder, distributed=False)"
41 | ]
42 | },
43 | {
44 | "cell_type": "code",
45 | "execution_count": 4,
46 | "metadata": {},
47 | "outputs": [
48 | {
49 | "data": {
50 | "text/plain": [
51 | "node(name=None, phases=None, mRID=None, bustype=None, nominal_voltage=None, voltage_A=None, voltage_B=None, voltage_C=None, overhead_line=[], recloser=[])"
52 | ]
53 | },
54 | "execution_count": 4,
55 | "metadata": {},
56 | "output_type": "execute_result"
57 | }
58 | ],
59 | "source": [
60 | "glm.node()"
61 | ]
62 | },
63 | {
64 | "cell_type": "code",
65 | "execution_count": 5,
66 | "metadata": {},
67 | "outputs": [],
68 | "source": [
69 | "node1 = glm.node(name='node1', mRID = new_mrid())\n",
70 | "node2 = glm.node(name='node2', mRID = new_mrid())\n",
71 | "\n",
72 | "network.add_to_graph(node1)\n",
73 | "network.add_to_graph(node2)\n"
74 | ]
75 | },
76 | {
77 | "cell_type": "code",
78 | "execution_count": 8,
79 | "metadata": {},
80 | "outputs": [],
81 | "source": [
82 | "node1.nominal_voltage = 4160\n",
83 | "node1.phases = cim.OrderedPhaseCodeKind.ABCN"
84 | ]
85 | },
86 | {
87 | "cell_type": "code",
88 | "execution_count": 11,
89 | "metadata": {},
90 | "outputs": [],
91 | "source": [
92 | "line = glm.overhead_line(name='new_line',_from=node1, to=node2)\n",
93 | "network.add_to_graph(line)"
94 | ]
95 | },
96 | {
97 | "cell_type": "code",
98 | "execution_count": 12,
99 | "metadata": {},
100 | "outputs": [
101 | {
102 | "name": "stdout",
103 | "output_type": "stream",
104 | "text": [
105 | "{\n",
106 | " \"null\": {\n",
107 | " \"name\": \"new_line\",\n",
108 | " \"_from\": \"61141626-c03f-4b6e-95a3-cf338fa11dc2\",\n",
109 | " \"to\": \"d926ab4b-5309-454c-8a35-4640cb7d3632\"\n",
110 | " }\n",
111 | "}\n"
112 | ]
113 | }
114 | ],
115 | "source": [
116 | "network.pprint(glm.overhead_line)"
117 | ]
118 | },
119 | {
120 | "cell_type": "code",
121 | "execution_count": null,
122 | "metadata": {},
123 | "outputs": [],
124 | "source": [
125 | "line."
126 | ]
127 | }
128 | ],
129 | "metadata": {
130 | "kernelspec": {
131 | "display_name": ".venv",
132 | "language": "python",
133 | "name": "python3"
134 | },
135 | "language_info": {
136 | "codemirror_mode": {
137 | "name": "ipython",
138 | "version": 3
139 | },
140 | "file_extension": ".py",
141 | "mimetype": "text/x-python",
142 | "name": "python",
143 | "nbconvert_exporter": "python",
144 | "pygments_lexer": "ipython3",
145 | "version": "3.10.12"
146 | }
147 | },
148 | "nbformat": 4,
149 | "nbformat_minor": 2
150 | }
151 |
--------------------------------------------------------------------------------
/examples/feeder_example.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Distribution Feeder Example"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "## Import Libraries"
15 | ]
16 | },
17 | {
18 | "cell_type": "code",
19 | "execution_count": null,
20 | "metadata": {},
21 | "outputs": [],
22 | "source": [
23 | "from cimgraph.databases import ConnectionParameters\n",
24 | "from cimgraph.databases import RDFlibConnection, BlazegraphConnection, Neo4jConnection, GraphDBConnection\n",
25 | "from cimgraph.models import FeederModel\n",
26 | "import cimgraph.utils as utils\n",
27 | "\n",
28 | "import importlib"
29 | ]
30 | },
31 | {
32 | "cell_type": "markdown",
33 | "metadata": {},
34 | "source": [
35 | "## Initialize Model - Read From File"
36 | ]
37 | },
38 | {
39 | "cell_type": "code",
40 | "execution_count": null,
41 | "metadata": {},
42 | "outputs": [],
43 | "source": [
44 | "# Specify CIM data profile -- Feature/SETO branch\n",
45 | "cim_profile = 'cimhub_2023'\n",
46 | "cim = importlib.import_module('cimgraph.data_profile.' + cim_profile)"
47 | ]
48 | },
49 | {
50 | "cell_type": "code",
51 | "execution_count": null,
52 | "metadata": {},
53 | "outputs": [],
54 | "source": [
55 | "# RDFLib File Reader Connection\n",
56 | "params = ConnectionParameters(filename=\"../cimgraph/tests/test_models/ieee13.xml\", cim_profile='cimhub_2023', iec61970_301=8)\n",
57 | "rdf = RDFlibConnection(params)"
58 | ]
59 | },
60 | {
61 | "cell_type": "code",
62 | "execution_count": null,
63 | "metadata": {},
64 | "outputs": [],
65 | "source": [
66 | "feeder_mrid = \"49AD8E07-3BF9-A4E2-CB8F-C3722F837B62\"\n",
67 | "feeder = cim.Feeder(mRID = feeder_mrid)\n",
68 | "network = FeederModel(connection=rdf, container=feeder, distributed=False)"
69 | ]
70 | },
71 | {
72 | "cell_type": "markdown",
73 | "metadata": {},
74 | "source": [
75 | "## Initialize Model - Read from Database"
76 | ]
77 | },
78 | {
79 | "cell_type": "code",
80 | "execution_count": null,
81 | "metadata": {},
82 | "outputs": [],
83 | "source": [
84 | "cim_profile = 'rc4_2021'\n",
85 | "cim = importlib.import_module('cimgraph.data_profile.' + cim_profile)"
86 | ]
87 | },
88 | {
89 | "cell_type": "code",
90 | "execution_count": null,
91 | "metadata": {},
92 | "outputs": [],
93 | "source": [
94 | "params = ConnectionParameters(url = \"http://localhost:8889/bigdata/namespace/kb/sparql\", cim_profile=cim_profile, iec61970_301=7)\n",
95 | "bg = BlazegraphConnection(params)"
96 | ]
97 | },
98 | {
99 | "cell_type": "code",
100 | "execution_count": null,
101 | "metadata": {},
102 | "outputs": [],
103 | "source": [
104 | "feeder_mrid = \"_49AD8E07-3BF9-A4E2-CB8F-C3722F837B62\"\n",
105 | "feeder = cim.Feeder(mRID = feeder_mrid)\n",
106 | "network = FeederModel(connection=bg, container=feeder, distributed=False)"
107 | ]
108 | },
109 | {
110 | "cell_type": "markdown",
111 | "metadata": {},
112 | "source": [
113 | "## Query Model"
114 | ]
115 | },
116 | {
117 | "cell_type": "code",
118 | "execution_count": null,
119 | "metadata": {},
120 | "outputs": [],
121 | "source": [
122 | "# Get all ACLineSegment Data\n",
123 | "network.get_all_edges(cim.ACLineSegment)"
124 | ]
125 | },
126 | {
127 | "cell_type": "code",
128 | "execution_count": null,
129 | "metadata": {},
130 | "outputs": [],
131 | "source": [
132 | "# Print all ACLineSegment Data\n",
133 | "network.pprint(cim.ACLineSegment, show_empty=False, json_ld=False)"
134 | ]
135 | },
136 | {
137 | "cell_type": "code",
138 | "execution_count": null,
139 | "metadata": {},
140 | "outputs": [],
141 | "source": [
142 | "## Get Measurements\n",
143 | "network.get_all_edges(cim.Analog)\n",
144 | "for line in network.graph[cim.ACLineSegment].values():\n",
145 | " for meas in line.Measurements:\n",
146 | " print('Measurement: ', meas.name, ', type:', meas.measurementType, ', phases:', meas.phases)"
147 | ]
148 | },
149 | {
150 | "cell_type": "markdown",
151 | "metadata": {},
152 | "source": [
153 | "## Utils Shortcuts"
154 | ]
155 | },
156 | {
157 | "cell_type": "code",
158 | "execution_count": null,
159 | "metadata": {},
160 | "outputs": [],
161 | "source": [
162 | "# Get Bus Coordinates\n",
163 | "utils.get_all_bus_locations(network)"
164 | ]
165 | },
166 | {
167 | "cell_type": "code",
168 | "execution_count": null,
169 | "metadata": {},
170 | "outputs": [],
171 | "source": [
172 | "# Bulk query for all classes related to lines\n",
173 | "utils.get_all_line_data(network)"
174 | ]
175 | },
176 | {
177 | "cell_type": "code",
178 | "execution_count": null,
179 | "metadata": {},
180 | "outputs": [],
181 | "source": [
182 | "# Print impedance data\n",
183 | "for impedance in network.graph[cim.PerLengthPhaseImpedance].values():\n",
184 | " print('\\n name:', impedance.name)\n",
185 | " for data in impedance.PhaseImpedanceData:\n",
186 | " print('row:', data.row, 'col:', data.column, 'r:', data.r, 'x:', data.x, 'b:', data.b)"
187 | ]
188 | },
189 | {
190 | "cell_type": "code",
191 | "execution_count": null,
192 | "metadata": {},
193 | "outputs": [],
194 | "source": [
195 | "# Get entire network model\n",
196 | "utils.get_all_data(network)"
197 | ]
198 | },
199 | {
200 | "cell_type": "code",
201 | "execution_count": null,
202 | "metadata": {},
203 | "outputs": [],
204 | "source": [
205 | "utils.write_xml(network, \"../cimgraph/tests/test_output/ieee13test.xml\")"
206 | ]
207 | },
208 | {
209 | "cell_type": "code",
210 | "execution_count": null,
211 | "metadata": {},
212 | "outputs": [],
213 | "source": [
214 | "utils.write_csv(network, '../cimgraph/tests/test_output')"
215 | ]
216 | }
217 | ],
218 | "metadata": {
219 | "kernelspec": {
220 | "display_name": ".venv",
221 | "language": "python",
222 | "name": "python3"
223 | },
224 | "language_info": {
225 | "codemirror_mode": {
226 | "name": "ipython",
227 | "version": 3
228 | },
229 | "file_extension": ".py",
230 | "mimetype": "text/x-python",
231 | "name": "python",
232 | "nbconvert_exporter": "python",
233 | "pygments_lexer": "ipython3",
234 | "version": "3.10.12"
235 | }
236 | },
237 | "nbformat": 4,
238 | "nbformat_minor": 2
239 | }
240 |
--------------------------------------------------------------------------------
/examples/node_breaker_example.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "import importlib\n",
10 | "cim_profile = \"rc4_2021\"\n",
11 | "cim = importlib.import_module(\"cimgraph.data_profile.\" + cim_profile)"
12 | ]
13 | },
14 | {
15 | "cell_type": "code",
16 | "execution_count": 2,
17 | "metadata": {},
18 | "outputs": [],
19 | "source": [
20 | "from cimgraph.databases import ConnectionParameters\n",
21 | "from cimgraph.databases import RDFlibConnection\n",
22 | "from cimgraph.models import NodeBreakerModel\n",
23 | "import json"
24 | ]
25 | },
26 | {
27 | "cell_type": "code",
28 | "execution_count": 3,
29 | "metadata": {},
30 | "outputs": [],
31 | "source": [
32 | "# RDFLib File Reader Connection\n",
33 | "params = ConnectionParameters(filename=\"../cimgraph/tests/test_models/maple10nodebreaker.xml\", cim_profile=cim_profile, iec61970_301=7)\n",
34 | "rdf = RDFlibConnection(params)"
35 | ]
36 | },
37 | {
38 | "cell_type": "code",
39 | "execution_count": 4,
40 | "metadata": {},
41 | "outputs": [],
42 | "source": [
43 | "geo_region_id = \"_EE4C60AE-550D-4599-92F4-022DF3118B3C\"\n",
44 | "geo_region = cim.GeographicalRegion(mRID = geo_region_id)"
45 | ]
46 | },
47 | {
48 | "cell_type": "code",
49 | "execution_count": 5,
50 | "metadata": {},
51 | "outputs": [],
52 | "source": [
53 | "network = NodeBreakerModel(connection=rdf, container=geo_region, distributed=False)"
54 | ]
55 | },
56 | {
57 | "cell_type": "code",
58 | "execution_count": 6,
59 | "metadata": {},
60 | "outputs": [],
61 | "source": [
62 | "network = NodeBreakerModel(connection=rdf, container=geo_region, distributed=True)"
63 | ]
64 | },
65 | {
66 | "cell_type": "code",
67 | "execution_count": 20,
68 | "metadata": {},
69 | "outputs": [
70 | {
71 | "name": "stdout",
72 | "output_type": "stream",
73 | "text": [
74 | "subregion small\n",
75 | "substation maple10bus_sub2\n",
76 | "voltage level SUB2_115.0_B1\n",
77 | "voltage level SUB2_4.16_B1\n",
78 | "voltage level SUB2_34.5_B1\n",
79 | "voltage level SUB2_230.0_B1\n",
80 | "feeder feeder_11 contains PV aggregates\n",
81 | "s10_der_ag2 1.0 MW\n",
82 | "feeder feeder_9 contains PV aggregates\n",
83 | "s9_der_ag2 2.0 MW\n",
84 | "feeder feeder_8 contains PV aggregates\n",
85 | "s9_der_ag1 1.0 MW\n",
86 | "feeder feeder_10 contains PV aggregates\n",
87 | "s10_der_ag1 5.0 MW\n",
88 | "substation maple10bus_sub1\n",
89 | "voltage level SUB1_230.0_B1\n",
90 | "voltage level SUB1_115.0_B1\n",
91 | "voltage level SUB1_12.47_B1\n",
92 | "voltage level SUB1_34.5_B1\n",
93 | "feeder feeder_4 contains PV aggregates\n",
94 | "s7_der_ag4 1.0 MW\n",
95 | "feeder feeder_1 contains PV aggregates\n",
96 | "s7_der_ag1 2.0 MW\n",
97 | "feeder feeder_5 contains PV aggregates\n",
98 | "s8_der_ag1 5.0 MW\n",
99 | "feeder feeder_6 contains PV aggregates\n",
100 | "s8_der_ag2 2.0 MW\n",
101 | "feeder feeder_3 contains PV aggregates\n",
102 | "s7_der_ag3 2.0 MW\n",
103 | "feeder feeder_7 contains PV aggregates\n",
104 | "s8_der_ag3 6.0 MW\n",
105 | "feeder feeder_2 contains PV aggregates\n",
106 | "none\n"
107 | ]
108 | }
109 | ],
110 | "source": [
111 | "for sr_area in network.distributed_areas[cim.SubGeographicalRegion].values():\n",
112 | " print(\"subregion\", sr_area.container.name)\n",
113 | " for sub_area in sr_area.distributed_areas[cim.Substation].values():\n",
114 | " print(\"substation\", sub_area.container.name)\n",
115 | "\n",
116 | " for vl_area in sub_area.distributed_areas[cim.VoltageLevel].values():\n",
117 | " print(\"voltage level\", vl_area.container.name)\n",
118 | " \n",
119 | " for feeder_area in sub_area.distributed_areas[cim.Feeder].values():\n",
120 | " print(\"feeder\", feeder_area.container.name, \"contains PV aggregates\")\n",
121 | " feeder_area.get_all_edges(cim.PowerElectronicsConnection)\n",
122 | " feeder_area.get_all_edges(cim.PhotovoltaicUnit)\n",
123 | " if cim.PowerElectronicsConnection in feeder_area.graph:\n",
124 | " for pv in feeder_area.graph[cim.PowerElectronicsConnection].values():\n",
125 | " print(pv.name, float(pv.p)/1000000, \"MW\")\n",
126 | " else:\n",
127 | " print(\"none\")\n"
128 | ]
129 | }
130 | ],
131 | "metadata": {
132 | "kernelspec": {
133 | "display_name": ".venv",
134 | "language": "python",
135 | "name": "python3"
136 | },
137 | "language_info": {
138 | "codemirror_mode": {
139 | "name": "ipython",
140 | "version": 3
141 | },
142 | "file_extension": ".py",
143 | "mimetype": "text/x-python",
144 | "name": "python",
145 | "nbconvert_exporter": "python",
146 | "pygments_lexer": "ipython3",
147 | "version": "3.10.12"
148 | }
149 | },
150 | "nbformat": 4,
151 | "nbformat_minor": 2
152 | }
153 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "cim-graph"
3 | version = "0.3.1a2"
4 | description = "CIM Graph"
5 | authors = ["A. Anderson <19935503+AAndersn@users.noreply.github.com>",
6 | "C. Allwardt <3979063+craig8@users.noreply.github.com>"]
7 | packages = [
8 | { include = "cimgraph" }
9 | ]
10 | readme = "README.md"
11 |
12 | [tool.poetry.dependencies]
13 | python = ">=3.10,<4.0"
14 | defusedxml = "^0.7.1"
15 | SPARQLWrapper = "^2.0.0"
16 | neo4j = "^5.10.0"
17 | rdflib = "^7.0.0"
18 | oxrdflib = "^0.3.6"
19 | mysql-connector-python = "^9.2.0"
20 | gridappsd-python = {version = "^2025.3.2a1", allow-prereleases = true}
21 | nest-asyncio = "^1.5.8"
22 | mermaid-python = "^0.1"
23 |
24 | [tool.poetry.group.dev.dependencies]
25 | pytest = "^8.3.5"
26 | pre-commit = "^2.17.0"
27 | graphviz = "^0.19.1"
28 | ipykernel = "^6.27.1"
29 |
30 | [build-system]
31 | requires = ["poetry-core>=1.2.0"]
32 | build-backend = "poetry.core.masonry.api"
33 |
34 | [tool.yapfignore]
35 | ignore_patterns = [
36 | ".venv/**",
37 | ".pytest_cache/**",
38 | "dist/**",
39 | "docs/**",
40 | "docker/**",
41 | "cimgraph/data_profile/**",
42 | "!*.py"
43 | ]
44 |
45 | [tool.yapf]
46 | based_on_style = "pep8"
47 | spaces_before_comment = 4
48 | column_limit = 99
49 | split_before_logical_operator = true
50 |
51 | [tool.isort]
52 | line_length = 99
53 |
--------------------------------------------------------------------------------
/tests/test_CIM_PROFILE.py:
--------------------------------------------------------------------------------
1 | from cimgraph.data_profile import CIM_PROFILE
2 |
3 |
4 | def test_CIM_PROFILE():
5 | assert len(CIM_PROFILE) == 5
6 | assert CIM_PROFILE.CIM17V40.value == 'cim17v40'
7 | assert CIM_PROFILE.CIMHUB_2023.value == 'cimhub_2023'
8 | assert CIM_PROFILE.CIMHUB_UFLS.value == 'cimhub_ufls'
9 | assert CIM_PROFILE.RC4_2021.value == 'rc4_2021'
10 | assert CIM_PROFILE.UFLS.value == 'ufls'
11 |
--------------------------------------------------------------------------------
/tests/test_blazegraph_seto.py:
--------------------------------------------------------------------------------
1 | import os
2 | import unittest
3 | from uuid import UUID
4 |
5 | import cimgraph.data_profile.cimhub_2023 as cim
6 | from cimgraph import utils
7 | from cimgraph.databases import BlazegraphConnection
8 | from cimgraph.models import FeederModel
9 | from cimgraph.queries import sparql
10 |
11 |
12 | class TestBlazegraphSETO(unittest.TestCase):
13 |
14 | def setUp(self):
15 | # Backup environment variables
16 | self.original_env = {
17 | 'CIMG_CIM_PROFILE': os.getenv('CIMG_CIM_PROFILE'),
18 | 'CIMG_URL': os.getenv('CIMG_URL'),
19 | 'CIMG_DATABASE': os.getenv('CIMG_DATABASE'),
20 | 'CIMG_HOST': os.getenv('CIMG_HOST'),
21 | 'CIMG_PORT': os.getenv('CIMG_PORT'),
22 | 'CIMG_USERNAME': os.getenv('CIMG_USERNAME'),
23 | 'CIMG_PASSWORD': os.getenv('CIMG_PASSWORD'),
24 | 'CIMG_NAMESPACE': os.getenv('CIMG_NAMESPACE'),
25 | 'CIMG_IEC61970_301': os.getenv('CIMG_IEC61970_301'),
26 | 'CIMG_USE_UNITS': os.getenv('CIMG_USE_UNITS'),
27 | }
28 |
29 | # Set environment variables for testing
30 | os.environ['CIMG_CIM_PROFILE'] = 'cimhub_2023'
31 | os.environ['CIMG_URL'] = 'http://localhost:8889/bigdata/namespace/kb/sparql'
32 | os.environ['CIMG_NAMESPACE'] = 'http://iec.ch/TC57/CIM100#'
33 | os.environ['CIMG_IEC61970_301'] = '8'
34 | os.environ['CIMG_USE_UNITS'] = 'false'
35 |
36 | self.feeder_mrid = '49AD8E07-3BF9-A4E2-CB8F-C3722F837B62'
37 |
38 | def tearDown(self):
39 | # Restore environment variables
40 | for key, value in self.original_env.items():
41 | if value is not None:
42 | os.environ[key] = value
43 | else:
44 | os.environ.pop(key, None)
45 |
46 | def test_blazegraph_connection_with_env_vars(self):
47 |
48 | connection = BlazegraphConnection()
49 | self.assertIsInstance(connection, BlazegraphConnection, 'Connection should be an instance of BlazegraphConnection')
50 | self.assertEqual(connection.cim_profile, 'cimhub_2023', 'CIM profile mismatch')
51 | self.assertEqual(connection.url, 'http://localhost:8889/bigdata/namespace/kb/sparql', 'URL mismatch')
52 | self.assertEqual(connection.namespace, 'http://iec.ch/TC57/CIM100#', 'Namespace mismatch')
53 | self.assertEqual(connection.iec61970_301, 8, 'IEC61970_301 mismatch')
54 |
55 | def test_get_object_sparql(self):
56 | """Test get_object_sparql to retrieve object from mrid"""
57 |
58 | query = sparql.get_object_sparql(mRID=self.feeder_mrid)
59 | expected_str = '\n PREFIX r: \n PREFIX cim: \n SELECT DISTINCT ?identifier ?obj_class\n WHERE {\n \n VALUES ?identifier {"49AD8E07-3BF9-A4E2-CB8F-C3722F837B62"}\n bind(iri(concat("urn:uuid:", ?identifier)) as ?eq)\n\n ?eq a ?classraw.\n bind(strafter(str(?classraw),"http://iec.ch/TC57/CIM100#") as ?obj_class)\n }\n ORDER by ?identifier\n '
60 | self.assertEqual(query, expected_str)
61 |
62 | def test_get_object(self):
63 | database = BlazegraphConnection()
64 | feeder = database.get_object(mRID=self.feeder_mrid)
65 | expected_str = '{"@id": "49ad8e07-3bf9-a4e2-cb8f-c3722f837b62", "@type": "Feeder"}'
66 | self.assertEqual(feeder.__str__(), expected_str)
67 |
68 | def test_get_nodes_sparql(self):
69 | feeder = cim.Feeder(mRID=self.feeder_mrid)
70 | query = sparql.get_all_nodes_from_container(feeder)
71 | expected_str = '\n PREFIX r: \n PREFIX cim: \n SELECT DISTINCT ?ConnectivityNode ?Terminal ?Equipment\n WHERE {\n \n VALUES ?identifier {"49AD8E07-3BF9-A4E2-CB8F-C3722F837B62"}\n bind(iri(concat("urn:uuid:", ?identifier)) as ?c).\n {\n\n ?node cim:ConnectivityNode.ConnectivityNodeContainer ?c.\n ?t cim:Terminal.ConnectivityNode ?node.\n ?t cim:Terminal.ConductingEquipment ?eq.\n ?eq a ?eq_cls.\n\n bind(strafter(str(?node),"urn:uuid:") as ?ConnectivityNode).\n bind(strafter(str(?t),"urn:uuid:") as ?Terminal).\n bind(strafter(str(?eq),"urn:uuid:") as ?eq_id).\n\n bind(concat("{\\"@id\\":\\"", str(?eq_id),"\\",\\"@type\\":\\"",strafter(str(?eq_cls),"http://iec.ch/TC57/CIM100#"), "\\"}") as ?Equipment)\n }\n \n UNION\n {\n {?eq cim:Equipment.EquipmentContainer ?c.}\n UNION\n {?eq cim:Equipment.AdditionalEquipmentContainer ?c.}\n OPTIONAL {\n ?t cim:Terminal.ConductingEquipment ?eq.\n ?t cim:Terminal.ConnectivityNode ?node.\n }\n ?eq a ?eq_cls.\n\n bind(strafter(str(?node),"urn:uuid:") as ?ConnectivityNode).\n bind(strafter(str(?t),"urn:uuid:") as ?Terminal).\n bind(strafter(str(?eq),"urn:uuid:") as ?eq_id).\n\n bind(concat("{\\"@id\\":\\"", str(?eq_id),"\\",\\"@type\\":\\"",strafter(str(?eq_cls),"http://iec.ch/TC57/CIM100#"), "\\"}") as ?Equipment)\n }\n }\n ORDER by ?ConnectivityNode\n '
72 | self.assertEqual(query, expected_str)
73 |
74 | def test_get_feeder_model(self):
75 | database = BlazegraphConnection()
76 | feeder = cim.Feeder(mRID=self.feeder_mrid)
77 | network = FeederModel(connection=database, container=feeder, distributed=False)
78 | initial_keys = len(network.graph.keys())
79 | self.assertEqual(initial_keys, 14)
80 |
81 | def test_get_all_line_data(self):
82 | database = BlazegraphConnection()
83 | feeder = cim.Feeder(mRID=self.feeder_mrid)
84 | network = FeederModel(connection=database, container=feeder, distributed=False)
85 | line = network.graph[cim.ACLineSegment][UUID('0bbd0ea3-f665-465b-86fd-fc8b8466ad53')]
86 | self.assertEqual(len(line.Terminals),2)
87 | utils.get_all_line_data(network)
88 | # check size of graph
89 | total_keys = len(network.graph.keys())
90 | self.assertEqual(total_keys, 21)
91 | # check value of line 645646
92 | self.assertEqual(line.name, '645646')
93 | self.assertEqual(line.length, 91.44)
94 |
95 | # check phase C
96 | for phase in line.ACLineSegmentPhases:
97 | if phase.phase.value == 'C':
98 | break
99 | self.assertEqual(phase.name, '645646_C')
100 | self.assertEqual(phase.ACLineSegment, line)
101 |
102 |
103 | if __name__ == '__main__':
104 | unittest.main()
105 |
--------------------------------------------------------------------------------
/tests/test_get_all_edges.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pytest
4 |
5 | from cimgraph.databases.gridappsd import GridappsdConnection
6 | from cimgraph.models import FeederModel, GraphModel
7 |
8 |
9 | @pytest.fixture
10 | def env_setup():
11 | original_env = {
12 | 'CIMG_CIM_PROFILE': os.getenv('CIMG_CIM_PROFILE'),
13 | 'CIMG_URL': os.getenv('CIMG_URL'),
14 | 'CIMG_DATABASE': os.getenv('CIMG_DATABASE'),
15 | 'CIMG_HOST': os.getenv('CIMG_HOST'),
16 | 'CIMG_PORT': os.getenv('CIMG_PORT'),
17 | 'CIMG_USERNAME': os.getenv('CIMG_USERNAME'),
18 | 'CIMG_PASSWORD': os.getenv('CIMG_PASSWORD'),
19 | 'CIMG_NAMESPACE': os.getenv('CIMG_NAMESPACE'),
20 | 'CIMG_IEC61970_301': os.getenv('CIMG_IEC61970_301'),
21 | 'CIMG_USE_UNITS': os.getenv('CIMG_USE_UNITS'),
22 | }
23 |
24 | # Set environment variables for testing
25 | os.environ['CIMG_CIM_PROFILE'] = 'cimhub_2023'
26 | os.environ['CIMG_URL'] = 'http://localhost:8889/bigdata/namespace/kb/sparql'
27 | os.environ['CIMG_DATABASE'] = 'powergridmodel'
28 | os.environ['CIMG_HOST'] = 'localhost'
29 | os.environ['CIMG_PORT'] = '61613'
30 | os.environ['CIMG_USERNAME'] = 'test_app_user'
31 | os.environ['CIMG_PASSWORD'] = '4Test'
32 | os.environ['CIMG_NAMESPACE'] = 'http://iec.ch/TC57/CIM100#'
33 | os.environ['CIMG_IEC61970_301'] = '8'
34 | os.environ['CIMG_USE_UNITS'] = 'False'
35 |
36 | yield
37 |
38 | for key, value in original_env.items():
39 | if value is not None:
40 | os.environ[key] = value
41 | else:
42 | os.environ.pop(key, None)
43 |
44 | @pytest.fixture(params=[{
45 | 'connection': GridappsdConnection,
46 | 'mrid': '49AD8E07-3BF9-A4E2-CB8F-C3722F837B62',
47 | 'distributed': False
48 | }])
49 | def network(env_setup, request) -> GraphModel:
50 |
51 | connection, mrid, distributed = request.param['connection'](), request.param['mrid'], request.param['distributed']
52 |
53 | feeder = connection.get_object(mrid=mrid)
54 | assert feeder, 'Feeder object not found.'
55 | yield FeederModel(connection=connection, container=feeder, distributed=distributed)
56 |
57 | connection.disconnect()
58 |
59 | @pytest.fixture
60 | def cim():
61 | cim_profile = 'cimhub_2023'
62 | # cim_profile = 'rc4_2021'
63 | # import cimgraph.data_profile.rc4_2021 as cim
64 | import cimgraph.data_profile.cimhub_2023 as cim
65 | return cim
66 |
67 |
68 |
69 | def test_get_all_edges(network, cim):
70 | network.get_all_edges(cim.ACLineSegment)
71 | network.get_all_edges(cim.ACLineSegmentPhase)
72 | network.get_all_edges(cim.PerLengthPhaseImpedance)
73 | network.get_all_edges(cim.PhaseImpedanceData)
74 | network.get_all_edges(cim.WireSpacingInfo)
75 | network.get_all_edges(cim.WirePosition)
76 | network.get_all_edges(cim.OverheadWireInfo)
77 | network.get_all_edges(cim.ConcentricNeutralCableInfo)
78 | network.get_all_edges(cim.TapeShieldCableInfo)
79 |
80 | network.get_all_edges(cim.PowerTransformer)
81 | network.get_all_edges(cim.TransformerTank)
82 | network.get_all_edges(cim.TransformerTankEnd)
83 | network.get_all_edges(cim.TransformerTankInfo)
84 | network.get_all_edges(cim.TransformerEndInfo)
85 | network.get_all_edges(cim.PowerTransformerEnd)
86 | network.get_all_edges(cim.PowerTransformerInfo)
87 | network.get_all_edges(cim.TransformerCoreAdmittance)
88 | network.get_all_edges(cim.TransformerMeshImpedance)
89 | network.get_all_edges(cim.TransformerStarImpedance)
90 | network.get_all_edges(cim.ShortCircuitTest)
91 | network.get_all_edges(cim.NoLoadTest)
92 | network.get_all_edges(cim.RatioTapChanger)
93 | network.get_all_edges(cim.TapChanger)
94 | network.get_all_edges(cim.TapChangerControl)
95 | network.get_all_edges(cim.TapChangerInfo)
96 |
97 | network.get_all_edges(cim.EnergyConsumer)
98 | network.get_all_edges(cim.EnergyConsumerPhase)
99 | network.get_all_edges(cim.EnergySource)
100 |
101 | network.get_all_edges(cim.ConnectivityNode)
102 | network.get_all_edges(cim.Terminal)
103 |
104 | network.get_all_edges(cim.OperationalLimitSet)
105 | network.get_all_edges(cim.OperationalLimitType)
106 | network.get_all_edges(cim.VoltageLimit)
107 | network.get_all_edges(cim.CurrentLimit)
108 | network.get_all_edges(cim.Feeder)
109 | network.get_all_edges(cim.BaseVoltage)
110 | network.get_all_edges(cim.CoordinateSystem)
111 | network.get_all_edges(cim.Location)
112 | network.get_all_edges(cim.PositionPoint)
113 |
--------------------------------------------------------------------------------
/tests/test_gridappsd_connection.py:
--------------------------------------------------------------------------------
1 | import os
2 | import unittest
3 | from uuid import UUID
4 |
5 | import cimgraph.data_profile.cimhub_2023 as cim
6 | from cimgraph import utils
7 | from cimgraph.databases import GridappsdConnection
8 | from cimgraph.models import FeederModel
9 | from cimgraph.queries import sparql
10 |
11 |
12 | class TestBlazegraphSETO(unittest.TestCase):
13 |
14 | def setUp(self):
15 | # Backup environment variables
16 | self.original_env = {
17 | 'CIMG_CIM_PROFILE': os.getenv('CIMG_CIM_PROFILE'),
18 | 'CIMG_URL': os.getenv('CIMG_URL'),
19 | 'CIMG_DATABASE': os.getenv('CIMG_DATABASE'),
20 | 'CIMG_HOST': os.getenv('CIMG_HOST'),
21 | 'CIMG_PORT': os.getenv('CIMG_PORT'),
22 | 'CIMG_USERNAME': os.getenv('CIMG_USERNAME'),
23 | 'CIMG_PASSWORD': os.getenv('CIMG_PASSWORD'),
24 | 'CIMG_NAMESPACE': os.getenv('CIMG_NAMESPACE'),
25 | 'CIMG_IEC61970_301': os.getenv('CIMG_IEC61970_301'),
26 | 'CIMG_USE_UNITS': os.getenv('CIMG_USE_UNITS'),
27 | }
28 |
29 | # Set environment variables for testing
30 | os.environ['CIMG_CIM_PROFILE'] = 'cimhub_2023'
31 | os.environ['CIMG_URL'] = 'http://localhost:8889/bigdata/namespace/kb/sparql'
32 | os.environ['CIMG_DATABASE'] = 'powergridmodel'
33 | os.environ['CIMG_HOST'] = 'localhost'
34 | os.environ['CIMG_PORT'] = '61613'
35 | os.environ['CIMG_USERNAME'] = 'test_app_user'
36 | os.environ['CIMG_PASSWORD'] = '4Test'
37 | os.environ['CIMG_NAMESPACE'] = 'http://iec.ch/TC57/CIM100#'
38 | os.environ['CIMG_IEC61970_301'] = '8'
39 | os.environ['CIMG_USE_UNITS'] = 'False'
40 | self.feeder_mrid = '49AD8E07-3BF9-A4E2-CB8F-C3722F837B62'
41 |
42 | def tearDown(self):
43 | # Restore environment variables
44 | for key, value in self.original_env.items():
45 | if value is not None:
46 | os.environ[key] = value
47 | else:
48 | os.environ.pop(key, None)
49 |
50 | def test_blazegraph_connection_with_env_vars(self):
51 |
52 | connection = GridappsdConnection()
53 | self.assertIsInstance(connection, GridappsdConnection, 'Connection should be an instance of GridappsdConnection')
54 | self.assertEqual(connection.cim_profile, 'cimhub_2023', 'CIM profile mismatch')
55 | self.assertEqual(connection.url, 'http://localhost:8889/bigdata/namespace/kb/sparql', 'URL mismatch')
56 | self.assertEqual(connection.namespace, 'http://iec.ch/TC57/CIM100#', 'Namespace mismatch')
57 | self.assertEqual(connection.iec61970_301, 8, 'IEC61970_301 mismatch')
58 |
59 | def test_get_object_sparql(self):
60 | """Test get_object_sparql to retrieve object from mrid"""
61 |
62 | query = sparql.get_object_sparql(mRID=self.feeder_mrid)
63 | expected_str = '\n PREFIX r: \n PREFIX cim: \n SELECT DISTINCT ?identifier ?obj_class\n WHERE {\n \n VALUES ?identifier {"49AD8E07-3BF9-A4E2-CB8F-C3722F837B62"}\n bind(iri(concat("urn:uuid:", ?identifier)) as ?eq)\n\n ?eq a ?classraw.\n bind(strafter(str(?classraw),"http://iec.ch/TC57/CIM100#") as ?obj_class)\n }\n ORDER by ?identifier\n '
64 | self.assertEqual(query, expected_str)
65 |
66 | def test_get_object(self):
67 | database = GridappsdConnection()
68 | feeder = database.get_object(mRID=self.feeder_mrid)
69 | expected_str = '{"@id": "49ad8e07-3bf9-a4e2-cb8f-c3722f837b62", "@type": "Feeder"}'
70 | self.assertEqual(feeder.__str__(), expected_str)
71 |
72 | def test_get_nodes_sparql(self):
73 | feeder = cim.Feeder(mRID=self.feeder_mrid)
74 | query = sparql.get_all_nodes_from_container(feeder)
75 | expected_str = '\n PREFIX r: \n PREFIX cim: \n SELECT DISTINCT ?ConnectivityNode ?Terminal ?Equipment\n WHERE {\n \n VALUES ?identifier {"49AD8E07-3BF9-A4E2-CB8F-C3722F837B62"}\n bind(iri(concat("urn:uuid:", ?identifier)) as ?c).\n {\n\n ?node cim:ConnectivityNode.ConnectivityNodeContainer ?c.\n ?t cim:Terminal.ConnectivityNode ?node.\n ?t cim:Terminal.ConductingEquipment ?eq.\n ?eq a ?eq_cls.\n\n bind(strafter(str(?node),"urn:uuid:") as ?ConnectivityNode).\n bind(strafter(str(?t),"urn:uuid:") as ?Terminal).\n bind(strafter(str(?eq),"urn:uuid:") as ?eq_id).\n\n bind(concat("{\\"@id\\":\\"", str(?eq_id),"\\",\\"@type\\":\\"",strafter(str(?eq_cls),"http://iec.ch/TC57/CIM100#"), "\\"}") as ?Equipment)\n }\n \n UNION\n {\n {?eq cim:Equipment.EquipmentContainer ?c.}\n UNION\n {?eq cim:Equipment.AdditionalEquipmentContainer ?c.}\n OPTIONAL {\n ?t cim:Terminal.ConductingEquipment ?eq.\n ?t cim:Terminal.ConnectivityNode ?node.\n }\n ?eq a ?eq_cls.\n\n bind(strafter(str(?node),"urn:uuid:") as ?ConnectivityNode).\n bind(strafter(str(?t),"urn:uuid:") as ?Terminal).\n bind(strafter(str(?eq),"urn:uuid:") as ?eq_id).\n\n bind(concat("{\\"@id\\":\\"", str(?eq_id),"\\",\\"@type\\":\\"",strafter(str(?eq_cls),"http://iec.ch/TC57/CIM100#"), "\\"}") as ?Equipment)\n }\n }\n ORDER by ?ConnectivityNode\n '
76 | self.assertEqual(query, expected_str)
77 |
78 | def test_get_feeder_model(self):
79 | database = GridappsdConnection()
80 | feeder = cim.Feeder(mRID=self.feeder_mrid)
81 | network = FeederModel(connection=database, container=feeder, distributed=False)
82 | initial_keys = len(network.graph.keys())
83 | self.assertEqual(initial_keys, 14)
84 |
85 | def test_get_all_line_data(self):
86 | database = GridappsdConnection()
87 | feeder = cim.Feeder(mRID=self.feeder_mrid)
88 | network = FeederModel(connection=database, container=feeder, distributed=False)
89 | line = network.graph[cim.ACLineSegment][UUID('0bbd0ea3-f665-465b-86fd-fc8b8466ad53')]
90 | self.assertEqual(len(line.Terminals),2)
91 | utils.get_all_line_data(network)
92 | # check size of graph
93 | total_keys = len(network.graph.keys())
94 | self.assertEqual(total_keys, 21)
95 | # check value of line 645646
96 | self.assertEqual(line.name, '645646')
97 | self.assertEqual(line.length, 91.44)
98 |
99 | # check phase C
100 | for phase in line.ACLineSegmentPhases:
101 | if phase.phase.value == 'C':
102 | break
103 | self.assertEqual(phase.name, '645646_C')
104 | self.assertEqual(phase.ACLineSegment, line)
105 |
106 |
107 | if __name__ == '__main__':
108 | unittest.main()
109 |
--------------------------------------------------------------------------------
/tests/test_gridappsd_datasource.py:
--------------------------------------------------------------------------------
1 | import os
2 | import unittest
3 |
4 | from cimgraph.databases.gridappsd import GridappsdConnection
5 |
6 |
7 | class TestGridappsdDatasource(unittest.TestCase):
8 |
9 | def setUp(self):
10 | # Backup environment variables
11 | self.original_env = {
12 | 'CIMG_CIM_PROFILE': os.getenv('CIMG_CIM_PROFILE'),
13 | 'CIMG_URL': os.getenv('CIMG_URL'),
14 | 'CIMG_DATABASE': os.getenv('CIMG_DATABASE'),
15 | 'CIMG_HOST': os.getenv('CIMG_HOST'),
16 | 'CIMG_PORT': os.getenv('CIMG_PORT'),
17 | 'CIMG_USERNAME': os.getenv('CIMG_USERNAME'),
18 | 'CIMG_PASSWORD': os.getenv('CIMG_PASSWORD'),
19 | 'CIMG_NAMESPACE': os.getenv('CIMG_NAMESPACE'),
20 | 'CIMG_IEC61970_301': os.getenv('CIMG_IEC61970_301'),
21 | 'CIMG_USE_UNITS': os.getenv('CIMG_USE_UNITS'),
22 | }
23 |
24 | # Set environment variables for testing
25 | os.environ['CIMG_CIM_PROFILE'] = 'cimhub_2023'
26 | os.environ['CIMG_URL'] = 'http://localhost:8889/bigdata/namespace/kb/sparql'
27 | os.environ['CIMG_DATABASE'] = 'powergridmodel'
28 | os.environ['CIMG_HOST'] = 'localhost'
29 | os.environ['CIMG_PORT'] = '61613'
30 | os.environ['CIMG_USERNAME'] = 'test_app_user'
31 | os.environ['CIMG_PASSWORD'] = '4Test'
32 | os.environ['CIMG_NAMESPACE'] = 'http://iec.ch/TC57/CIM100#'
33 | os.environ['CIMG_IEC61970_301'] = '8'
34 | os.environ['CIMG_USE_UNITS'] = 'False'
35 |
36 | def tearDown(self):
37 | # Restore environment variables
38 | for key, value in self.original_env.items():
39 | if value is not None:
40 | os.environ[key] = value
41 | else:
42 | os.environ.pop(key, None)
43 |
44 | def test_get_gridappsds_connection_with_env_vars(self):
45 |
46 | connection = GridappsdConnection()
47 | self.assertIsInstance(connection, GridappsdConnection, 'Connection should be an instance of GridappsdConnection')
48 | self.assertEqual(connection.cim_profile, 'cimhub_2023', 'CIM profile mismatch')
49 | self.assertEqual(connection.url, 'http://localhost:8889/bigdata/namespace/kb/sparql', 'URL mismatch')
50 | self.assertEqual(connection.database, 'powergridmodel', 'Database mismatch')
51 | self.assertEqual(connection.host, 'localhost', 'Host mismatch')
52 | self.assertEqual(connection.port, 61613, 'Port mismatch')
53 | self.assertEqual(connection.username, 'test_app_user', 'Username mismatch')
54 | self.assertEqual(connection.password, '4Test', 'Password mismatch')
55 | self.assertEqual(connection.namespace, 'http://iec.ch/TC57/CIM100#', 'Namespace mismatch')
56 | self.assertEqual(connection.iec61970_301, 8, 'IEC61970_301 mismatch')
57 | self.assertFalse(connection.use_units, 'USE_UNITS mismatch')
58 |
59 | def test_connection_established(self):
60 | # Overwrite for this test the profile.
61 | import cimgraph.data_profile.cim17v40 as cim
62 | from cimgraph.models import FeederModel
63 | os.environ['CIMG_CIM_PROFILE'] = 'cim17v40'
64 |
65 | # Hard coded feader id
66 | feeder_mrid = '49AD8E07-3BF9-A4E2-CB8F-C3722F837B62'
67 | #eeder = cim.Feeder(mRID = feeder_mrid)
68 | connection = GridappsdConnection()
69 | feeder = connection.get_object(mRID=feeder_mrid)
70 | assert feeder, 'Feeder object.'
71 | assert connection.gapps.connected, "Couldn't connect"
72 | network = FeederModel(connection=connection, container=feeder, distributed=False)
73 | #network = FeederModel(connection=connection, container=feeder, distributed=False)
74 |
75 | connection.disconnect()
76 |
77 |
78 | if __name__ == '__main__':
79 | unittest.main()
80 |
--------------------------------------------------------------------------------
/tests/test_mermaid.py:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | breaker = cim.Breaker(name = 'breaker12', open=False, inService=True)
5 | substation = cim.Substation(name = 'sub1234')
6 | t1 = cim.Terminal(name = 'brk12_t1')
7 | t2 = cim.Terminal(name = 'brk12_t2')
8 | basev = cim.BaseVoltage(name = 'base12kv', nominalVoltage=12470)
9 | breaker.EquipmentContainer = substation
10 | breaker.Terminals = [t1, t2]
11 | breaker.BaseVoltage = basev
12 |
--------------------------------------------------------------------------------
/tests/test_neo4j.py:
--------------------------------------------------------------------------------
1 | import os
2 | import unittest
3 | from uuid import UUID
4 |
5 | import cimgraph.data_profile.cimhub_2023 as cim
6 | from cimgraph import utils
7 | from cimgraph.databases import Neo4jConnection
8 | from cimgraph.models import FeederModel
9 | from cimgraph.queries import cypher
10 |
11 |
12 | class TestBlazegraphSETO(unittest.TestCase):
13 |
14 | def setUp(self):
15 | # Backup environment variables
16 | self.original_env = {
17 | 'CIMG_CIM_PROFILE': os.getenv('CIMG_CIM_PROFILE'),
18 | 'CIMG_URL': os.getenv('CIMG_URL'),
19 | 'CIMG_DATABASE': os.getenv('CIMG_DATABASE'),
20 | 'CIMG_HOST': os.getenv('CIMG_HOST'),
21 | 'CIMG_PORT': os.getenv('CIMG_PORT'),
22 | 'CIMG_USERNAME': os.getenv('CIMG_USERNAME'),
23 | 'CIMG_PASSWORD': os.getenv('CIMG_PASSWORD'),
24 | 'CIMG_NAMESPACE': os.getenv('CIMG_NAMESPACE'),
25 | 'CIMG_IEC61970_301': os.getenv('CIMG_IEC61970_301'),
26 | 'CIMG_USE_UNITS': os.getenv('CIMG_USE_UNITS'),
27 | }
28 |
29 | # Set environment variables for testing
30 | os.environ['CIMG_CIM_PROFILE'] = 'cimhub_2023'
31 | os.environ['CIMG_URL'] = 'neo4j://localhost:7687'
32 | os.environ['CIMG_DATABASE'] = 'neo4j'
33 | os.environ['CIMG_HOST'] = 'localhost'
34 | os.environ['CIMG_PORT'] = '7687'
35 | os.environ['CIMG_USERNAME'] = 'neo4j'
36 | os.environ['CIMG_PASSWORD'] = 'test1234'
37 | os.environ['CIMG_NAMESPACE'] = 'http://iec.ch/TC57/CIM100#'
38 | os.environ['CIMG_IEC61970_301'] = '8'
39 | os.environ['CIMG_USE_UNITS'] = 'False'
40 | self.feeder_mrid = '49AD8E07-3BF9-A4E2-CB8F-C3722F837B62'
41 |
42 | def tearDown(self):
43 | # Restore environment variables
44 | for key, value in self.original_env.items():
45 | if value is not None:
46 | os.environ[key] = value
47 | else:
48 | os.environ.pop(key, None)
49 |
50 | def test_blazegraph_connection_with_env_vars(self):
51 |
52 | connection = Neo4jConnection()
53 | self.assertIsInstance(connection, Neo4jConnection, 'Connection should be an instance of Neo4jConnection')
54 | self.assertEqual(connection.cim_profile, 'cimhub_2023', 'CIM profile mismatch')
55 | self.assertEqual(connection.url, 'neo4j://localhost:7687', 'URL mismatch')
56 | self.assertEqual(connection.namespace, 'http://iec.ch/TC57/CIM100#', 'Namespace mismatch')
57 | self.assertEqual(connection.iec61970_301, 8, 'IEC61970_301 mismatch')
58 |
59 | def test_get_object_cypher(self):
60 | """Test get_object_sparql to retrieve object from mrid"""
61 |
62 | query = cypher.get_object_cypher(mRID=self.feeder_mrid)
63 | expected_str = '\nMATCH(n)\nWHERE n.uri = "urn:uuid:49AD8E07-3BF9-A4E2-CB8F-C3722F837B62"\nRETURN DISTINCT n.uri as identifier,\nlabels(n)[1] as class'
64 | self.assertEqual(query, expected_str)
65 |
66 | def test_get_object(self):
67 | database = Neo4jConnection()
68 | feeder = database.get_object(mRID=self.feeder_mrid)
69 | expected_str = '{"@id": "49ad8e07-3bf9-a4e2-cb8f-c3722f837b62", "@type": "Feeder"}'
70 | self.assertEqual(feeder.__str__(), expected_str)
71 | database.disconnect()
72 |
73 | def test_get_nodes_sparql(self):
74 | feeder = cim.Feeder(mRID=self.feeder_mrid)
75 | query = cypher.get_all_nodes_from_container(feeder)
76 | expected_str = 'MATCH (container:Feeder)\nWHERE container.uri = "urn:uuid:49AD8E07-3BF9-A4E2-CB8F-C3722F837B62"\nMATCH (eq) - [:`Equipment.EquipmentContainer`] - (container)\nOPTIONAL MATCH (cnode) - [:`Terminal.ConnectivityNode`] - (term:Terminal) - [:`Terminal.ConductingEquipment`] -> (eq)\nRETURN DISTINCT\nREPLACE(cnode.uri, "urn:uuid:", "") as ConnectivityNode,\nREPLACE(term.uri, "urn:uuid:", "") as Terminal,\nREPLACE(eq.uri, "urn:uuid:", "") as eq_id,\nLABELS(eq)[1] as eq_class'
77 | self.assertEqual(query, expected_str)
78 |
79 | def test_get_feeder_model(self):
80 | database = Neo4jConnection()
81 | feeder = cim.Feeder(mRID=self.feeder_mrid)
82 | network = FeederModel(connection=database, container=feeder, distributed=False)
83 | initial_keys = len(network.graph.keys())
84 | self.assertEqual(initial_keys, 13)
85 | database.disconnect()
86 |
87 |
88 | def test_get_all_line_data(self):
89 | database = Neo4jConnection()
90 | feeder = cim.Feeder(mRID=self.feeder_mrid)
91 | network = FeederModel(connection=database, container=feeder, distributed=False)
92 | line = network.graph[cim.ACLineSegment][UUID('0bbd0ea3-f665-465b-86fd-fc8b8466ad53')]
93 | self.assertEqual(len(line.Terminals),2)
94 | utils.get_all_line_data(network)
95 | # check size of graph
96 | total_keys = len(network.graph.keys())
97 | self.assertEqual(total_keys, 21)
98 | # check value of line 645646
99 | self.assertEqual(line.name, '645646')
100 | self.assertEqual(line.length, 91.44)
101 |
102 | # check phase C
103 | for phase in line.ACLineSegmentPhases:
104 | if phase.phase.value == 'C':
105 | break
106 | self.assertEqual(phase.name, '645646_C')
107 | self.assertEqual(phase.ACLineSegment, line)
108 | database.disconnect()
109 |
110 |
111 |
112 | if __name__ == '__main__':
113 | unittest.main()
114 |
--------------------------------------------------------------------------------
/tests/test_rdflib.py:
--------------------------------------------------------------------------------
1 | import os
2 | import unittest
3 | from uuid import UUID
4 |
5 | import cimgraph.data_profile.cimhub_2023 as cim
6 | from cimgraph import utils
7 | from cimgraph.databases import RDFlibConnection
8 | from cimgraph.models import FeederModel
9 | from cimgraph.queries import sparql
10 |
11 |
12 | class TestRDFlibConnection(unittest.TestCase):
13 |
14 | def setUp(self):
15 | # Backup environment variables
16 | self.original_env = {
17 | 'CIMG_CIM_PROFILE': os.getenv('CIMG_CIM_PROFILE'),
18 | 'CIMG_URL': os.getenv('CIMG_URL'),
19 | 'CIMG_DATABASE': os.getenv('CIMG_DATABASE'),
20 | 'CIMG_HOST': os.getenv('CIMG_HOST'),
21 | 'CIMG_PORT': os.getenv('CIMG_PORT'),
22 | 'CIMG_USERNAME': os.getenv('CIMG_USERNAME'),
23 | 'CIMG_PASSWORD': os.getenv('CIMG_PASSWORD'),
24 | 'CIMG_NAMESPACE': os.getenv('CIMG_NAMESPACE'),
25 | 'CIMG_IEC61970_301': os.getenv('CIMG_IEC61970_301'),
26 | 'CIMG_USE_UNITS': os.getenv('CIMG_USE_UNITS'),
27 | }
28 |
29 | # Set environment variables for testing
30 | os.environ['CIMG_CIM_PROFILE'] = 'cimhub_2023'
31 | os.environ['CIMG_URL'] = 'http://localhost:8889/bigdata/namespace/kb/sparql'
32 | os.environ['CIMG_NAMESPACE'] = 'http://iec.ch/TC57/CIM100#'
33 | os.environ['CIMG_IEC61970_301'] = '8'
34 | os.environ['CIMG_USE_UNITS'] = 'false'
35 |
36 | self.feeder_mrid = '49AD8E07-3BF9-A4E2-CB8F-C3722F837B62'
37 |
38 | def tearDown(self):
39 | # Restore environment variables
40 | for key, value in self.original_env.items():
41 | if value is not None:
42 | os.environ[key] = value
43 | else:
44 | os.environ.pop(key, None)
45 |
46 | def test_xml_connection_with_env_vars(self):
47 |
48 | connection = RDFlibConnection(filename='tests/test_models/ieee13.xml')
49 | self.assertIsInstance(connection, RDFlibConnection, 'Connection should be an instance of RDFlibConnection')
50 | self.assertEqual(connection.cim_profile, 'cimhub_2023', 'CIM profile mismatch')
51 | self.assertEqual(connection.namespace, 'http://iec.ch/TC57/CIM100#', 'Namespace mismatch')
52 | self.assertEqual(connection.iec61970_301, 8, 'IEC61970_301 mismatch')
53 |
54 |
55 | def test_get_feeder_model(self):
56 | database = RDFlibConnection(filename='tests/test_models/ieee13.xml')
57 | feeder = cim.Feeder(mRID=self.feeder_mrid)
58 | network = FeederModel(connection=database, container=feeder, distributed=False)
59 | initial_keys = len(network.graph.keys())
60 | self.assertEqual(initial_keys, 14)
61 |
62 | def test_get_all_line_data(self):
63 | database = RDFlibConnection(filename='tests/test_models/ieee13.xml')
64 | feeder = cim.Feeder(mRID=self.feeder_mrid)
65 | network = FeederModel(connection=database, container=feeder, distributed=False)
66 | line = network.graph[cim.ACLineSegment][UUID('0bbd0ea3-f665-465b-86fd-fc8b8466ad53')]
67 | self.assertEqual(len(line.Terminals),2)
68 | utils.get_all_line_data(network)
69 | # check size of graph
70 | total_keys = len(network.graph.keys())
71 | self.assertEqual(total_keys, 21)
72 | # check value of line 645646
73 | self.assertEqual(line.name, '645646')
74 | self.assertEqual(line.length, 91.44)
75 |
76 | # check phase C
77 | for phase in line.ACLineSegmentPhases:
78 | if phase.phase.value == 'C':
79 | break
80 | self.assertEqual(phase.name, '645646_C')
81 | self.assertEqual(phase.ACLineSegment, line)
82 |
83 |
84 | if __name__ == '__main__':
85 | unittest.main()
86 |
--------------------------------------------------------------------------------
/tests/test_uuid.py:
--------------------------------------------------------------------------------
1 | import json
2 | import re
3 | import unittest
4 | from uuid import UUID
5 |
6 | import cimgraph.data_profile.cimhub_2023 as cim
7 |
8 |
9 | class TestIdentityUUID(unittest.TestCase):
10 |
11 |
12 | def test_initialization_with_mRID(self):
13 | """Test initializing a Feeder with mRID parameter."""
14 |
15 | feeder = cim.Feeder(mRID='49ad8e07-3bf9-a4e2-cb8f-c3722f837b62')
16 |
17 | # Test string representation
18 | expected_str = '{"@id": "49ad8e07-3bf9-a4e2-cb8f-c3722f837b62", "@type": "Feeder"}'
19 | self.assertEqual(feeder.__str__(), expected_str)
20 |
21 | # Test URI method
22 | self.assertEqual(feeder.uri(), '49ad8e07-3bf9-a4e2-cb8f-c3722f837b62')
23 |
24 | def test_initialization_with_mRID_caps_underscore(self):
25 | """Test initializing a Feeder with mRID parameter."""
26 |
27 | feeder = cim.Feeder(mRID='_49AD8E07-3BF9-A4E2-CB8F-C3722F837B62')
28 |
29 | # Test string representation
30 | expected_str = '{"@id": "49ad8e07-3bf9-a4e2-cb8f-c3722f837b62", "@type": "Feeder"}'
31 | self.assertEqual(feeder.__str__(), expected_str)
32 |
33 | # Test URI method
34 | self.assertEqual(feeder.uri(), '_49AD8E07-3BF9-A4E2-CB8F-C3722F837B62')
35 |
36 | def test_set_uuid_after_initialization(self):
37 | """Test setting UUID after initialization using uuid() method."""
38 | # Initialize empty Feeder, then set UUID
39 | feeder = cim.Feeder()
40 | feeder.uuid(mRID='49ad8e07-3bf9-a4e2-cb8f-c3722f837b62')
41 |
42 | # Verify using string representation
43 | # Note: Using a helper method to compare JSON contents
44 | self.verify_json_content(
45 | feeder.__str__(),
46 | {'@id': '49ad8e07-3bf9-a4e2-cb8f-c3722f837b62', '@type': 'Feeder'}
47 | )
48 |
49 | # Test URI method
50 | self.assertEqual(feeder.uri(), '49ad8e07-3bf9-a4e2-cb8f-c3722f837b62')
51 |
52 | def test_initialization_with_UUID_object(self):
53 | """Test initializing a Feeder with UUID object."""
54 | uuid_obj = UUID('49ad8e07-3bf9-a4e2-cb8f-c3722f837b62')
55 | feeder = cim.Feeder(identifier=uuid_obj)
56 |
57 | # Verify using string representation
58 | self.verify_json_content(
59 | feeder.__str__(),
60 | {'@id': '49ad8e07-3bf9-a4e2-cb8f-c3722f837b62', '@type': 'Feeder'}
61 | )
62 |
63 | # Test URI method
64 | self.assertEqual(feeder.uri(), '49ad8e07-3bf9-a4e2-cb8f-c3722f837b62')
65 |
66 | def test_invalid_identifier_handling(self):
67 | """Test that invalid identifiers produce expected behavior and a valid UUID is created."""
68 | # Create a new test case for an invalid identifier
69 | with self.assertLogs(level='WARNING') as log_context:
70 | # This will raise an AssertionError if no logs of the specified level are issued
71 | feeder = cim.Feeder(identifier='qwertyuiop')
72 |
73 | # Check that we received some warning logs
74 | self.assertTrue(len(log_context.records) > 0)
75 |
76 | # Check for expected content in warning messages
77 | warning_text = '\n'.join(record.getMessage() for record in log_context.records)
78 | self.assertIn('qwertyuiop', warning_text)
79 |
80 | # Two approaches to verify warnings:
81 | # 1. Less strict: Just check that warnings were generated with the invalid ID
82 | self.assertTrue(any('qwertyuiop' in record.getMessage() for record in log_context.records))
83 |
84 | # 2. More specific: Check for specific warning messages if needed
85 | expected_messages = [
86 | 'Identifier qwertyuiop must be a UUID object',
87 | 'URI qwertyuiop not a valid UUID',
88 | 'qwertyuiop not a valid UUID'
89 | ]
90 |
91 | # Check for each expected message (allowing for partial matches)
92 | for expected in expected_messages:
93 | found = False
94 | for record in log_context.records:
95 | if expected in record.getMessage():
96 | found = True
97 | break
98 | # Uncomment this if you want strict validation of each message
99 | # self.assertTrue(found, f"Expected warning containing '{expected}' not found")
100 | self.validate_uuid_generation(feeder)
101 |
102 | def validate_uuid_generation(self, feeder):
103 | # Check that a valid UUID was generated - this part is the same
104 | feeder_json = feeder.__str__()
105 | if isinstance(feeder_json, dict):
106 | uuid_str = feeder_json['@id']
107 | else:
108 | # Extract UUID from JSON string if __str__ returns a string
109 | match = re.search(r'"@id":\s*"([^"]+)"', feeder_json)
110 | uuid_str = match.group(1) if match else None
111 |
112 | # Verify the UUID follows the proper format
113 | uuid_pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
114 | self.assertIsNotNone(re.match(uuid_pattern, uuid_str))
115 |
116 | # Check that the URI method returns the same UUID
117 | self.assertEqual(feeder.uri(), uuid_str)
118 |
119 | # Check type is correctly set
120 | if isinstance(feeder_json, dict):
121 | self.assertEqual(feeder_json['@type'], 'Feeder')
122 | else:
123 | self.assertIn('"@type": "Feeder"', feeder_json)
124 |
125 | # Helper method to compare JSON content regardless of formatting
126 | def verify_json_content(self, json_str, expected_dict):
127 | """Helper method to compare JSON content regardless of formatting."""
128 |
129 | # If json_str is already a dictionary (like from __str__()), use it directly
130 | if isinstance(json_str, dict):
131 | actual_dict = json_str
132 | else:
133 | # Otherwise, parse the JSON string into a dictionary
134 | actual_dict = json.loads(json_str)
135 |
136 | # Compare dictionaries
137 | self.assertEqual(actual_dict, expected_dict)
138 |
139 |
140 | if __name__ == '__main__':
141 | unittest.main()
142 |
--------------------------------------------------------------------------------
/tests/test_xmlfile.py:
--------------------------------------------------------------------------------
1 | import os
2 | import unittest
3 | from uuid import UUID
4 |
5 | import cimgraph.data_profile.cimhub_2023 as cim
6 | from cimgraph import utils
7 | from cimgraph.databases import XMLFile
8 | from cimgraph.models import FeederModel
9 | from cimgraph.queries import sparql
10 |
11 |
12 | class TestXMLFile(unittest.TestCase):
13 |
14 | def setUp(self):
15 | # Backup environment variables
16 | self.original_env = {
17 | 'CIMG_CIM_PROFILE': os.getenv('CIMG_CIM_PROFILE'),
18 | 'CIMG_URL': os.getenv('CIMG_URL'),
19 | 'CIMG_DATABASE': os.getenv('CIMG_DATABASE'),
20 | 'CIMG_HOST': os.getenv('CIMG_HOST'),
21 | 'CIMG_PORT': os.getenv('CIMG_PORT'),
22 | 'CIMG_USERNAME': os.getenv('CIMG_USERNAME'),
23 | 'CIMG_PASSWORD': os.getenv('CIMG_PASSWORD'),
24 | 'CIMG_NAMESPACE': os.getenv('CIMG_NAMESPACE'),
25 | 'CIMG_IEC61970_301': os.getenv('CIMG_IEC61970_301'),
26 | 'CIMG_USE_UNITS': os.getenv('CIMG_USE_UNITS'),
27 | }
28 |
29 | # Set environment variables for testing
30 | os.environ['CIMG_CIM_PROFILE'] = 'cimhub_2023'
31 | os.environ['CIMG_URL'] = 'http://localhost:8889/bigdata/namespace/kb/sparql'
32 | os.environ['CIMG_NAMESPACE'] = 'http://iec.ch/TC57/CIM100#'
33 | os.environ['CIMG_IEC61970_301'] = '8'
34 | os.environ['CIMG_USE_UNITS'] = 'false'
35 |
36 | self.feeder_mrid = '49AD8E07-3BF9-A4E2-CB8F-C3722F837B62'
37 |
38 | def tearDown(self):
39 | # Restore environment variables
40 | for key, value in self.original_env.items():
41 | if value is not None:
42 | os.environ[key] = value
43 | else:
44 | os.environ.pop(key, None)
45 |
46 | def test_xml_connection_with_env_vars(self):
47 |
48 | connection = XMLFile(filename='tests/test_models/ieee13.xml')
49 | self.assertIsInstance(connection, XMLFile, 'Connection should be an instance of XMLFile')
50 | self.assertEqual(connection.cim_profile, 'cimhub_2023', 'CIM profile mismatch')
51 | self.assertEqual(connection.namespace, 'http://iec.ch/TC57/CIM100#', 'Namespace mismatch')
52 | self.assertEqual(connection.iec61970_301, 8, 'IEC61970_301 mismatch')
53 |
54 |
55 | def test_get_feeder_model(self):
56 | database = XMLFile(filename='tests/test_models/ieee13.xml')
57 | feeder = cim.Feeder(mRID=self.feeder_mrid)
58 | network = FeederModel(connection=database, container=feeder, distributed=False)
59 | initial_keys = len(network.graph.keys())
60 | self.assertEqual(initial_keys, 48)
61 |
62 | def test_get_all_line_data(self):
63 | database = XMLFile(filename='tests/test_models/ieee13.xml')
64 | feeder = cim.Feeder(mRID=self.feeder_mrid)
65 | network = FeederModel(connection=database, container=feeder, distributed=False)
66 | line = network.graph[cim.ACLineSegment][UUID('0bbd0ea3-f665-465b-86fd-fc8b8466ad53')]
67 | self.assertEqual(len(line.Terminals),2)
68 | # check value of line 645646
69 | self.assertEqual(line.name, '645646')
70 | self.assertEqual(line.length, 91.44)
71 |
72 | # check phase C
73 | for phase in line.ACLineSegmentPhases:
74 | if phase.phase.value == 'C':
75 | break
76 | self.assertEqual(phase.name, '645646_C')
77 | self.assertEqual(phase.ACLineSegment, line)
78 |
79 |
80 | if __name__ == '__main__':
81 | unittest.main()
82 |
--------------------------------------------------------------------------------
/tests/timing.py:
--------------------------------------------------------------------------------
1 | import os
2 | from dataclasses import dataclass
3 | from functools import wraps
4 | from time import time
5 |
6 | OUTPUT_TIME = os.environ.get('CIMGRAPH_TIME', False)
7 |
8 | @dataclass
9 | class Counts:
10 | hits: int = 0
11 | total: float = 0.0
12 |
13 | timing_keys: dict[str, Counts] = {}
14 |
15 | def print_timing():
16 | for k, v in timing_keys.items():
17 | print(k, v)
18 |
19 | def clear_timing():
20 | timing_keys.clear()
21 |
22 |
23 | def timing(f):
24 | @wraps(f)
25 | def wrap(*args, **kw):
26 | if OUTPUT_TIME:
27 | if f.__name__ not in timing_keys:
28 | timing_keys[f.__name__] = Counts()
29 | ts = time()
30 | result = f(*args, **kw)
31 | te = time()
32 | timing_keys[f.__name__].hits +=1
33 | timing_keys[f.__name__].total += te-ts
34 | return result
35 | else:
36 | return f(*args, **kw)
37 | return wrap
38 |
--------------------------------------------------------------------------------