grid-data-models 2.3.6__tar.gz → 2.3.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/PKG-INFO +6 -6
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/pyproject.toml +4 -4
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/__init__.py +1 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/distribution_system.py +35 -23
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/model_reduction/__init__.py +1 -0
- grid_data_models-2.3.8/src/gdm/distribution/model_reduction/reducer.py +408 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/sys_functools.py +145 -72
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_3_2__to__2_3_3.py +6 -2
- grid_data_models-2.3.8/src/gdm/distribution/upgrade_handler/from__2_3_6__to__2_3_7.py +15 -0
- grid_data_models-2.3.8/src/gdm/distribution/upgrade_handler/from__2_3_7__to__2_3_8.py +15 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/upgrade_handler.py +12 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/utils/aggregate_components.py +5 -4
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/mcp/server.py +47 -31
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/tracked_changes.py +37 -1
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/version.py +1 -1
- grid_data_models-2.3.6/src/gdm/distribution/model_reduction/reducer.py +0 -176
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/.gitignore +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/LICENSE.txt +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/README.md +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/__init__.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/cli/cli.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/cli/reducer.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/constants.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/dataset/__init__.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/dataset/cost_model.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/dataset/dataset_system.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/catalog_system.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/common/__init__.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/common/curve.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/common/limitset.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/common/sequence_pair.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/components/__init__.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/components/base/__init__.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/components/base/distribution_branch_base.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/components/base/distribution_component_base.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/components/base/distribution_switch_base.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/components/base/distribution_transformer_base.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/components/distribution_battery.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/components/distribution_bus.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/components/distribution_capacitor.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/components/distribution_feeder.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/components/distribution_load.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/components/distribution_regulator.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/components/distribution_solar.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/components/distribution_substation.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/components/distribution_transformer.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/components/distribution_vsource.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/components/geometry_branch.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/components/geometry_switch.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/components/matrix_impedance_branch.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/components/matrix_impedance_fuse.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/components/matrix_impedance_recloser.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/components/matrix_impedance_switch.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/components/sequence_impedance_branch.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/controllers/__init__.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/controllers/base/__init__.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/controllers/base/capacitor_controller_base.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/controllers/base/inverter_controller_base.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/controllers/distribution_capacitor_controller.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/controllers/distribution_inverter_controller.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/controllers/distribution_recloser_controller.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/controllers/distribution_regulator_controller.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/controllers/distribution_switch_controller.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/distribution_graph.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/enums.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/__init__.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/bare_conductor_equipment.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/base/__init__.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/base/matrix_impedance_branch_equipment_base.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/battery_equipment.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/capacitor_equipment.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/concentric_cable_equipment.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/distribution_transformer_equipment.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/geometry_branch_equipment.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/inverter_equipment.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/load_equipment.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/matrix_impedance_branch_equipment.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/matrix_impedance_fuse_equipment.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/matrix_impedance_recloser_equipment.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/matrix_impedance_switch_equipment.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/phase_capacitor_equipment.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/phase_load_equipment.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/phase_voltagesource_equipment.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/recloser_controller_equipment.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/sequence_impedance_branch_equipment.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/solar_equipment.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/voltagesource_equipment.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/market/__init__.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/market/aggregator.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/market/tariff.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/__init__.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_0_1__to__2_1_2.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_1_2__to__2_1_3.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_1_3__to__2_1_4.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_1_4__to__2_1_5.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_1_5__to__2_2_0.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_2_0__to__2_2_1.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_2_1__to__2_3_0.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_3_0__to__2_3_1.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_3_1__to__2_3_2.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_3_3__to__2_3_4.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_3_4__to__2_3_5.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_3_5__to__2_3_6.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/utils/__init__.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/exceptions.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/hashing_utils.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/mcp/__init__.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/mcp/exceptions.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/mcp/inspection/__init__.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/mcp/inspection/inspector.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/mcp/inspection/relationships.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/mcp/inspection/topology.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/mcp/knowledge/__init__.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/mcp/knowledge/documentation.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/mcp/operations/__init__.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/mcp/operations/merger.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/mcp/operations/splitter.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/mcp/schemas.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/mcp/utilities/__init__.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/mcp/utilities/subsystem.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/mcp/utilities/timeseries.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/mcp/validation/__init__.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/mcp/validation/auto_fixer.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/mcp/validation/diagnostics.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/mcp/validation/suggestions.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/mcp/version.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/quantities.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/structural/__init__.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/structural/components/__init__.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/structural/components/base.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/structural/components/building.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/structural/components/overhead_line_segment.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/structural/components/pole.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/structural/components/pvsystem.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/structural/components/transformer.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/structural/components/underground_cable.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/structural/components/underground_junction.py +0 -0
- {grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/structural/structural_system.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: grid-data-models
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.8
|
|
4
4
|
Project-URL: Documentation, https://github.com/NREL-Distribution-Suites/grid-data-models#readme
|
|
5
5
|
Project-URL: Issues, https://github.com/NREL-Distribution-Suites/grid-data-models/issues
|
|
6
6
|
Project-URL: Source, https://github.com/NREL-Distribution-Suites/grid-data-models
|
|
@@ -13,9 +13,9 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
13
13
|
Requires-Python: >=3.11
|
|
14
14
|
Requires-Dist: geopandas
|
|
15
15
|
Requires-Dist: importlib-metadata
|
|
16
|
-
Requires-Dist: infrasys
|
|
16
|
+
Requires-Dist: infrasys==1.2.1
|
|
17
17
|
Requires-Dist: networkx
|
|
18
|
-
Requires-Dist: pandas~=3.0.
|
|
18
|
+
Requires-Dist: pandas~=3.0.5
|
|
19
19
|
Requires-Dist: plotly
|
|
20
20
|
Requires-Dist: pydantic
|
|
21
21
|
Requires-Dist: semver
|
|
@@ -30,13 +30,13 @@ Requires-Dist: pytest-doctestplus; extra == 'dev'
|
|
|
30
30
|
Requires-Dist: ruff; extra == 'dev'
|
|
31
31
|
Provides-Extra: doc
|
|
32
32
|
Requires-Dist: autodoc-pydantic[erdantic]; extra == 'doc'
|
|
33
|
-
Requires-Dist: jupyter-book<3,>=2.1.
|
|
33
|
+
Requires-Dist: jupyter-book<3,>=2.1.6; extra == 'doc'
|
|
34
34
|
Requires-Dist: myst-parser; extra == 'doc'
|
|
35
35
|
Requires-Dist: pydata-sphinx-theme; extra == 'doc'
|
|
36
36
|
Requires-Dist: sphinx; extra == 'doc'
|
|
37
37
|
Requires-Dist: sphinxcontrib-mermaid; extra == 'doc'
|
|
38
38
|
Provides-Extra: mcp
|
|
39
|
-
Requires-Dist: mcp>=1.
|
|
39
|
+
Requires-Dist: mcp>=2.1.0; extra == 'mcp'
|
|
40
40
|
Description-Content-Type: text/markdown
|
|
41
41
|
|
|
42
42
|
|
|
@@ -24,19 +24,19 @@ dependencies = [
|
|
|
24
24
|
"semver",
|
|
25
25
|
"networkx",
|
|
26
26
|
"pydantic",
|
|
27
|
-
"infrasys
|
|
27
|
+
"infrasys==1.2.1",
|
|
28
28
|
"importlib_metadata",
|
|
29
29
|
"typer",
|
|
30
|
-
"pandas~=3.0.
|
|
30
|
+
"pandas~=3.0.5",
|
|
31
31
|
"geopandas",
|
|
32
32
|
"plotly",
|
|
33
33
|
]
|
|
34
34
|
|
|
35
35
|
[project.optional-dependencies]
|
|
36
|
-
mcp = ["mcp>=1.
|
|
36
|
+
mcp = ["mcp>=2.1.0"]
|
|
37
37
|
dev = ["pre-commit", "pytest", "pytest-cov", "pytest-doctestplus", "pytest-asyncio", "ruff", "docutils"]
|
|
38
38
|
doc = [
|
|
39
|
-
"jupyter-book>=2.1.
|
|
39
|
+
"jupyter-book>=2.1.6,<3",
|
|
40
40
|
"sphinx",
|
|
41
41
|
"pydata-sphinx-theme",
|
|
42
42
|
"myst-parser",
|
|
@@ -5,6 +5,7 @@ __version__ = VERSION
|
|
|
5
5
|
# from gdm.tracked_changes import apply_tracked_changes, apply_update_scenario, get_distribution_system_on_date
|
|
6
6
|
from gdm.distribution.model_reduction.reducer import (
|
|
7
7
|
reduce_to_primary_system,
|
|
8
|
+
reduce_to_radial_network,
|
|
8
9
|
reduce_to_three_phase_system,
|
|
9
10
|
)
|
|
10
11
|
from gdm.distribution.distribution_graph import build_graph_from_system
|
{grid_data_models-2.3.6 → grid_data_models-2.3.8}/src/gdm/distribution/distribution_system.py
RENAMED
|
@@ -213,6 +213,9 @@ class DistributionSystem(System):
|
|
|
213
213
|
"type": edge.__class__,
|
|
214
214
|
"is_closed": True,
|
|
215
215
|
"in_service": edge.in_service,
|
|
216
|
+
"phases": [phase.value for phase in edge.winding_phases[0]]
|
|
217
|
+
if isinstance(edge, DistributionTransformerBase)
|
|
218
|
+
else [phase.value for phase in edge.phases],
|
|
216
219
|
}
|
|
217
220
|
if isinstance(edge, DistributionSwitchBase):
|
|
218
221
|
data["is_closed"] = True if len(edge.is_closed) == sum(edge.is_closed) else False
|
|
@@ -408,13 +411,20 @@ class DistributionSystem(System):
|
|
|
408
411
|
cycles = self.get_cycles(dfs_tree)
|
|
409
412
|
while cycles:
|
|
410
413
|
cycle = cycles[0]
|
|
411
|
-
|
|
412
|
-
logger.debug(f" Cycle found: {cycle}, switch
|
|
413
|
-
if
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
414
|
+
switch_edges = self.find_switch_buses_in_cycle(cycle)
|
|
415
|
+
logger.debug(f" Cycle found: {cycle}, switch edges in cycle: {switch_edges}")
|
|
416
|
+
if switch_edges:
|
|
417
|
+
bus_1, bus_2 = switch_edges[0]
|
|
418
|
+
if dfs_tree.has_edge(bus_1, bus_2):
|
|
419
|
+
dfs_tree.remove_edge(bus_1, bus_2)
|
|
420
|
+
elif dfs_tree.has_edge(bus_2, bus_1):
|
|
421
|
+
dfs_tree.remove_edge(bus_2, bus_1)
|
|
422
|
+
else:
|
|
423
|
+
logger.warning(
|
|
424
|
+
f" Switch edge ({bus_1}, {bus_2}) not present in DFS tree; skipping."
|
|
425
|
+
)
|
|
426
|
+
cycles.pop(0)
|
|
427
|
+
continue
|
|
418
428
|
else:
|
|
419
429
|
bus_1 = random.choice(cycle)
|
|
420
430
|
if cycle.index(bus_1) == len(cycle) - 1:
|
|
@@ -454,12 +464,12 @@ class DistributionSystem(System):
|
|
|
454
464
|
dfs_tree.add_edges_from(pruned_edges_tuples)
|
|
455
465
|
return dfs_tree
|
|
456
466
|
|
|
457
|
-
def find_switch_buses_in_cycle(self, cycle: list[str]) -> list[str]:
|
|
458
|
-
"""Finds the switch
|
|
467
|
+
def find_switch_buses_in_cycle(self, cycle: list[str]) -> list[tuple[str, str]]:
|
|
468
|
+
"""Finds the switch edges in a given cycle.
|
|
459
469
|
|
|
460
|
-
This method identifies the switch
|
|
461
|
-
It checks for edges in the cycle that correspond to
|
|
462
|
-
|
|
470
|
+
This method identifies the switch edges that are part of a cycle in the directed graph.
|
|
471
|
+
It checks for edges in the cycle that correspond to switches and returns the
|
|
472
|
+
(bus_1, bus_2) pairs for each switch edge found.
|
|
463
473
|
|
|
464
474
|
Parameters
|
|
465
475
|
----------
|
|
@@ -468,27 +478,27 @@ class DistributionSystem(System):
|
|
|
468
478
|
|
|
469
479
|
Returns
|
|
470
480
|
-------
|
|
471
|
-
list[str]
|
|
472
|
-
A list of
|
|
481
|
+
list[tuple[str, str]]
|
|
482
|
+
A list of (bus_1, bus_2) tuples for each switch edge found in the cycle.
|
|
473
483
|
|
|
474
484
|
Notes
|
|
475
485
|
-----
|
|
476
|
-
- The method iterates through the edges in the cycle and checks for any
|
|
477
|
-
- It returns the
|
|
486
|
+
- The method iterates through the edges in the cycle and checks for any switches.
|
|
487
|
+
- It returns the bus pairs that are connected by these switches.
|
|
478
488
|
"""
|
|
479
|
-
|
|
489
|
+
switch_edges: list[tuple[str, str]] = []
|
|
490
|
+
ugraph = self.get_undirected_graph()
|
|
480
491
|
for i in range(len(cycle)):
|
|
481
492
|
bus_1 = cycle[i]
|
|
482
493
|
bus_2 = cycle[(i + 1) % len(cycle)]
|
|
483
|
-
edge_data =
|
|
494
|
+
edge_data = ugraph.get_edge_data(bus_1, bus_2)
|
|
484
495
|
if edge_data:
|
|
485
|
-
for
|
|
496
|
+
for _, data in edge_data.items():
|
|
486
497
|
if issubclass(data.get("type"), MatrixImpedanceSwitch):
|
|
487
|
-
|
|
488
|
-
switch_buses.append(bus_2)
|
|
498
|
+
switch_edges.append((bus_1, bus_2))
|
|
489
499
|
logger.info(f"Switch found between {bus_1} and {bus_2}")
|
|
490
500
|
break
|
|
491
|
-
return
|
|
501
|
+
return switch_edges
|
|
492
502
|
|
|
493
503
|
def get_split_phase_mapping(
|
|
494
504
|
self,
|
|
@@ -952,7 +962,9 @@ class DistributionSystem(System):
|
|
|
952
962
|
logger.info("Converting models from GeometryBranch to MatrixImpedanceBranch...")
|
|
953
963
|
auto_add = self.auto_add_composed_components
|
|
954
964
|
self.auto_add_composed_components = True
|
|
955
|
-
branches = list(self.get_components(GeometryBranch)) + list(
|
|
965
|
+
branches = list(self.get_components(GeometryBranch)) + list(
|
|
966
|
+
self.get_components(GeometrySwitch)
|
|
967
|
+
)
|
|
956
968
|
for branch in branches:
|
|
957
969
|
impedence_branch = branch.to_matrix_representation(
|
|
958
970
|
frequency_hz, soil_resistivity_ohm_m
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from typing import Type, Union, Callable
|
|
3
|
+
|
|
4
|
+
from infrasys.time_series_models import SingleTimeSeries, TimeSeriesData
|
|
5
|
+
from infrasys import Component
|
|
6
|
+
import networkx as nx
|
|
7
|
+
|
|
8
|
+
from gdm.distribution.components.base.distribution_branch_base import DistributionBranchBase
|
|
9
|
+
from gdm.distribution.components.base.distribution_switch_base import DistributionSwitchBase
|
|
10
|
+
from gdm.distribution.components.distribution_bus import DistributionBus
|
|
11
|
+
from gdm.distribution.components.distribution_load import DistributionLoad
|
|
12
|
+
from gdm.distribution.components.distribution_solar import DistributionSolar
|
|
13
|
+
from gdm.distribution.components.distribution_battery import DistributionBattery
|
|
14
|
+
from gdm.distribution.components.geometry_branch import GeometryBranch
|
|
15
|
+
from gdm.distribution.components.matrix_impedance_branch import MatrixImpedanceBranch
|
|
16
|
+
from gdm.distribution.components.matrix_impedance_switch import MatrixImpedanceSwitch
|
|
17
|
+
from gdm.distribution.distribution_system import (
|
|
18
|
+
DistributionSystem,
|
|
19
|
+
UserAttributes,
|
|
20
|
+
)
|
|
21
|
+
from gdm.distribution.equipment.matrix_impedance_switch_equipment import (
|
|
22
|
+
MatrixImpedanceSwitchEquipment,
|
|
23
|
+
)
|
|
24
|
+
from gdm.distribution.enums import Phase
|
|
25
|
+
from gdm.distribution.sys_functools import (
|
|
26
|
+
get_aggregated_load_time_series,
|
|
27
|
+
get_aggregated_solar_time_series,
|
|
28
|
+
get_aggregated_battery_time_series,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get_three_phase_buses(
|
|
33
|
+
dist_system: DistributionSystem,
|
|
34
|
+
) -> list[str]:
|
|
35
|
+
three_phase_buses = [
|
|
36
|
+
bus.name
|
|
37
|
+
for bus in dist_system.get_components(
|
|
38
|
+
DistributionBus,
|
|
39
|
+
filter_func=lambda x: set((Phase.A, Phase.B, Phase.C)).issubset(x.phases),
|
|
40
|
+
)
|
|
41
|
+
]
|
|
42
|
+
graph = dist_system.get_undirected_graph()
|
|
43
|
+
|
|
44
|
+
subgraph = graph.subgraph(three_phase_buses)
|
|
45
|
+
connected_components = list(nx.connected_components(subgraph))
|
|
46
|
+
|
|
47
|
+
max_size = 0
|
|
48
|
+
max_size_set = three_phase_buses
|
|
49
|
+
for island in connected_components:
|
|
50
|
+
if len(island) > max_size:
|
|
51
|
+
max_size = len(island)
|
|
52
|
+
max_size_set = island
|
|
53
|
+
|
|
54
|
+
return max_size_set
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _get_primary_buses(dist_system: DistributionSystem) -> list[str]:
|
|
58
|
+
return [
|
|
59
|
+
bus.name
|
|
60
|
+
for bus in dist_system.get_components(
|
|
61
|
+
DistributionBus,
|
|
62
|
+
filter_func=lambda x: x.rated_voltage.to("kilovolt").magnitude > 1.0,
|
|
63
|
+
)
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _get_aggregated_bus_component(
|
|
68
|
+
subtree_system: DistributionSystem,
|
|
69
|
+
bus: DistributionBus,
|
|
70
|
+
model_type: DistributionLoad | DistributionSolar,
|
|
71
|
+
split_phase_mapping: dict[str, set[Phase]],
|
|
72
|
+
model_components: list[DistributionLoad | DistributionSolar] | None = None,
|
|
73
|
+
) -> DistributionLoad | DistributionSolar:
|
|
74
|
+
if model_components is None:
|
|
75
|
+
model_components = subtree_system.get_components(model_type)
|
|
76
|
+
return model_type.aggregate(
|
|
77
|
+
instances=list(model_components),
|
|
78
|
+
bus=bus,
|
|
79
|
+
name=str(uuid.uuid4()),
|
|
80
|
+
split_phase_mapping=split_phase_mapping,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _aggregate_subtree_components(
|
|
85
|
+
subtree_system: DistributionSystem,
|
|
86
|
+
dist_system: DistributionSystem,
|
|
87
|
+
reduced_system: DistributionSystem,
|
|
88
|
+
model_types: list[Type[Component]],
|
|
89
|
+
source_bus_names: list[str],
|
|
90
|
+
aggregated_component_uuids: set[uuid.UUID],
|
|
91
|
+
target_bus: DistributionBus,
|
|
92
|
+
split_phase_mapping: dict[str, set[Phase]],
|
|
93
|
+
agg_time_series: bool,
|
|
94
|
+
time_series_type: Type[TimeSeriesData],
|
|
95
|
+
ts_agg_func_mapper: dict[Type[Component], Callable],
|
|
96
|
+
) -> None:
|
|
97
|
+
for model_type in model_types:
|
|
98
|
+
model_components = [
|
|
99
|
+
component
|
|
100
|
+
for component in subtree_system.get_components(model_type)
|
|
101
|
+
if component.uuid not in aggregated_component_uuids
|
|
102
|
+
and getattr(component, "bus", None).name in source_bus_names
|
|
103
|
+
]
|
|
104
|
+
aggregated_component_uuids.update(component.uuid for component in model_components)
|
|
105
|
+
if not model_components:
|
|
106
|
+
continue
|
|
107
|
+
agg_component = _get_aggregated_bus_component(
|
|
108
|
+
subtree_system,
|
|
109
|
+
target_bus,
|
|
110
|
+
model_type=model_type,
|
|
111
|
+
split_phase_mapping=split_phase_mapping,
|
|
112
|
+
model_components=model_components,
|
|
113
|
+
)
|
|
114
|
+
reduced_system.add_component(agg_component)
|
|
115
|
+
if not agg_time_series:
|
|
116
|
+
continue
|
|
117
|
+
agg_comp = reduced_system.get_component(model_type, agg_component.name)
|
|
118
|
+
ts_metadata = dist_system.list_time_series_metadata(
|
|
119
|
+
model_components[0], time_series_type=time_series_type
|
|
120
|
+
)
|
|
121
|
+
for metadata in ts_metadata:
|
|
122
|
+
ts_aggregate = ts_agg_func_mapper[model_type](
|
|
123
|
+
dist_system, model_components, metadata.name, time_series_type
|
|
124
|
+
)
|
|
125
|
+
user_attr = UserAttributes.model_validate(metadata.features)
|
|
126
|
+
user_attr.use_actual = True
|
|
127
|
+
reduced_system.add_time_series(ts_aggregate, agg_comp, **user_attr.model_dump())
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _reduce_system(
|
|
131
|
+
dist_system: DistributionSystem,
|
|
132
|
+
bus_subset: list[DistributionBus],
|
|
133
|
+
name: str,
|
|
134
|
+
agg_time_series: bool = False,
|
|
135
|
+
agg_timeseries: bool | None = None,
|
|
136
|
+
time_series_type: Type[TimeSeriesData] = SingleTimeSeries,
|
|
137
|
+
) -> DistributionSystem:
|
|
138
|
+
if agg_timeseries is not None:
|
|
139
|
+
agg_time_series = agg_timeseries
|
|
140
|
+
|
|
141
|
+
closed_graph = _closed_edge_graph(dist_system)
|
|
142
|
+
if nx.cycle_basis(nx.Graph(closed_graph)):
|
|
143
|
+
raise ValueError("The system contains closed loops; run reduce_to_radial_network first.")
|
|
144
|
+
|
|
145
|
+
original_tree = dist_system.get_directed_graph()
|
|
146
|
+
reduced_system = dist_system.get_subsystem(
|
|
147
|
+
bus_subset,
|
|
148
|
+
name,
|
|
149
|
+
keep_time_series=agg_time_series,
|
|
150
|
+
time_series_type=time_series_type,
|
|
151
|
+
directed_graph=original_tree,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
split_phase_mapping = dist_system.get_split_phase_mapping(directed_graph=original_tree)
|
|
155
|
+
reduced_network_tree = original_tree.subgraph(bus_subset)
|
|
156
|
+
retained_bus_names = set(bus_subset)
|
|
157
|
+
aggregated_bus_names: set[str] = set()
|
|
158
|
+
aggregated_component_uuids: set[uuid.UUID] = set()
|
|
159
|
+
ts_agg_func_mapper: dict[Union[Type[DistributionLoad], Type[DistributionSolar]], Callable] = {
|
|
160
|
+
DistributionLoad: get_aggregated_load_time_series,
|
|
161
|
+
DistributionSolar: get_aggregated_solar_time_series,
|
|
162
|
+
DistributionBattery: get_aggregated_battery_time_series,
|
|
163
|
+
}
|
|
164
|
+
for node in reduced_network_tree.nodes():
|
|
165
|
+
if reduced_network_tree.out_degree(node) < original_tree.out_degree(node):
|
|
166
|
+
sucessors_diff = set(original_tree.successors(node)) - set(
|
|
167
|
+
reduced_network_tree.successors(node)
|
|
168
|
+
)
|
|
169
|
+
successors_descendants = [
|
|
170
|
+
snode
|
|
171
|
+
for successor in sucessors_diff
|
|
172
|
+
for snode in nx.descendants(original_tree, successor)
|
|
173
|
+
] + list(sucessors_diff)
|
|
174
|
+
successors_descendants = list(
|
|
175
|
+
set(successors_descendants) - retained_bus_names - aggregated_bus_names
|
|
176
|
+
)
|
|
177
|
+
aggregated_bus_names.update(successors_descendants)
|
|
178
|
+
if not successors_descendants:
|
|
179
|
+
continue
|
|
180
|
+
subtree = original_tree.subgraph(successors_descendants)
|
|
181
|
+
subtree_system = dist_system.get_subsystem(
|
|
182
|
+
list(subtree.nodes),
|
|
183
|
+
"",
|
|
184
|
+
directed_graph=original_tree,
|
|
185
|
+
)
|
|
186
|
+
model_types = subtree_system.get_model_types_with_field_type(DistributionBus)
|
|
187
|
+
_aggregate_subtree_components(
|
|
188
|
+
subtree_system,
|
|
189
|
+
dist_system,
|
|
190
|
+
reduced_system,
|
|
191
|
+
model_types,
|
|
192
|
+
successors_descendants,
|
|
193
|
+
aggregated_component_uuids,
|
|
194
|
+
reduced_system.get_component(DistributionBus, node),
|
|
195
|
+
split_phase_mapping,
|
|
196
|
+
agg_time_series,
|
|
197
|
+
time_series_type,
|
|
198
|
+
ts_agg_func_mapper,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
return reduced_system
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def reduce_to_three_phase_system(
|
|
205
|
+
dist_system: DistributionSystem,
|
|
206
|
+
name: str,
|
|
207
|
+
agg_time_series: bool = False,
|
|
208
|
+
agg_timeseries: bool | None = None,
|
|
209
|
+
time_series_type: Type[TimeSeriesData] = SingleTimeSeries,
|
|
210
|
+
) -> DistributionSystem:
|
|
211
|
+
three_phase_buses = _get_three_phase_buses(dist_system)
|
|
212
|
+
return _reduce_system(
|
|
213
|
+
dist_system,
|
|
214
|
+
three_phase_buses,
|
|
215
|
+
name,
|
|
216
|
+
agg_time_series,
|
|
217
|
+
agg_timeseries,
|
|
218
|
+
time_series_type,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def reduce_to_primary_system(
|
|
223
|
+
dist_system: DistributionSystem,
|
|
224
|
+
name: str,
|
|
225
|
+
agg_time_series: bool = False,
|
|
226
|
+
agg_timeseries: bool | None = None,
|
|
227
|
+
time_series_type: Type[TimeSeriesData] = SingleTimeSeries,
|
|
228
|
+
) -> DistributionSystem:
|
|
229
|
+
primary_buses = _get_primary_buses(dist_system)
|
|
230
|
+
return _reduce_system(
|
|
231
|
+
dist_system,
|
|
232
|
+
primary_buses,
|
|
233
|
+
name,
|
|
234
|
+
agg_time_series,
|
|
235
|
+
agg_timeseries,
|
|
236
|
+
time_series_type,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _normalize_cycle(cycle: list[str]) -> tuple[str, ...]:
|
|
241
|
+
"""Rotate a cycle so that it starts at its lexicographically smallest bus."""
|
|
242
|
+
start = min(range(len(cycle)), key=cycle.__getitem__)
|
|
243
|
+
return tuple(cycle[start:] + cycle[:start])
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _closed_edge_graph(system: DistributionSystem) -> nx.MultiGraph:
|
|
247
|
+
"""Build an undirected graph containing only electrically closed edges.
|
|
248
|
+
|
|
249
|
+
Mirrors the edge filtering performed by ``get_directed_graph``: open switch
|
|
250
|
+
edges (any phase open) are excluded, while all other branches and
|
|
251
|
+
transformers count as closed.
|
|
252
|
+
"""
|
|
253
|
+
graph = system.get_undirected_graph()
|
|
254
|
+
closed_graph = nx.MultiGraph()
|
|
255
|
+
for node in graph.nodes():
|
|
256
|
+
closed_graph.add_node(node)
|
|
257
|
+
for u, v, key, data in graph.edges(data=True, keys=True):
|
|
258
|
+
if data.get("is_closed"):
|
|
259
|
+
closed_graph.add_edge(u, v, key=key, **data)
|
|
260
|
+
return closed_graph
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _cycle_components(
|
|
264
|
+
system: DistributionSystem,
|
|
265
|
+
cycle: tuple[str, ...],
|
|
266
|
+
graph: nx.MultiGraph,
|
|
267
|
+
is_candidate: Callable[[type], bool],
|
|
268
|
+
) -> list[DistributionBranchBase]:
|
|
269
|
+
"""Resolve the branch components on a cycle that match ``is_candidate``.
|
|
270
|
+
|
|
271
|
+
Components are deduplicated and returned sorted by name so that any
|
|
272
|
+
downstream selection is deterministic.
|
|
273
|
+
"""
|
|
274
|
+
seen: dict[uuid.UUID, DistributionBranchBase] = {}
|
|
275
|
+
for i in range(len(cycle)):
|
|
276
|
+
bus_1, bus_2 = cycle[i], cycle[(i + 1) % len(cycle)]
|
|
277
|
+
if not graph.has_edge(bus_1, bus_2):
|
|
278
|
+
continue
|
|
279
|
+
for data in graph[bus_1][bus_2].values():
|
|
280
|
+
component_type = data["type"]
|
|
281
|
+
if is_candidate(component_type):
|
|
282
|
+
component = system.get_component(component_type, data["name"])
|
|
283
|
+
seen.setdefault(component.uuid, component)
|
|
284
|
+
return sorted(seen.values(), key=lambda component: component.name)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _switch_equipment_from_branch(
|
|
288
|
+
line: DistributionBranchBase,
|
|
289
|
+
) -> MatrixImpedanceSwitchEquipment:
|
|
290
|
+
"""Build switch equipment from a branch's impedance data."""
|
|
291
|
+
if isinstance(line, GeometryBranch):
|
|
292
|
+
line = line.to_matrix_representation(frequency_hz=60)
|
|
293
|
+
if not isinstance(line, MatrixImpedanceBranch):
|
|
294
|
+
msg = (
|
|
295
|
+
f"Cannot convert {line.__class__.__name__} to a switch. Only "
|
|
296
|
+
"MatrixImpedanceBranch and GeometryBranch lines are supported."
|
|
297
|
+
)
|
|
298
|
+
raise ValueError(msg)
|
|
299
|
+
return MatrixImpedanceSwitchEquipment(**line.equipment.model_dump(exclude_none=True))
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _convert_line_to_open_switch(
|
|
303
|
+
system: DistributionSystem, line: DistributionBranchBase
|
|
304
|
+
) -> MatrixImpedanceSwitch:
|
|
305
|
+
"""Replace ``line`` with an open switch between the same buses."""
|
|
306
|
+
name = f"{line.name}_switch"
|
|
307
|
+
existing_names = {switch.name for switch in system.get_components(MatrixImpedanceSwitch)}
|
|
308
|
+
suffix = 1
|
|
309
|
+
while name in existing_names:
|
|
310
|
+
suffix += 1
|
|
311
|
+
name = f"{line.name}_switch_{suffix}"
|
|
312
|
+
|
|
313
|
+
switch = MatrixImpedanceSwitch(
|
|
314
|
+
buses=line.buses,
|
|
315
|
+
length=line.length,
|
|
316
|
+
phases=list(line.phases),
|
|
317
|
+
substation=line.substation,
|
|
318
|
+
feeder=line.feeder,
|
|
319
|
+
name=name,
|
|
320
|
+
is_closed=[False] * len(line.phases),
|
|
321
|
+
equipment=_switch_equipment_from_branch(line),
|
|
322
|
+
uuid=uuid.uuid5(uuid.NAMESPACE_URL, f"gdm.radial_switch.{name}"),
|
|
323
|
+
)
|
|
324
|
+
system.remove_component(line)
|
|
325
|
+
system.add_component(switch)
|
|
326
|
+
return switch
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def reduce_to_radial_network(dist_system: DistributionSystem, name: str) -> DistributionSystem:
|
|
330
|
+
"""Reduce a looped distribution system to a radial network.
|
|
331
|
+
|
|
332
|
+
Every electrical loop in the input is broken deterministically: if a loop
|
|
333
|
+
contains a closed switch, that switch is opened; otherwise the shortest
|
|
334
|
+
line on the loop (ties broken by component name) is replaced with an open
|
|
335
|
+
``MatrixImpedanceSwitch`` between the same buses. The input system is not
|
|
336
|
+
modified; a reduced copy is returned.
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
dist_system : DistributionSystem
|
|
341
|
+
Source distribution system, possibly containing loops. Must be a single
|
|
342
|
+
connected component fed from one voltage source (the same requirement
|
|
343
|
+
as ``get_source_bus``).
|
|
344
|
+
name : str
|
|
345
|
+
Name of the reduced system.
|
|
346
|
+
|
|
347
|
+
Returns
|
|
348
|
+
-------
|
|
349
|
+
DistributionSystem
|
|
350
|
+
A radial copy of the input system in which no closed loop remains, so
|
|
351
|
+
that ``get_directed_graph`` prunes no edges and emits no "pruned from
|
|
352
|
+
DFS tree" warnings.
|
|
353
|
+
|
|
354
|
+
Notes
|
|
355
|
+
-----
|
|
356
|
+
- Geometry-based lines are converted to their matrix representation at
|
|
357
|
+
60 Hz when they are selected for conversion.
|
|
358
|
+
- Only cycle edges are touched, so the reduced network stays connected.
|
|
359
|
+
"""
|
|
360
|
+
system = dist_system.deepcopy()
|
|
361
|
+
system.name = name
|
|
362
|
+
closed_graph = _closed_edge_graph(system)
|
|
363
|
+
|
|
364
|
+
while True:
|
|
365
|
+
cycles = sorted(
|
|
366
|
+
_normalize_cycle(cycle) for cycle in DistributionSystem.get_cycles(closed_graph)
|
|
367
|
+
)
|
|
368
|
+
if not cycles:
|
|
369
|
+
break
|
|
370
|
+
cycle = cycles[0]
|
|
371
|
+
|
|
372
|
+
switches = _cycle_components(
|
|
373
|
+
system, cycle, closed_graph, lambda t: issubclass(t, DistributionSwitchBase)
|
|
374
|
+
)
|
|
375
|
+
if switches:
|
|
376
|
+
switch = switches[0]
|
|
377
|
+
switch.is_closed = [False] * len(switch.phases)
|
|
378
|
+
bus_1, bus_2 = switch.buses[0].name, switch.buses[1].name
|
|
379
|
+
for key in [
|
|
380
|
+
k for k, data in closed_graph[bus_1][bus_2].items() if data["name"] == switch.name
|
|
381
|
+
]:
|
|
382
|
+
closed_graph.remove_edge(bus_1, bus_2, key=key)
|
|
383
|
+
continue
|
|
384
|
+
|
|
385
|
+
lines = _cycle_components(
|
|
386
|
+
system,
|
|
387
|
+
cycle,
|
|
388
|
+
closed_graph,
|
|
389
|
+
lambda t: (
|
|
390
|
+
issubclass(t, DistributionBranchBase) and not issubclass(t, DistributionSwitchBase)
|
|
391
|
+
),
|
|
392
|
+
)
|
|
393
|
+
if not lines:
|
|
394
|
+
msg = (
|
|
395
|
+
f"Cycle {list(cycle)} contains no branches or switches that can be "
|
|
396
|
+
"opened to break the loop; cannot reduce the system to a radial network."
|
|
397
|
+
)
|
|
398
|
+
raise ValueError(msg)
|
|
399
|
+
|
|
400
|
+
line = min(lines, key=lambda b: (b.length.to("meter").magnitude, b.name))
|
|
401
|
+
_convert_line_to_open_switch(system, line)
|
|
402
|
+
bus_1, bus_2 = line.buses[0].name, line.buses[1].name
|
|
403
|
+
for key in [
|
|
404
|
+
k for k, data in closed_graph[bus_1][bus_2].items() if data["name"] == line.name
|
|
405
|
+
]:
|
|
406
|
+
closed_graph.remove_edge(bus_1, bus_2, key=key)
|
|
407
|
+
|
|
408
|
+
return system
|