├── .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 | ![cimantic-graphs](cimantic_graphs.png) 127 | 128 | ![cimantic-graphs-init](cimantic_graphs_init.png) 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 | ![profile](profile.png) 133 | 134 | ![init](init.png) 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 | 4 | cim.data_profile 5 | dataclasses 6 | filenames 7 | reStructuredText 8 | allGlobals 9 | false 10 | false 11 | true 12 | false 13 | false 14 | 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' 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)}\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)}\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)}\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 | --------------------------------------------------------------------------------