grid-data-models 2.3.7__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.
Files changed (138) hide show
  1. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/PKG-INFO +5 -5
  2. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/pyproject.toml +3 -3
  3. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/__init__.py +1 -0
  4. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/distribution_system.py +29 -22
  5. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/model_reduction/__init__.py +1 -0
  6. grid_data_models-2.3.8/src/gdm/distribution/model_reduction/reducer.py +408 -0
  7. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/sys_functools.py +145 -72
  8. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_3_2__to__2_3_3.py +6 -2
  9. grid_data_models-2.3.8/src/gdm/distribution/upgrade_handler/from__2_3_7__to__2_3_8.py +15 -0
  10. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/upgrade_handler.py +6 -0
  11. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/mcp/server.py +47 -31
  12. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/tracked_changes.py +37 -1
  13. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/version.py +1 -1
  14. grid_data_models-2.3.7/src/gdm/distribution/model_reduction/reducer.py +0 -176
  15. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/.gitignore +0 -0
  16. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/LICENSE.txt +0 -0
  17. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/README.md +0 -0
  18. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/__init__.py +0 -0
  19. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/cli/cli.py +0 -0
  20. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/cli/reducer.py +0 -0
  21. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/constants.py +0 -0
  22. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/dataset/__init__.py +0 -0
  23. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/dataset/cost_model.py +0 -0
  24. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/dataset/dataset_system.py +0 -0
  25. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/catalog_system.py +0 -0
  26. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/common/__init__.py +0 -0
  27. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/common/curve.py +0 -0
  28. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/common/limitset.py +0 -0
  29. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/common/sequence_pair.py +0 -0
  30. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/components/__init__.py +0 -0
  31. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/components/base/__init__.py +0 -0
  32. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/components/base/distribution_branch_base.py +0 -0
  33. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/components/base/distribution_component_base.py +0 -0
  34. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/components/base/distribution_switch_base.py +0 -0
  35. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/components/base/distribution_transformer_base.py +0 -0
  36. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/components/distribution_battery.py +0 -0
  37. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/components/distribution_bus.py +0 -0
  38. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/components/distribution_capacitor.py +0 -0
  39. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/components/distribution_feeder.py +0 -0
  40. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/components/distribution_load.py +0 -0
  41. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/components/distribution_regulator.py +0 -0
  42. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/components/distribution_solar.py +0 -0
  43. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/components/distribution_substation.py +0 -0
  44. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/components/distribution_transformer.py +0 -0
  45. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/components/distribution_vsource.py +0 -0
  46. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/components/geometry_branch.py +0 -0
  47. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/components/geometry_switch.py +0 -0
  48. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/components/matrix_impedance_branch.py +0 -0
  49. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/components/matrix_impedance_fuse.py +0 -0
  50. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/components/matrix_impedance_recloser.py +0 -0
  51. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/components/matrix_impedance_switch.py +0 -0
  52. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/components/sequence_impedance_branch.py +0 -0
  53. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/controllers/__init__.py +0 -0
  54. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/controllers/base/__init__.py +0 -0
  55. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/controllers/base/capacitor_controller_base.py +0 -0
  56. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/controllers/base/inverter_controller_base.py +0 -0
  57. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/controllers/distribution_capacitor_controller.py +0 -0
  58. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/controllers/distribution_inverter_controller.py +0 -0
  59. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/controllers/distribution_recloser_controller.py +0 -0
  60. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/controllers/distribution_regulator_controller.py +0 -0
  61. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/controllers/distribution_switch_controller.py +0 -0
  62. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/distribution_graph.py +0 -0
  63. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/enums.py +0 -0
  64. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/__init__.py +0 -0
  65. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/bare_conductor_equipment.py +0 -0
  66. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/base/__init__.py +0 -0
  67. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/base/matrix_impedance_branch_equipment_base.py +0 -0
  68. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/battery_equipment.py +0 -0
  69. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/capacitor_equipment.py +0 -0
  70. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/concentric_cable_equipment.py +0 -0
  71. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/distribution_transformer_equipment.py +0 -0
  72. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/geometry_branch_equipment.py +0 -0
  73. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/inverter_equipment.py +0 -0
  74. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/load_equipment.py +0 -0
  75. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/matrix_impedance_branch_equipment.py +0 -0
  76. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/matrix_impedance_fuse_equipment.py +0 -0
  77. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/matrix_impedance_recloser_equipment.py +0 -0
  78. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/matrix_impedance_switch_equipment.py +0 -0
  79. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/phase_capacitor_equipment.py +0 -0
  80. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/phase_load_equipment.py +0 -0
  81. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/phase_voltagesource_equipment.py +0 -0
  82. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/recloser_controller_equipment.py +0 -0
  83. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/sequence_impedance_branch_equipment.py +0 -0
  84. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/solar_equipment.py +0 -0
  85. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/equipment/voltagesource_equipment.py +0 -0
  86. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/market/__init__.py +0 -0
  87. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/market/aggregator.py +0 -0
  88. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/market/tariff.py +0 -0
  89. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/__init__.py +0 -0
  90. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_0_1__to__2_1_2.py +0 -0
  91. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_1_2__to__2_1_3.py +0 -0
  92. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_1_3__to__2_1_4.py +0 -0
  93. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_1_4__to__2_1_5.py +0 -0
  94. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_1_5__to__2_2_0.py +0 -0
  95. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_2_0__to__2_2_1.py +0 -0
  96. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_2_1__to__2_3_0.py +0 -0
  97. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_3_0__to__2_3_1.py +0 -0
  98. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_3_1__to__2_3_2.py +0 -0
  99. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_3_3__to__2_3_4.py +0 -0
  100. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_3_4__to__2_3_5.py +0 -0
  101. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_3_5__to__2_3_6.py +0 -0
  102. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/upgrade_handler/from__2_3_6__to__2_3_7.py +0 -0
  103. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/utils/__init__.py +0 -0
  104. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/distribution/utils/aggregate_components.py +0 -0
  105. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/exceptions.py +0 -0
  106. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/hashing_utils.py +0 -0
  107. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/mcp/__init__.py +0 -0
  108. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/mcp/exceptions.py +0 -0
  109. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/mcp/inspection/__init__.py +0 -0
  110. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/mcp/inspection/inspector.py +0 -0
  111. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/mcp/inspection/relationships.py +0 -0
  112. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/mcp/inspection/topology.py +0 -0
  113. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/mcp/knowledge/__init__.py +0 -0
  114. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/mcp/knowledge/documentation.py +0 -0
  115. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/mcp/operations/__init__.py +0 -0
  116. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/mcp/operations/merger.py +0 -0
  117. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/mcp/operations/splitter.py +0 -0
  118. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/mcp/schemas.py +0 -0
  119. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/mcp/utilities/__init__.py +0 -0
  120. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/mcp/utilities/subsystem.py +0 -0
  121. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/mcp/utilities/timeseries.py +0 -0
  122. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/mcp/validation/__init__.py +0 -0
  123. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/mcp/validation/auto_fixer.py +0 -0
  124. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/mcp/validation/diagnostics.py +0 -0
  125. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/mcp/validation/suggestions.py +0 -0
  126. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/mcp/version.py +0 -0
  127. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/quantities.py +0 -0
  128. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/structural/__init__.py +0 -0
  129. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/structural/components/__init__.py +0 -0
  130. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/structural/components/base.py +0 -0
  131. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/structural/components/building.py +0 -0
  132. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/structural/components/overhead_line_segment.py +0 -0
  133. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/structural/components/pole.py +0 -0
  134. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/structural/components/pvsystem.py +0 -0
  135. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/structural/components/transformer.py +0 -0
  136. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/structural/components/underground_cable.py +0 -0
  137. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/structural/components/underground_junction.py +0 -0
  138. {grid_data_models-2.3.7 → grid_data_models-2.3.8}/src/gdm/structural/structural_system.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: grid-data-models
3
- Version: 2.3.7
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~=1.2
16
+ Requires-Dist: infrasys==1.2.1
17
17
  Requires-Dist: networkx
18
- Requires-Dist: pandas~=3.0.3
18
+ Requires-Dist: pandas~=3.0.5
19
19
  Requires-Dist: plotly
20
20
  Requires-Dist: pydantic
21
21
  Requires-Dist: semver
@@ -36,7 +36,7 @@ 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.28.1; extra == 'mcp'
39
+ Requires-Dist: mcp>=2.1.0; extra == 'mcp'
40
40
  Description-Content-Type: text/markdown
41
41
 
42
42
 
@@ -24,16 +24,16 @@ dependencies = [
24
24
  "semver",
25
25
  "networkx",
26
26
  "pydantic",
27
- "infrasys~=1.2",
27
+ "infrasys==1.2.1",
28
28
  "importlib_metadata",
29
29
  "typer",
30
- "pandas~=3.0.3",
30
+ "pandas~=3.0.5",
31
31
  "geopandas",
32
32
  "plotly",
33
33
  ]
34
34
 
35
35
  [project.optional-dependencies]
36
- mcp = ["mcp>=1.28.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
39
  "jupyter-book>=2.1.6,<3",
@@ -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
@@ -411,13 +411,20 @@ class DistributionSystem(System):
411
411
  cycles = self.get_cycles(dfs_tree)
412
412
  while cycles:
413
413
  cycle = cycles[0]
414
- switch_buses = self.find_switch_buses_in_cycle(cycle)
415
- logger.debug(f" Cycle found: {cycle}, switch buses in cycle: {switch_buses}")
416
- if switch_buses:
417
- try:
418
- dfs_tree.remove_edge(*switch_buses)
419
- except Exception:
420
- dfs_tree.remove_edge(*switch_buses[::-1])
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
421
428
  else:
422
429
  bus_1 = random.choice(cycle)
423
430
  if cycle.index(bus_1) == len(cycle) - 1:
@@ -457,12 +464,12 @@ class DistributionSystem(System):
457
464
  dfs_tree.add_edges_from(pruned_edges_tuples)
458
465
  return dfs_tree
459
466
 
460
- def find_switch_buses_in_cycle(self, cycle: list[str]) -> list[str]:
461
- """Finds the switch buses in a given cycle.
467
+ def find_switch_buses_in_cycle(self, cycle: list[str]) -> list[tuple[str, str]]:
468
+ """Finds the switch edges in a given cycle.
462
469
 
463
- This method identifies the switch buses that are part of a cycle in the directed graph.
464
- It checks for edges in the cycle that correspond to open switches and returns the names
465
- of the buses associated with those switches.
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.
466
473
 
467
474
  Parameters
468
475
  ----------
@@ -471,27 +478,27 @@ class DistributionSystem(System):
471
478
 
472
479
  Returns
473
480
  -------
474
- list[str]
475
- A list of bus names that are associated with open switches in the cycle.
481
+ list[tuple[str, str]]
482
+ A list of (bus_1, bus_2) tuples for each switch edge found in the cycle.
476
483
 
477
484
  Notes
478
485
  -----
479
- - The method iterates through the edges in the cycle and checks for any open switches.
480
- - It returns the names of the buses that are connected by these open switches.
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.
481
488
  """
482
- switch_buses = []
489
+ switch_edges: list[tuple[str, str]] = []
490
+ ugraph = self.get_undirected_graph()
483
491
  for i in range(len(cycle)):
484
492
  bus_1 = cycle[i]
485
493
  bus_2 = cycle[(i + 1) % len(cycle)]
486
- edge_data = self.get_undirected_graph().get_edge_data(bus_1, bus_2)
494
+ edge_data = ugraph.get_edge_data(bus_1, bus_2)
487
495
  if edge_data:
488
- for key, data in edge_data.items():
496
+ for _, data in edge_data.items():
489
497
  if issubclass(data.get("type"), MatrixImpedanceSwitch):
490
- switch_buses.append(bus_1)
491
- switch_buses.append(bus_2)
498
+ switch_edges.append((bus_1, bus_2))
492
499
  logger.info(f"Switch found between {bus_1} and {bus_2}")
493
500
  break
494
- return list(set(switch_buses))
501
+ return switch_edges
495
502
 
496
503
  def get_split_phase_mapping(
497
504
  self,
@@ -1,2 +1,3 @@
1
1
  from gdm.distribution.model_reduction.reducer import reduce_to_primary_system
2
+ from gdm.distribution.model_reduction.reducer import reduce_to_radial_network
2
3
  from gdm.distribution.model_reduction.reducer import reduce_to_three_phase_system
@@ -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