gridstate 2.4.1__tar.gz → 2.6.0__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 (228) hide show
  1. {gridstate-2.4.1 → gridstate-2.6.0}/CHANGELOG.md +35 -0
  2. {gridstate-2.4.1 → gridstate-2.6.0}/PKG-INFO +1 -1
  3. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/_version.py +3 -3
  4. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/api.py +5 -6
  5. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/ipm_hints.py +1 -1
  6. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/pipeline/config.py +55 -0
  7. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/pipeline/passes.py +4 -0
  8. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/pipeline/runner.py +9 -0
  9. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/preprocessing/ipm_setup.py +81 -10
  10. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/solution_hints.py +40 -1
  11. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate.egg-info/PKG-INFO +1 -1
  12. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate.egg-info/scm_version.json +2 -2
  13. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_boxed_balance.py +27 -0
  14. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_ipm_balance_modes.py +67 -0
  15. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_numpy_only_runtime.py +2 -1
  16. gridstate-2.6.0/tests/test_solution_hints.py +98 -0
  17. gridstate-2.4.1/tests/test_solution_hints.py +0 -40
  18. {gridstate-2.4.1 → gridstate-2.6.0}/.github/workflows/ci.yml +0 -0
  19. {gridstate-2.4.1 → gridstate-2.6.0}/.github/workflows/release.yml +0 -0
  20. {gridstate-2.4.1 → gridstate-2.6.0}/.gitignore +0 -0
  21. {gridstate-2.4.1 → gridstate-2.6.0}/.pre-commit-config.yaml +0 -0
  22. {gridstate-2.4.1 → gridstate-2.6.0}/LICENSE +0 -0
  23. {gridstate-2.4.1 → gridstate-2.6.0}/Makefile +0 -0
  24. {gridstate-2.4.1 → gridstate-2.6.0}/README.md +0 -0
  25. {gridstate-2.4.1 → gridstate-2.6.0}/build_cython.py +0 -0
  26. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/__init__.py +0 -0
  27. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/__pyinstaller/__init__.py +0 -0
  28. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/__pyinstaller/hook-gridstate.py +0 -0
  29. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/_arrays.py +0 -0
  30. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/adapters/__init__.py +0 -0
  31. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/adapters/from_pandapower.py +0 -0
  32. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algebra/__init__.py +0 -0
  33. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algebra/_selinv_kernels.py +0 -0
  34. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algebra/base.py +0 -0
  35. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algebra/estimators.py +0 -0
  36. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algebra/jacobian_pattern.py +0 -0
  37. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algebra/normalized_residuals.py +0 -0
  38. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algorithms/__init__.py +0 -0
  39. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algorithms/_gram_numba.py +0 -0
  40. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algorithms/ipm.py +0 -0
  41. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algorithms/ipm_pd.py +0 -0
  42. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algorithms/kkt_solver.py +0 -0
  43. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algorithms/status.py +0 -0
  44. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algorithms/wls.py +0 -0
  45. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/bad_data_repass.py +0 -0
  46. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/bounds.py +0 -0
  47. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/constants.py +0 -0
  48. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/contract/__init__.py +0 -0
  49. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/contract/_legacy.py +0 -0
  50. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/contract/containers.py +0 -0
  51. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/contract/derived.py +0 -0
  52. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/contract/derived_tables.py +0 -0
  53. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/contract/serialize.py +0 -0
  54. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/contract/tables.py +0 -0
  55. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/contract/validate.py +0 -0
  56. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/contract/version.py +0 -0
  57. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/load_characteristics.py +0 -0
  58. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/losses.py +0 -0
  59. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/measurement_ids.py +0 -0
  60. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/pipeline/__init__.py +0 -0
  61. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/pipeline/context.py +0 -0
  62. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/pipeline/manifest.py +0 -0
  63. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/pipeline/orchestration.py +0 -0
  64. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/pipeline/preparation.py +0 -0
  65. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/pipeline/steps.py +0 -0
  66. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/post_processing.py +0 -0
  67. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/preprocessing/__init__.py +0 -0
  68. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/preprocessing/_scan.py +0 -0
  69. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/preprocessing/meas_rows.py +0 -0
  70. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/preprocessing/mirror_voltage.py +0 -0
  71. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/preprocessing/node_props.py +0 -0
  72. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/preprocessing/pseudo_measurements.py +0 -0
  73. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/py.typed +0 -0
  74. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/quality_summary.py +0 -0
  75. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/resolve_pass.py +0 -0
  76. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/result.py +0 -0
  77. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/runtime.py +0 -0
  78. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/shunt_sanity.py +0 -0
  79. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/state.py +0 -0
  80. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/__init__.py +0 -0
  81. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/_filters.py +0 -0
  82. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/apply_resolved.py +0 -0
  83. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/generators.py +0 -0
  84. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/kinds.py +0 -0
  85. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/load_only_injection.py +0 -0
  86. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/loss_filter.py +0 -0
  87. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/measurements.py +0 -0
  88. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/noise.py +0 -0
  89. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/on_line.py +0 -0
  90. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/quality.py +0 -0
  91. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/rpn.py +0 -0
  92. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/shunts.py +0 -0
  93. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/sigma_floor.py +0 -0
  94. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/units.py +0 -0
  95. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/voltage_filter.py +0 -0
  96. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/voltage_nominal.py +0 -0
  97. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/topology.py +0 -0
  98. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/units.py +0 -0
  99. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/utils.py +0 -0
  100. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/v_mirror.py +0 -0
  101. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/v_refine.py +0 -0
  102. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/validation/__init__.py +0 -0
  103. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/validation/_diagnostics.py +0 -0
  104. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/validation/bad_data.py +0 -0
  105. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/validation/chi2_test.py +0 -0
  106. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/validation/observability.py +0 -0
  107. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/working.py +0 -0
  108. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/ybus.py +0 -0
  109. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/z_vector.py +0 -0
  110. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate.egg-info/SOURCES.txt +0 -0
  111. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate.egg-info/dependency_links.txt +0 -0
  112. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate.egg-info/entry_points.txt +0 -0
  113. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate.egg-info/requires.txt +0 -0
  114. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate.egg-info/scm_file_list.json +0 -0
  115. {gridstate-2.4.1 → gridstate-2.6.0}/gridstate.egg-info/top_level.txt +0 -0
  116. {gridstate-2.4.1 → gridstate-2.6.0}/pyproject.toml +0 -0
  117. {gridstate-2.4.1 → gridstate-2.6.0}/setup.cfg +0 -0
  118. {gridstate-2.4.1 → gridstate-2.6.0}/tests/__init__.py +0 -0
  119. {gridstate-2.4.1 → gridstate-2.6.0}/tests/_golden.py +0 -0
  120. {gridstate-2.4.1 → gridstate-2.6.0}/tests/conftest.py +0 -0
  121. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_adapters_lazy_import.py +0 -0
  122. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_aggregate_generators.py +0 -0
  123. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_algebra_base.py +0 -0
  124. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_anti_overshoot.py +0 -0
  125. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_arrays_helpers.py +0 -0
  126. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_bad_data.py +0 -0
  127. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_bad_data_repass.py +0 -0
  128. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_bounds.py +0 -0
  129. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_breaker_reactance.py +0 -0
  130. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_chain_mirror.py +0 -0
  131. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_contract.py +0 -0
  132. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_contract_edge_cases.py +0 -0
  133. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_contract_runtime.py +0 -0
  134. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case118.json +0 -0
  135. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case118.npz +0 -0
  136. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case118_degraded.json +0 -0
  137. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case118_degraded.npz +0 -0
  138. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case14.json +0 -0
  139. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case14.npz +0 -0
  140. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case14_degraded.json +0 -0
  141. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case14_degraded.npz +0 -0
  142. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case30.json +0 -0
  143. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case30.npz +0 -0
  144. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case30_degraded.json +0 -0
  145. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case30_degraded.npz +0 -0
  146. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/manifest.json +0 -0
  147. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/ieee/case118.npz +0 -0
  148. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/ieee/case118_truth.npz +0 -0
  149. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/ieee/case14.npz +0 -0
  150. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/ieee/case14_truth.npz +0 -0
  151. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/ieee/case30.npz +0 -0
  152. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/ieee/case30_truth.npz +0 -0
  153. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_derived_tables.py +0 -0
  154. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_enum_codes.py +0 -0
  155. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_flow_sigma_floor.py +0 -0
  156. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_from_pandapower.py +0 -0
  157. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_generator_shunt_kernels.py +0 -0
  158. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_golden.py +0 -0
  159. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_gram_numba.py +0 -0
  160. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_ieee_cases.py +0 -0
  161. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_ipm.py +0 -0
  162. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_ipm_hints.py +0 -0
  163. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_ipm_injection_roles.py +0 -0
  164. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_ipm_integration.py +0 -0
  165. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_ipm_pd.py +0 -0
  166. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_jacobian_pattern.py +0 -0
  167. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_kkt_solver.py +0 -0
  168. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_load_characteristic_segments.py +0 -0
  169. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_load_only_injection.py +0 -0
  170. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_losses_breakdown.py +0 -0
  171. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_losses_q_convention.py +0 -0
  172. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_losses_trafo_classification.py +0 -0
  173. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_materialize_kernel.py +0 -0
  174. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_measurement_filter_kernels.py +0 -0
  175. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_measurement_ids.py +0 -0
  176. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_mirror_voltage_kernel.py +0 -0
  177. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_near_critical_ranking.py +0 -0
  178. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_neutral_wording.py +0 -0
  179. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_noise_model.py +0 -0
  180. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_normalized_residuals.py +0 -0
  181. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_observability.py +0 -0
  182. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_on_line_kernel.py +0 -0
  183. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_perf_synthetic_network.py +0 -0
  184. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_pipeline.py +0 -0
  185. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_pipeline_config_validation.py +0 -0
  186. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_pipeline_idempotent.py +0 -0
  187. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_pipeline_invariant_errors.py +0 -0
  188. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_pipeline_step_order.py +0 -0
  189. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_pipeline_warm_start.py +0 -0
  190. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_pipeline_working_layer.py +0 -0
  191. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_post_processing.py +0 -0
  192. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_preparation_steps.py +0 -0
  193. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_prepare_network.py +0 -0
  194. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_pseudo_unknown_load.py +0 -0
  195. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_pseudo_v_unobservable.py +0 -0
  196. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_quality_summary_paths.py +0 -0
  197. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_reconcile_balance.py +0 -0
  198. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_refine_merge.py +0 -0
  199. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_resolve_pass.py +0 -0
  200. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_result_quality_summary.py +0 -0
  201. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_rn_selected_inversion.py +0 -0
  202. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_serialize_npz.py +0 -0
  203. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_shunt_sanity.py +0 -0
  204. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_solve_status.py +0 -0
  205. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_state.py +0 -0
  206. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_state_range.py +0 -0
  207. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_telemetry_apply_kernel.py +0 -0
  208. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_telemetry_bounds.py +0 -0
  209. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_telemetry_physics_kernels.py +0 -0
  210. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_tm_code_quality.py +0 -0
  211. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_topology_kernels.py +0 -0
  212. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_topology_port.py +0 -0
  213. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_transformer_from_shunt.py +0 -0
  214. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_units.py +0 -0
  215. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_units_array_kernels.py +0 -0
  216. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_v_mirror.py +0 -0
  217. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_v_refine.py +0 -0
  218. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_variance_charging.py +0 -0
  219. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_version.py +0 -0
  220. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_voltage_calibration_gen_nodes.py +0 -0
  221. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_voltage_range_filter_fallback.py +0 -0
  222. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_wls.py +0 -0
  223. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_wls_robustness.py +0 -0
  224. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_working_container.py +0 -0
  225. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_ybus.py +0 -0
  226. {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_z_vector.py +0 -0
  227. {gridstate-2.4.1 → gridstate-2.6.0}/tools/gen_ieee_fixtures.py +0 -0
  228. {gridstate-2.4.1 → gridstate-2.6.0}/tools/golden.py +0 -0
@@ -9,6 +9,41 @@ types of the merged pull requests; breaking and behavior-changing items are
9
9
  listed separately under "Breaking changes" and "Behavior changes". A release is
10
10
  cut by tagging the merged main commit; the version is derived from that tag.
11
11
 
12
+ ## v2.6.0 (2026-10-18)
13
+
14
+ ### Behavior changes
15
+
16
+ - `ipm_pd` gets the 300-iteration budget with equality rows too (it was 100
17
+ before the primal fallback), including the global `ipm_balance_mode="hard"`:
18
+ with the adaptive box damping and the measurement-space stopping test of
19
+ v2.4.1 the long budget converges where the short one fell back.
20
+
21
+ ### Feat
22
+
23
+ - `ipm_transit_balance_mode` (new, default `soft`): `hard` makes the P and Q
24
+ balance rows of transit nodes (no `exist_gen` / `exist_load`) equality
25
+ constraints of the solver, to `ipm_transit_balance_tol_p_mw` (new, default
26
+ 0.01) / `ipm_transit_balance_tol_q_mvar` (new, default 0.1); the other
27
+ balance rows stay soft. The global hard balance overrides it. `IPMSetup`
28
+ gains `eq_rows` / `eq_tol`.
29
+
30
+ ## v2.5.0 (2026-10-18)
31
+
32
+ ### Behavior changes
33
+
34
+ - After the run, transit nodes (no `exist_gen` / `exist_load`) whose solved
35
+ injection exceeds `transit_injection_warn_mva` (new, default 5 MVA; 0 turns
36
+ it off) are listed in a WARNING: a data conflict, or a generation, load or
37
+ shunt the model does not describe. Estimates are not affected.
38
+
39
+ ### Feat
40
+
41
+ - `ipm_transit_balance_weight_factor` (new, default 0 — off): the balance row
42
+ of a transit node gets the variance median(σ²_data) / factor (never looser
43
+ than the soft balance). Their injection is known to be zero; with the
44
+ default soft weight a data conflict spreads over them as a fictitious
45
+ injection.
46
+
12
47
  ## v2.4.1 (2026-10-18)
13
48
 
14
49
  ### Behavior changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gridstate
3
- Version: 2.4.1
3
+ Version: 2.6.0
4
4
  Summary: Power system State Estimation: pure-Python WLS/IPM on numpy/scipy, input via a versioned file contract (npz).
5
5
  Author-email: Evgeny Istomin <gena@alpisto.eu>
6
6
  License: MIT
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.4.1'
22
- __version_tuple__ = version_tuple = (2, 4, 1)
21
+ __version__ = version = '2.6.0'
22
+ __version_tuple__ = version_tuple = (2, 6, 0)
23
23
 
24
- __commit_id__ = commit_id = 'g640125491'
24
+ __commit_id__ = commit_id = 'ge81a73f8f'
@@ -826,7 +826,7 @@ def _run_ipm(
826
826
  delta, v, _pg, _qg, _pn, _qn = unpack_full(x, layout_ipm)
827
827
  return algebra.evaluate_jacobian(v, delta)
828
828
 
829
- eq_idx = setup.balance_rows if setup.balance_tol is not None else None
829
+ eq_idx = setup.eq_rows
830
830
  pd_result: IPMResult | None = None
831
831
  if solver == "ipm_pd":
832
832
  mb_rows = mb_lo = mb_hi = None
@@ -852,13 +852,12 @@ def _run_ipm(
852
852
  meas_box_lo=mb_lo,
853
853
  meas_box_hi=mb_hi,
854
854
  eq_idx=eq_idx,
855
- eq_tol=setup.balance_tol,
855
+ eq_tol=setup.eq_tol,
856
856
  mu_init=1.0,
857
857
  step_tol=max(tolerance, 1e-4),
858
858
  # Weakly determined units need up to ~260 iterations with separate
859
- # injection parts; a hard balance does not converge in that budget
860
- # either way, so it keeps the short one before the fallback.
861
- max_iter=max(max_iterations, 100 if eq_idx is not None else 300),
859
+ # injection parts, with or without equality rows.
860
+ max_iter=max(max_iterations, 300),
862
861
  tr_radius=0.5,
863
862
  huber_c=huber_c,
864
863
  huber_mask=huber_mask,
@@ -910,7 +909,7 @@ def _run_ipm(
910
909
  huber_adaptive_k=2.0,
911
910
  kkt_solver=kkt,
912
911
  eq_idx=eq_idx,
913
- eq_tol=setup.balance_tol,
912
+ eq_tol=setup.eq_tol,
914
913
  )
915
914
  if solver == "ipm_pd":
916
915
  result.message = f"ipm_pd fallback to ipm ({pd_message}): {result.message}"
@@ -219,7 +219,7 @@ def ipm_convergence_hints(
219
219
  meas_index=setup.meas_index,
220
220
  network_pu=network_pu,
221
221
  # Equality rows: their σ is a penalty, not a measurement error.
222
- skip=setup.balance_rows if setup.balance_tol is not None else None,
222
+ skip=setup.eq_rows,
223
223
  part_rows=setup.role_rows,
224
224
  top=top,
225
225
  )
@@ -396,6 +396,17 @@ class PipelineConfig:
396
396
  "and logged with hints (e.g. the low-voltage root at a node whose V is barely "
397
397
  "observable). 0 disables the check.",
398
398
  )
399
+ transit_injection_warn_mva: float = _param(
400
+ 5.0,
401
+ group=_G_EST,
402
+ label="Transit injection warning, MVA",
403
+ control="number",
404
+ min=0.0,
405
+ max=1e4,
406
+ help="After the run, transit nodes (no exist_gen/exist_load) whose solved "
407
+ "injection exceeds this are listed in a WARNING: a data conflict or an "
408
+ "undescribed generation, load or shunt. 0 disables the warning.",
409
+ )
399
410
  kkt_solver: str = _param(
400
411
  "auto",
401
412
  group=_G_EST,
@@ -587,6 +598,50 @@ class PipelineConfig:
587
598
  "balance row with variance median(σ²_data) / factor. The box carries the node's "
588
599
  "freedom; a loose balance only hides data conflicts as an imbalance. 0 — off.",
589
600
  )
601
+ ipm_transit_balance_weight_factor: float = _param(
602
+ 0.0,
603
+ group=_G_IPM,
604
+ label="Balance weight of transit nodes",
605
+ control="number",
606
+ min=0.0,
607
+ max=1e6,
608
+ depends={"algorithm": "ipm"},
609
+ help="Transit nodes (no exist_gen/exist_load) get a balance row with variance "
610
+ "median(σ²_data) / factor. Their injection is known to be zero; a loose balance "
611
+ "turns data conflicts into a fictitious injection. 0 — off.",
612
+ )
613
+ ipm_transit_balance_mode: str = _param(
614
+ "soft",
615
+ group=_G_IPM,
616
+ label="Balance mode of transit nodes",
617
+ control="select",
618
+ choices=["soft", "hard"],
619
+ depends={"algorithm": "ipm"},
620
+ help="soft — the balance of a transit node (no exist_gen/exist_load) is a "
621
+ "weighted row like the others (ipm_transit_balance_weight_factor); hard — an "
622
+ "equality constraint to ipm_transit_balance_tol_p_mw / _q_mvar, the other "
623
+ "balance rows stay soft. No effect when ipm_balance_mode=hard.",
624
+ )
625
+ ipm_transit_balance_tol_p_mw: float = _param(
626
+ 0.01,
627
+ group=_G_IPM,
628
+ label="Transit balance tolerance P, MW",
629
+ control="number",
630
+ min=1e-6,
631
+ max=1e3,
632
+ depends={"ipm_transit_balance_mode": "hard"},
633
+ help="Admissible |P injection| of a transit node under the hard transit balance.",
634
+ )
635
+ ipm_transit_balance_tol_q_mvar: float = _param(
636
+ 0.1,
637
+ group=_G_IPM,
638
+ label="Transit balance tolerance Q, MVAr",
639
+ control="number",
640
+ min=1e-6,
641
+ max=1e3,
642
+ depends={"ipm_transit_balance_mode": "hard"},
643
+ help="Admissible |Q injection| of a transit node under the hard transit balance.",
644
+ )
590
645
  ipm_prior_sigma2_bus_equiv_pu: float = _param(
591
646
  0.01,
592
647
  group=_G_IPM,
@@ -51,6 +51,10 @@ def _ipm_kwargs(cfg: PipelineConfig) -> dict:
51
51
  "balance_weight_factor": cfg.ipm_balance_weight_factor,
52
52
  "prior_sigma2_bus_equiv_pu": cfg.ipm_prior_sigma2_bus_equiv_pu,
53
53
  "boxed_balance_weight_factor": cfg.ipm_boxed_balance_weight_factor,
54
+ "transit_balance_weight_factor": cfg.ipm_transit_balance_weight_factor,
55
+ "transit_balance_mode": cfg.ipm_transit_balance_mode,
56
+ "transit_balance_tol_p_mw": cfg.ipm_transit_balance_tol_p_mw,
57
+ "transit_balance_tol_q_mvar": cfg.ipm_transit_balance_tol_q_mvar,
54
58
  "trust_region_scope": cfg.ipm_trust_region_scope,
55
59
  "balance_mode": cfg.ipm_balance_mode,
56
60
  "balance_tol_p_mw": cfg.ipm_balance_tol_p_mw,
@@ -64,6 +64,7 @@ neither :data:`STEPS` nor the manifest and have no ``step_start``):
64
64
 
65
65
  from __future__ import annotations
66
66
 
67
+ import logging
67
68
  import time
68
69
  from collections.abc import Callable
69
70
  from dataclasses import replace
@@ -76,10 +77,14 @@ from gridstate.pipeline.context import Step, _Ctx, _no_solution
76
77
  from gridstate.pipeline.steps import STEPS
77
78
  from gridstate.resolve_pass import fail_out_of_range, state_range_violation
78
79
  from gridstate.result import SEResult
80
+ from gridstate.solution_hints import transit_injection_hints
79
81
  from gridstate.state import InitialPoint
80
82
  from gridstate.working import Working
81
83
 
82
84
 
85
+ logger = logging.getLogger(__name__)
86
+
87
+
83
88
  # Consistency guard between config and derived (the config is used twice): the
84
89
  # producer of the plans and the step executor must be gated by ONE config. An
85
90
  # enabled step that did not get its plan means the configs diverged (derive
@@ -369,6 +374,10 @@ def run(
369
374
  },
370
375
  )
371
376
 
377
+ hints = transit_injection_hints(ctx.model.nodes.to_numpy(), cfg.transit_injection_warn_mva)
378
+ if hints:
379
+ logger.warning("%s", "\n".join(hints))
380
+
372
381
  _emit(
373
382
  on_event,
374
383
  {
@@ -282,6 +282,11 @@ class IPMSetup:
282
282
  # ``balance_tol`` holds the admissible |imbalance| per balance row in p.u.
283
283
  # ``None`` in the soft mode.
284
284
  balance_tol: np.ndarray | None = None
285
+ # Rows the solver enforces as equalities and their admissible |c| (p.u.):
286
+ # every balance row in the hard mode, the transit balance rows with
287
+ # ``transit_balance_mode="hard"``; ``None`` — no equality rows.
288
+ eq_rows: np.ndarray | None = None
289
+ eq_tol: np.ndarray | None = None
285
290
  # ``injection_roles``: rows of ``z`` that measure a box variable directly
286
291
  # (a generation / load part of a node injection) and their measurement ids;
287
292
  # ``role_dropped_ids`` — part rows folded back into a net injection row.
@@ -511,6 +516,15 @@ def _boxed_balance_masks(
511
516
  return boxed[0], boxed[1]
512
517
 
513
518
 
519
+ def _transit_mask(nodes_arr: np.ndarray, node_ids: list[int]) -> np.ndarray:
520
+ """Mask over ``node_ids``: transit nodes (neither ``exist_gen`` nor ``exist_load``)."""
521
+ rows = {int(r["id"]): r for r in nodes_arr}
522
+ return np.array(
523
+ [not (bool(rows[nid]["exist_gen"]) or bool(rows[nid]["exist_load"])) for nid in node_ids],
524
+ dtype=bool,
525
+ )
526
+
527
+
514
528
  def _median_measurement_sigma2(sigma2: np.ndarray) -> float | None:
515
529
  """Median variance (p.u.²) of the data rows; ``None`` when there are none."""
516
530
  positive = sigma2[sigma2 > 1e-15]
@@ -538,6 +552,7 @@ def build_ipm_setup(
538
552
  balance_sigma2: float | None = None,
539
553
  balance_weight_factor: float = 0.1,
540
554
  boxed_balance_weight_factor: float = 0.0,
555
+ transit_balance_weight_factor: float = 0.0,
541
556
  default_box_halfwidth_pu: float = 50.0,
542
557
  prior_sigma2_normal_pu: float = 0.0,
543
558
  prior_sigma2_bus_equiv_pu: float = 0.01,
@@ -546,6 +561,9 @@ def build_ipm_setup(
546
561
  hard_balance_rho_factor: float = 1e3,
547
562
  balance_tol_p_mw: float = 1.0,
548
563
  balance_tol_q_mvar: float = 10.0,
564
+ transit_balance_mode: str = "soft",
565
+ transit_balance_tol_p_mw: float = 0.01,
566
+ transit_balance_tol_q_mvar: float = 0.1,
549
567
  fixed_injection_from_input: bool = False,
550
568
  injection_roles: bool = False,
551
569
  measurements: _ArrayCollection | None = None,
@@ -576,6 +594,12 @@ def build_ipm_setup(
576
594
  ``median(σ²_data) / boxed_balance_weight_factor`` (never looser than
577
595
  the soft one): the box already carries the node's freedom, so a
578
596
  loose balance only hides data conflicts as an imbalance. ``0`` — off.
597
+ transit_balance_weight_factor: balance weight for transit nodes (no
598
+ ``exist_gen`` / ``exist_load``): their injection is known to be
599
+ zero, so a loose balance turns data conflicts into a fictitious
600
+ injection. Their variance is ``median(σ²_data) /
601
+ transit_balance_weight_factor`` (never looser than the soft one).
602
+ ``0`` — off.
579
603
  default_box_halfwidth_pu: half-width (p.u.) of the default box
580
604
  ``[-hw, +hw]`` for an exist_* node with unset bounds.
581
605
  Default 50 p.u. (±5 GW/GVAr), deliberately wider than any
@@ -612,6 +636,14 @@ def build_ipm_setup(
612
636
  hard_balance_rho_factor: see ``balance_mode``.
613
637
  balance_tol_p_mw, balance_tol_q_mvar: admissible |imbalance| of a hard
614
638
  P / Q balance row (MW / MVAr).
639
+ transit_balance_mode: ``"soft"`` (default) — the balance rows of transit
640
+ nodes (no ``exist_gen`` / ``exist_load``) are weighted like the
641
+ others (see ``transit_balance_weight_factor``); ``"hard"`` — they are
642
+ equality constraints with the penalty of ``balance_mode="hard"``,
643
+ the other balance rows stay soft. Ignored when ``balance_mode`` is
644
+ already ``"hard"``.
645
+ transit_balance_tol_p_mw, transit_balance_tol_q_mvar: admissible
646
+ |injection| of a transit node in the hard transit mode (MW / MVAr).
615
647
  fixed_injection_from_input: the balance row of a node carries the
616
648
  input injection of its parts without a box variable (no
617
649
  ``exist_gen`` / ``exist_load``) as a fixed term instead of zero.
@@ -716,6 +748,11 @@ def build_ipm_setup(
716
748
  if balance_mode not in ("soft", "hard"):
717
749
  raise ValueError(f"balance_mode={balance_mode!r} is not one of ['soft', 'hard']")
718
750
  hard = balance_mode == "hard"
751
+ if transit_balance_mode not in ("soft", "hard"):
752
+ raise ValueError(
753
+ f"transit_balance_mode={transit_balance_mode!r} is not one of ['soft', 'hard']"
754
+ )
755
+ transit_hard = transit_balance_mode == "hard" and not hard and n_balance > 0
719
756
 
720
757
  # ---- Prior measurements: z=0, individual σ². Only for box-vars with σ²>0 ----
721
758
  pgen_prior_arr = np.asarray(pgen_sec.prior_s2, dtype=np.float64)
@@ -790,6 +827,22 @@ def build_ipm_setup(
790
827
  q_rows = sigma2_new[n_old + n_balance : n_old + 2 * n_balance]
791
828
  p_rows[boxed_p] = np.minimum(p_rows[boxed_p], boxed_s2)
792
829
  q_rows[boxed_q] = np.minimum(q_rows[boxed_q], boxed_s2)
830
+ if transit_balance_weight_factor > 0.0 and n_balance > 0 and not hard:
831
+ median_sigma2 = _median_measurement_sigma2(sigma2_old)
832
+ if median_sigma2 is not None:
833
+ transit_s2 = min(
834
+ median_sigma2 / float(transit_balance_weight_factor), balance_sigma2_eff
835
+ )
836
+ transit = _transit_mask(nodes_arr, active_ids)
837
+ for start in (n_old, n_old + n_balance):
838
+ rows = sigma2_new[start : start + n_balance]
839
+ rows[transit] = np.minimum(rows[transit], transit_s2)
840
+ transit_eq = np.zeros(0, dtype=np.int64)
841
+ if transit_hard:
842
+ transit = _transit_mask(nodes_arr, active_ids)
843
+ pos = np.flatnonzero(transit)
844
+ transit_eq = np.concatenate([n_old + pos, n_old + n_balance + pos]).astype(np.int64)
845
+ sigma2_new[transit_eq] = _adaptive_balance_sigma2(sigma2_old, hard_balance_rho_factor)
793
846
  if n_prior > 0:
794
847
  prior_sigmas = np.concatenate(
795
848
  [
@@ -867,6 +920,31 @@ def build_ipm_setup(
867
920
  meas_id=new_meas_id,
868
921
  )
869
922
 
923
+ balance_tol = (
924
+ np.concatenate(
925
+ [
926
+ np.full(n_balance, balance_tol_p_mw / BASE_MVA),
927
+ np.full(n_balance, balance_tol_q_mvar / BASE_MVA),
928
+ ]
929
+ )
930
+ if hard
931
+ else None
932
+ )
933
+ eq_rows: np.ndarray | None = None
934
+ eq_tol: np.ndarray | None = None
935
+ if hard:
936
+ eq_rows = np.arange(n_old, n_old + 2 * n_balance, dtype=np.int64)
937
+ eq_tol = balance_tol
938
+ elif transit_eq.size:
939
+ n_t = transit_eq.size // 2
940
+ eq_rows = transit_eq
941
+ eq_tol = np.concatenate(
942
+ [
943
+ np.full(n_t, transit_balance_tol_p_mw / BASE_MVA),
944
+ np.full(n_t, transit_balance_tol_q_mvar / BASE_MVA),
945
+ ]
946
+ )
947
+
870
948
  return IPMSetup(
871
949
  layout=layout,
872
950
  box_idx_in_state=box_idx_arr,
@@ -881,16 +959,9 @@ def build_ipm_setup(
881
959
  meas_index=meas_index_aug,
882
960
  balance_start=n_old,
883
961
  n_balance=n_balance,
884
- balance_tol=(
885
- np.concatenate(
886
- [
887
- np.full(n_balance, balance_tol_p_mw / BASE_MVA),
888
- np.full(n_balance, balance_tol_q_mvar / BASE_MVA),
889
- ]
890
- )
891
- if hard
892
- else None
893
- ),
962
+ balance_tol=balance_tol,
963
+ eq_rows=eq_rows,
964
+ eq_tol=eq_tol,
894
965
  role_rows=np.arange(role_start, n_new, dtype=np.int64),
895
966
  role_meas_ids=role_ids.astype(np.int64),
896
967
  role_dropped_ids=np.asarray(
@@ -6,7 +6,9 @@ wrong. A typical case is a node whose voltage magnitude is barely observable
6
6
  problem then has a second, low-voltage root, and a change of the solver path
7
7
  can land on it while the solver reports success. :func:`low_voltage_nodes`
8
8
  finds such nodes; :func:`low_voltage_hints` and :func:`residual_hints` turn
9
- the solved state into readable lines for the log.
9
+ the solved state into readable lines for the log. :func:`transit_injection_hints`
10
+ lists transit nodes left with an injection, which points at a data conflict or
11
+ at a node whose generation, load or shunt is not described in the model.
10
12
  """
11
13
 
12
14
  from __future__ import annotations
@@ -162,10 +164,47 @@ def residual_hints(
162
164
  return lines
163
165
 
164
166
 
167
+ def transit_injection_hints(nodes: np.ndarray, threshold_mva: float, *, top: int = 10) -> list[str]:
168
+ """Transit nodes with a solved injection above ``threshold_mva``, largest first.
169
+
170
+ A transit node (active, neither ``exist_gen`` nor ``exist_load``) injects
171
+ nothing, so ``|p_inj_calc + j·q_inj_calc|`` there is what the estimate
172
+ could not fit elsewhere. ``nodes`` is a solved node table; empty when no
173
+ node exceeds the threshold or ``threshold_mva <= 0``.
174
+ """
175
+ if threshold_mva <= 0.0 or not len(nodes):
176
+ return []
177
+ transit = (
178
+ nodes["status"].astype(bool)
179
+ & ~nodes["exist_gen"].astype(bool)
180
+ & ~nodes["exist_load"].astype(bool)
181
+ )
182
+ s_mva = np.hypot(nodes["p_inj_calc"], nodes["q_inj_calc"])
183
+ hit = np.flatnonzero(transit & np.isfinite(s_mva) & (s_mva > threshold_mva))
184
+ if not hit.size:
185
+ return []
186
+ order = hit[np.argsort(-s_mva[hit])]
187
+ lines = [
188
+ f"{hit.size} transit node(s) (no exist_gen/exist_load) inject more than "
189
+ f"{threshold_mva:g} MVA; a data conflict or an undescribed generation, load or "
190
+ "shunt at the node:"
191
+ ]
192
+ for k in order[:top].tolist():
193
+ lines.append(
194
+ f" node {int(nodes['id'][k])} ({float(nodes['voltage_nominal'][k]):g} kV): "
195
+ f"P = {float(nodes['p_inj_calc'][k]):.1f} MW, "
196
+ f"Q = {float(nodes['q_inj_calc'][k]):.1f} MVAr"
197
+ )
198
+ if hit.size > top:
199
+ lines.append(f" ... and {hit.size - top} more")
200
+ return lines
201
+
202
+
165
203
  __all__ = [
166
204
  "KIND_NAMES",
167
205
  "describe_row",
168
206
  "low_voltage_hints",
169
207
  "low_voltage_nodes",
170
208
  "residual_hints",
209
+ "transit_injection_hints",
171
210
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gridstate
3
- Version: 2.4.1
3
+ Version: 2.6.0
4
4
  Summary: Power system State Estimation: pure-Python WLS/IPM on numpy/scipy, input via a versioned file contract (npz).
5
5
  Author-email: Evgeny Istomin <gena@alpisto.eu>
6
6
  License: MIT
@@ -1,7 +1,7 @@
1
1
  {
2
- "tag": "2.4.1",
2
+ "tag": "2.6.0",
3
3
  "distance": 0,
4
- "node": "g640125491ba6b35d3beb20b4fd33688f9cceb117",
4
+ "node": "ge81a73f8f85db175e6e502a935ee299b48796dc5",
5
5
  "dirty": false,
6
6
  "branch": "HEAD",
7
7
  "node_date": "2026-10-18"
@@ -209,3 +209,30 @@ def test_compensator_active_power_is_not_a_box_part():
209
209
  p_rows, q_rows = _setup(m, boxed_balance_weight_factor=10.0)
210
210
  assert p_rows[2] < SOFT
211
211
  assert q_rows[2] < SOFT
212
+
213
+
214
+ def test_transit_weight_off_by_default():
215
+ assert PipelineConfig().ipm_transit_balance_weight_factor == 0.0
216
+
217
+
218
+ def test_transit_nodes_get_the_weighted_row():
219
+ # Node order: 1 slack (generation box), 2 transit, 3 and 4 loads with a box.
220
+ p_rows, q_rows = _setup(_build_net_with_transit(), transit_balance_weight_factor=100.0)
221
+ tight = 2.5e-4 / 100.0
222
+ assert np.allclose(p_rows, [SOFT, tight, SOFT, SOFT])
223
+ assert np.allclose(q_rows, [SOFT, tight, SOFT, SOFT])
224
+
225
+
226
+ def test_transit_row_never_looser_than_the_soft_row():
227
+ p_rows, q_rows = _setup(_build_net_with_transit(), transit_balance_weight_factor=0.01)
228
+ assert np.allclose(p_rows, SOFT)
229
+ assert np.allclose(q_rows, SOFT)
230
+
231
+
232
+ def test_transit_and_boxed_weights_combine():
233
+ p_rows, _ = _setup(
234
+ _build_net_with_transit(),
235
+ boxed_balance_weight_factor=10.0,
236
+ transit_balance_weight_factor=100.0,
237
+ )
238
+ assert np.allclose(p_rows, [2.5e-5, 2.5e-6, 2.5e-5, 2.5e-5])
@@ -75,6 +75,52 @@ def test_unknown_balance_mode_is_rejected():
75
75
  _setup(_build_net_with_transit(), balance_mode="strict")
76
76
 
77
77
 
78
+ def test_transit_hard_mode_makes_only_transit_rows_equalities():
79
+ # Node order 1..4: node 2 is the transit node.
80
+ setup = _setup(
81
+ _build_net_with_transit(),
82
+ transit_balance_mode="hard",
83
+ transit_balance_tol_p_mw=0.02,
84
+ transit_balance_tol_q_mvar=0.2,
85
+ hard_balance_rho_factor=1e3,
86
+ )
87
+ assert setup.balance_tol is None
88
+ assert setup.eq_rows is not None and setup.eq_tol is not None
89
+ np.testing.assert_array_equal(setup.eq_rows, [2 + 1, 2 + N_BALANCE + 1])
90
+ np.testing.assert_allclose(setup.eq_tol, [0.02 / BASE_MVA, 0.2 / BASE_MVA])
91
+ sigma2 = setup.r_matrix.diagonal()[2 : 2 + 2 * N_BALANCE]
92
+ soft = _setup(_build_net_with_transit()).r_matrix.diagonal()[2 : 2 + 2 * N_BALANCE]
93
+ is_eq = np.zeros(2 * N_BALANCE, dtype=bool)
94
+ is_eq[[1, N_BALANCE + 1]] = True
95
+ assert np.allclose(sigma2[is_eq], 2.5e-4 / 1e3)
96
+ np.testing.assert_array_equal(sigma2[~is_eq], soft[~is_eq])
97
+
98
+
99
+ def test_soft_modes_have_no_equality_rows():
100
+ setup = _setup(_build_net_with_transit(), transit_balance_weight_factor=10.0)
101
+ assert setup.eq_rows is None and setup.eq_tol is None
102
+
103
+
104
+ def test_global_hard_mode_overrides_the_transit_mode():
105
+ setup = _setup(
106
+ _build_net_with_transit(),
107
+ balance_mode="hard",
108
+ balance_tol_p_mw=2.0,
109
+ transit_balance_mode="hard",
110
+ transit_balance_tol_p_mw=0.02,
111
+ )
112
+ assert setup.eq_rows is not None and setup.eq_tol is not None
113
+ np.testing.assert_array_equal(setup.eq_rows, setup.balance_rows)
114
+ np.testing.assert_array_equal(setup.eq_tol, setup.balance_tol)
115
+
116
+
117
+ def test_unknown_transit_balance_mode_is_rejected():
118
+ with pytest.raises(ValueError, match="transit_balance_mode"):
119
+ _setup(_build_net_with_transit(), transit_balance_mode="strict")
120
+ with pytest.raises(ValueError, match="ipm_transit_balance_mode"):
121
+ PipelineConfig(algorithm="ipm_pd", ipm_transit_balance_mode="strict")
122
+
123
+
78
124
  def test_fixed_injection_from_input():
79
125
  m = _build_net_with_transit()
80
126
  # Transit node 2 gets a non-estimated load and generation.
@@ -131,6 +177,26 @@ def test_hard_balance_closes_the_estimated_imbalance(algorithm: str):
131
177
  assert imb_p <= 0.1 and imb_q <= 0.1
132
178
 
133
179
 
180
+ @pytest.mark.parametrize("algorithm", ["ipm", "ipm_pd"])
181
+ def test_transit_hard_balance_zeroes_the_transit_injection(algorithm: str):
182
+ # Soft, the transit node 2 absorbs the data conflict (≈ 4 MW / 14 MVAr).
183
+ soft = _build_net_with_transit()
184
+ estimate(soft, algorithm=algorithm, reconcile_balance=False, include_quality_summary=False)
185
+ n_soft = soft.nodes.to_numpy()
186
+ assert abs(n_soft["p_inj_calc"][1]) > 1.0 and abs(n_soft["q_inj_calc"][1]) > 1.0
187
+ m = _build_net_with_transit()
188
+ res = estimate(
189
+ m,
190
+ algorithm=algorithm,
191
+ reconcile_balance=False,
192
+ include_quality_summary=False,
193
+ transit_balance_mode="hard",
194
+ )
195
+ assert res.success
196
+ n = m.nodes.to_numpy()
197
+ assert abs(n["p_inj_calc"][1]) <= 0.01 and abs(n["q_inj_calc"][1]) <= 0.1
198
+
199
+
134
200
  def test_ipm_pd_close_to_ipm():
135
201
  results = {}
136
202
  for algorithm in ("ipm", "ipm_pd"):
@@ -170,6 +236,7 @@ def test_measurement_bounds_require_ipm_pd():
170
236
  def test_config_defaults_and_ranges():
171
237
  cfg = PipelineConfig(algorithm="ipm_pd")
172
238
  assert cfg.ipm_balance_mode == "soft"
239
+ assert cfg.ipm_transit_balance_mode == "soft"
173
240
  assert not cfg.ipm_measurement_bounds and not cfg.ipm_fixed_injection_from_input
174
241
  # The soft balance weight may now exceed the median measurement weight tenfold.
175
242
  dataclasses.replace(cfg, ipm_balance_weight_factor=1e3)
@@ -152,7 +152,8 @@ _RUN_SRC = textwrap.dedent(
152
152
  from gridstate.pipeline import run, PipelineConfig
153
153
 
154
154
  working = build_working()
155
- cfg = PipelineConfig(materialize=False)
155
+ # The toy network leaves a transit injection; its WARNING would reach stderr.
156
+ cfg = PipelineConfig(materialize=False, transit_injection_warn_mva=0.0)
156
157
  res = run(working, config=cfg)
157
158
  vmax = float(np.max(res.v_pu)) if getattr(res, "v_pu", None) is not None and res.v_pu.size else float("nan")
158
159
  print(f"RUN_OK success={bool(res.success)} iters={int(res.iterations)} vmax={vmax:.6f}")