gridstate 2.3.0__tar.gz → 2.4.1__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.3.0 → gridstate-2.4.1}/CHANGELOG.md +43 -0
  2. {gridstate-2.3.0 → gridstate-2.4.1}/PKG-INFO +1 -1
  3. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/_version.py +3 -3
  4. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algorithms/ipm_pd.py +48 -8
  5. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/api.py +33 -0
  6. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/contract/__init__.py +2 -0
  7. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/contract/tables.py +39 -1
  8. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/contract/validate.py +25 -1
  9. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/contract/version.py +3 -1
  10. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/ipm_hints.py +16 -43
  11. gridstate-2.4.1/gridstate/load_characteristics.py +99 -0
  12. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/pipeline/config.py +24 -0
  13. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/pipeline/passes.py +1 -0
  14. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/pipeline/preparation.py +1 -1
  15. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/post_processing.py +34 -0
  16. gridstate-2.4.1/gridstate/solution_hints.py +171 -0
  17. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/shunts.py +25 -12
  18. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/working.py +19 -5
  19. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate.egg-info/PKG-INFO +1 -1
  20. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate.egg-info/SOURCES.txt +4 -0
  21. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate.egg-info/scm_file_list.json +4 -0
  22. gridstate-2.4.1/gridstate.egg-info/scm_version.json +8 -0
  23. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_breaker_reactance.py +13 -0
  24. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case118.json +6 -6
  25. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case118.npz +0 -0
  26. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case118_degraded.json +6 -6
  27. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case118_degraded.npz +0 -0
  28. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case14.json +6 -6
  29. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case14.npz +0 -0
  30. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case14_degraded.json +6 -6
  31. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case14_degraded.npz +0 -0
  32. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case30.json +6 -6
  33. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case30.npz +0 -0
  34. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case30_degraded.json +6 -6
  35. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case30_degraded.npz +0 -0
  36. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/manifest.json +75 -12
  37. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_ipm_pd.py +66 -1
  38. gridstate-2.4.1/tests/test_load_characteristic_segments.py +159 -0
  39. gridstate-2.4.1/tests/test_solution_hints.py +40 -0
  40. gridstate-2.3.0/gridstate.egg-info/scm_version.json +0 -8
  41. {gridstate-2.3.0 → gridstate-2.4.1}/.github/workflows/ci.yml +0 -0
  42. {gridstate-2.3.0 → gridstate-2.4.1}/.github/workflows/release.yml +0 -0
  43. {gridstate-2.3.0 → gridstate-2.4.1}/.gitignore +0 -0
  44. {gridstate-2.3.0 → gridstate-2.4.1}/.pre-commit-config.yaml +0 -0
  45. {gridstate-2.3.0 → gridstate-2.4.1}/LICENSE +0 -0
  46. {gridstate-2.3.0 → gridstate-2.4.1}/Makefile +0 -0
  47. {gridstate-2.3.0 → gridstate-2.4.1}/README.md +0 -0
  48. {gridstate-2.3.0 → gridstate-2.4.1}/build_cython.py +0 -0
  49. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/__init__.py +0 -0
  50. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/__pyinstaller/__init__.py +0 -0
  51. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/__pyinstaller/hook-gridstate.py +0 -0
  52. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/_arrays.py +0 -0
  53. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/adapters/__init__.py +0 -0
  54. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/adapters/from_pandapower.py +0 -0
  55. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algebra/__init__.py +0 -0
  56. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algebra/_selinv_kernels.py +0 -0
  57. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algebra/base.py +0 -0
  58. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algebra/estimators.py +0 -0
  59. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algebra/jacobian_pattern.py +0 -0
  60. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algebra/normalized_residuals.py +0 -0
  61. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algorithms/__init__.py +0 -0
  62. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algorithms/_gram_numba.py +0 -0
  63. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algorithms/ipm.py +0 -0
  64. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algorithms/kkt_solver.py +0 -0
  65. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algorithms/status.py +0 -0
  66. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algorithms/wls.py +0 -0
  67. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/bad_data_repass.py +0 -0
  68. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/bounds.py +0 -0
  69. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/constants.py +0 -0
  70. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/contract/_legacy.py +0 -0
  71. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/contract/containers.py +0 -0
  72. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/contract/derived.py +0 -0
  73. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/contract/derived_tables.py +0 -0
  74. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/contract/serialize.py +0 -0
  75. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/losses.py +0 -0
  76. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/measurement_ids.py +0 -0
  77. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/pipeline/__init__.py +0 -0
  78. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/pipeline/context.py +0 -0
  79. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/pipeline/manifest.py +0 -0
  80. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/pipeline/orchestration.py +0 -0
  81. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/pipeline/runner.py +0 -0
  82. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/pipeline/steps.py +0 -0
  83. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/preprocessing/__init__.py +0 -0
  84. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/preprocessing/_scan.py +0 -0
  85. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/preprocessing/ipm_setup.py +0 -0
  86. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/preprocessing/meas_rows.py +0 -0
  87. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/preprocessing/mirror_voltage.py +0 -0
  88. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/preprocessing/node_props.py +0 -0
  89. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/preprocessing/pseudo_measurements.py +0 -0
  90. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/py.typed +0 -0
  91. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/quality_summary.py +0 -0
  92. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/resolve_pass.py +0 -0
  93. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/result.py +0 -0
  94. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/runtime.py +0 -0
  95. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/shunt_sanity.py +0 -0
  96. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/state.py +0 -0
  97. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/__init__.py +0 -0
  98. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/_filters.py +0 -0
  99. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/apply_resolved.py +0 -0
  100. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/generators.py +0 -0
  101. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/kinds.py +0 -0
  102. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/load_only_injection.py +0 -0
  103. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/loss_filter.py +0 -0
  104. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/measurements.py +0 -0
  105. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/noise.py +0 -0
  106. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/on_line.py +0 -0
  107. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/quality.py +0 -0
  108. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/rpn.py +0 -0
  109. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/sigma_floor.py +0 -0
  110. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/units.py +0 -0
  111. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/voltage_filter.py +0 -0
  112. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/voltage_nominal.py +0 -0
  113. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/topology.py +0 -0
  114. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/units.py +0 -0
  115. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/utils.py +0 -0
  116. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/v_mirror.py +0 -0
  117. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/v_refine.py +0 -0
  118. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/validation/__init__.py +0 -0
  119. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/validation/_diagnostics.py +0 -0
  120. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/validation/bad_data.py +0 -0
  121. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/validation/chi2_test.py +0 -0
  122. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/validation/observability.py +0 -0
  123. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/ybus.py +0 -0
  124. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/z_vector.py +0 -0
  125. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate.egg-info/dependency_links.txt +0 -0
  126. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate.egg-info/entry_points.txt +0 -0
  127. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate.egg-info/requires.txt +0 -0
  128. {gridstate-2.3.0 → gridstate-2.4.1}/gridstate.egg-info/top_level.txt +0 -0
  129. {gridstate-2.3.0 → gridstate-2.4.1}/pyproject.toml +0 -0
  130. {gridstate-2.3.0 → gridstate-2.4.1}/setup.cfg +0 -0
  131. {gridstate-2.3.0 → gridstate-2.4.1}/tests/__init__.py +0 -0
  132. {gridstate-2.3.0 → gridstate-2.4.1}/tests/_golden.py +0 -0
  133. {gridstate-2.3.0 → gridstate-2.4.1}/tests/conftest.py +0 -0
  134. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_adapters_lazy_import.py +0 -0
  135. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_aggregate_generators.py +0 -0
  136. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_algebra_base.py +0 -0
  137. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_anti_overshoot.py +0 -0
  138. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_arrays_helpers.py +0 -0
  139. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_bad_data.py +0 -0
  140. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_bad_data_repass.py +0 -0
  141. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_bounds.py +0 -0
  142. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_boxed_balance.py +0 -0
  143. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_chain_mirror.py +0 -0
  144. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_contract.py +0 -0
  145. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_contract_edge_cases.py +0 -0
  146. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_contract_runtime.py +0 -0
  147. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/ieee/case118.npz +0 -0
  148. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/ieee/case118_truth.npz +0 -0
  149. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/ieee/case14.npz +0 -0
  150. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/ieee/case14_truth.npz +0 -0
  151. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/ieee/case30.npz +0 -0
  152. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/ieee/case30_truth.npz +0 -0
  153. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_derived_tables.py +0 -0
  154. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_enum_codes.py +0 -0
  155. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_flow_sigma_floor.py +0 -0
  156. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_from_pandapower.py +0 -0
  157. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_generator_shunt_kernels.py +0 -0
  158. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_golden.py +0 -0
  159. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_gram_numba.py +0 -0
  160. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_ieee_cases.py +0 -0
  161. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_ipm.py +0 -0
  162. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_ipm_balance_modes.py +0 -0
  163. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_ipm_hints.py +0 -0
  164. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_ipm_injection_roles.py +0 -0
  165. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_ipm_integration.py +0 -0
  166. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_jacobian_pattern.py +0 -0
  167. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_kkt_solver.py +0 -0
  168. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_load_only_injection.py +0 -0
  169. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_losses_breakdown.py +0 -0
  170. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_losses_q_convention.py +0 -0
  171. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_losses_trafo_classification.py +0 -0
  172. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_materialize_kernel.py +0 -0
  173. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_measurement_filter_kernels.py +0 -0
  174. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_measurement_ids.py +0 -0
  175. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_mirror_voltage_kernel.py +0 -0
  176. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_near_critical_ranking.py +0 -0
  177. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_neutral_wording.py +0 -0
  178. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_noise_model.py +0 -0
  179. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_normalized_residuals.py +0 -0
  180. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_numpy_only_runtime.py +0 -0
  181. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_observability.py +0 -0
  182. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_on_line_kernel.py +0 -0
  183. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_perf_synthetic_network.py +0 -0
  184. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_pipeline.py +0 -0
  185. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_pipeline_config_validation.py +0 -0
  186. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_pipeline_idempotent.py +0 -0
  187. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_pipeline_invariant_errors.py +0 -0
  188. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_pipeline_step_order.py +0 -0
  189. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_pipeline_warm_start.py +0 -0
  190. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_pipeline_working_layer.py +0 -0
  191. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_post_processing.py +0 -0
  192. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_preparation_steps.py +0 -0
  193. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_prepare_network.py +0 -0
  194. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_pseudo_unknown_load.py +0 -0
  195. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_pseudo_v_unobservable.py +0 -0
  196. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_quality_summary_paths.py +0 -0
  197. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_reconcile_balance.py +0 -0
  198. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_refine_merge.py +0 -0
  199. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_resolve_pass.py +0 -0
  200. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_result_quality_summary.py +0 -0
  201. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_rn_selected_inversion.py +0 -0
  202. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_serialize_npz.py +0 -0
  203. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_shunt_sanity.py +0 -0
  204. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_solve_status.py +0 -0
  205. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_state.py +0 -0
  206. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_state_range.py +0 -0
  207. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_telemetry_apply_kernel.py +0 -0
  208. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_telemetry_bounds.py +0 -0
  209. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_telemetry_physics_kernels.py +0 -0
  210. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_tm_code_quality.py +0 -0
  211. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_topology_kernels.py +0 -0
  212. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_topology_port.py +0 -0
  213. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_transformer_from_shunt.py +0 -0
  214. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_units.py +0 -0
  215. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_units_array_kernels.py +0 -0
  216. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_v_mirror.py +0 -0
  217. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_v_refine.py +0 -0
  218. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_variance_charging.py +0 -0
  219. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_version.py +0 -0
  220. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_voltage_calibration_gen_nodes.py +0 -0
  221. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_voltage_range_filter_fallback.py +0 -0
  222. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_wls.py +0 -0
  223. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_wls_robustness.py +0 -0
  224. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_working_container.py +0 -0
  225. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_ybus.py +0 -0
  226. {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_z_vector.py +0 -0
  227. {gridstate-2.3.0 → gridstate-2.4.1}/tools/gen_ieee_fixtures.py +0 -0
  228. {gridstate-2.3.0 → gridstate-2.4.1}/tools/golden.py +0 -0
@@ -9,6 +9,49 @@ 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.4.1 (2026-10-18)
13
+
14
+ ### Behavior changes
15
+
16
+ - `ipm_pd`: the proximal term on the box-variable diagonal adapts to the step
17
+ length: ×10 after a step αp < 0.1, up to `box_prox_max` (new, default 1e-2 of
18
+ the median weight), ÷10 after αp ≥ 0.5, down to `box_prox`. It damps the step
19
+ only; the fixed point is unchanged. Estimates on the test models move by
20
+ ≤ 1.6e-8 p.u. V with unchanged iteration counts.
21
+ - `ipm_pd`: a second stopping test, `fit_tol` (new, default 1e-2): the step moves
22
+ no data row by more than that fraction of its sigma (the μ, feasibility and
23
+ equality tests still apply).
24
+
25
+ ### Fix
26
+
27
+ - `ipm_pd` no longer runs into its iteration limit and the primal fallback when
28
+ box variables are seen only through near-zero-weight rows (loads with
29
+ effectively free injections): such directions have barrier curvature only,
30
+ their steps dwarf the box and the boundary rule cut every step to a tiny αp.
31
+ A state the data do not determine no longer keeps the V/δ step test from
32
+ passing (it could move, or cycle, without changing any estimate).
33
+
34
+ ## v2.4.0 (2026-10-17)
35
+
36
+ ### Behavior changes
37
+
38
+ - Breaker branches marked by the input format (R = 0, X = 1 Ohm) get
39
+ X = 110·min_x·Vn Ohm with min_x = 4e-6 (≈ 0.044/Vn p.u.) instead of a fixed
40
+ 1e-3 p.u.; `breaker_x_pu` (new) sets a fixed p.u. value instead. Estimates
41
+ move on models carrying the marker (≤ 4.5e-3 p.u. V with `ipm_pd` on the
42
+ test models) (#115)
43
+ - A solved state with V/Vnom below `implausible_v_ratio` (new, default 0.3)
44
+ at any node is marked not converged whatever the solver reports, with a
45
+ WARNING listing the nodes and hints; a non-converged WLS solve logs its
46
+ largest normalized residuals (#114, #115)
47
+
48
+ ### Feat
49
+
50
+ - Piecewise static load characteristics: optional input table
51
+ `load_characteristic_segments` (contract 3.2.0) with per-segment
52
+ coefficients up to V⁴ for P and Q; `apply_load_characteristic` uses it in
53
+ place of the quadratic row; without the table nothing changes (#113, #115)
54
+
12
55
  ## v2.3.0 (2026-10-17)
13
56
 
14
57
  ### Behavior changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gridstate
3
- Version: 2.3.0
3
+ Version: 2.4.1
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.3.0'
22
- __version_tuple__ = version_tuple = (2, 3, 0)
21
+ __version__ = version = '2.4.1'
22
+ __version_tuple__ = version_tuple = (2, 4, 1)
23
23
 
24
- __commit_id__ = commit_id = 'gef79dd7f7'
24
+ __commit_id__ = commit_id = 'g640125491'
@@ -50,6 +50,19 @@ corrector's second-order term may spoil descent, and then the centering
50
50
  direction is taken instead (one more solve on the same factor). The dual step
51
51
  keeps its own fraction-to-boundary length.
52
52
 
53
+ Adaptive box regularisation. A box variable seen only through rows of
54
+ negligible weight (a load whose injection row is a near-zero-weight pseudo
55
+ measurement) or through a combination the data do not resolve (a reactive flow
56
+ circulating between two such loads) has no curvature but the barrier's, and
57
+ the Newton step along it is orders of magnitude longer than the box. The
58
+ fraction-to-boundary rule then cuts every step to ``αp ≪ 1``, the
59
+ complementarity stops decreasing and the iteration crawls to its limit. The
60
+ proximal term on the box diagonal is therefore raised tenfold after every
61
+ step shorter than 0.1 (up to ``box_prox_max``) and lowered tenfold after
62
+ every step of 0.5 or longer (down to ``box_prox``). The term damps the step
63
+ only, so the fixed point is unchanged; a well-scaled problem takes long steps
64
+ and keeps the base term.
65
+
53
66
  Start on the central path: ``s`` = half-range of a two-sided constraint (the
54
67
  distance for a one-sided one), ``y = μ0/s``; the initial point need not satisfy
55
68
  ``g − l = s``.
@@ -96,6 +109,14 @@ def _blocking_constraint(
96
109
  return ku, False
97
110
 
98
111
 
112
+ # Adaptive box regularisation (see the module docstring): a primal step shorter
113
+ # than _PROX_SHORT raises the term by _PROX_GROW, one of _PROX_LONG or longer
114
+ # lowers it by the same factor.
115
+ _PROX_SHORT = 0.1
116
+ _PROX_LONG = 0.5
117
+ _PROX_GROW = 10.0
118
+
119
+
99
120
  Direction = tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]
100
121
 
101
122
 
@@ -212,6 +233,7 @@ def solve_ipm_pd(
212
233
  armijo_max: int = 10,
213
234
  max_search_failures: int = 20,
214
235
  step_tol: float = 1e-4,
236
+ fit_tol: float = 1e-2,
215
237
  feas_tol: float = 1e-6,
216
238
  max_iter: int = 100,
217
239
  box_eps: float = 1e-3,
@@ -225,6 +247,7 @@ def solve_ipm_pd(
225
247
  kkt_solver: KKTSolver | None = None,
226
248
  eq_prox: float = 1e-9,
227
249
  box_prox: float = 1e-6,
250
+ box_prox_max: float = 1e-2,
228
251
  ) -> IPMResult:
229
252
  """Mehrotra predictor-corrector IPM (see the module docstring).
230
253
 
@@ -251,6 +274,11 @@ def solve_ipm_pd(
251
274
  outside ``box_idx`` (V/δ) is below it. Box variables may still move
252
275
  along directions the data do not see (the common level of a node's
253
276
  generation and load), which change neither the state nor the residuals.
277
+ fit_tol: alternatively, stop once that step moves no data row by more
278
+ than ``fit_tol`` of its sigma (``max √w·|H·αp·Δx|``). A state the data
279
+ do not determine (a node behind loads whose injection rows carry
280
+ near-zero weight) can keep moving, or cycle, without changing any
281
+ estimated measurement; the fit has converged all the same.
254
282
  feas_tol: admissible primal infeasibility of the inequalities (p.u.).
255
283
  max_iter: iteration limit (one factorization each).
256
284
  box_eps: margin from the bounds when projecting ``x_init``.
@@ -271,6 +299,9 @@ def solve_ipm_pd(
271
299
  has no curvature along that common level but the barrier's, so a
272
300
  cold start produces huge box steps cut to tiny ``αp`` by the boundary
273
301
  rule; the term bounds those steps without moving the fixed point.
302
+ box_prox_max: ceiling of the adaptive box term (same units as
303
+ ``box_prox``, see the module docstring); a value not above
304
+ ``box_prox`` keeps the term fixed.
274
305
 
275
306
  Returns:
276
307
  :class:`~gridstate.algorithms.ipm.IPMResult`; ``status`` is ``"kkt"`` when
@@ -325,10 +356,9 @@ def solve_ipm_pd(
325
356
  is_eq = np.zeros(m, dtype=bool)
326
357
  is_eq[eq_rows] = True
327
358
  rho = w_base[eq_rows]
328
- prox = max(
329
- eq_prox * float(np.max(rho)) if rho.size else 0.0,
330
- box_prox * float(np.median(w_base[~is_eq])) if (~is_eq).any() else 0.0,
331
- )
359
+ eq_prox_abs = eq_prox * float(np.max(rho)) if rho.size else 0.0
360
+ w_median = float(np.median(w_base[~is_eq])) if (~is_eq).any() else 0.0
361
+ box_scale = box_prox
332
362
 
333
363
  huber_mask_arr = (
334
364
  np.asarray(huber_mask, dtype=bool) if huber_mask is not None else np.zeros(m, dtype=bool)
@@ -378,6 +408,7 @@ def solve_ipm_pd(
378
408
  w = w_base
379
409
  mu = mu0 if n_pairs else 0.0
380
410
  step_inf = float("inf")
411
+ fit_inf = float("inf")
381
412
  eq_inf = 0.0
382
413
  grad_inf = float("inf")
383
414
  status = "stalled"
@@ -399,7 +430,7 @@ def solve_ipm_pd(
399
430
  and mu <= mu_min
400
431
  and feas_inf <= feas_tol
401
432
  and bool(np.all(np.abs(c) <= eq_tol_arr))
402
- and step_inf <= step_tol
433
+ and (step_inf <= step_tol or fit_inf <= fit_tol)
403
434
  ):
404
435
  status = "kkt"
405
436
  break
@@ -413,7 +444,7 @@ def solve_ipm_pd(
413
444
  w_mat[eq_rows] = rho
414
445
  w_mat[mb_rows] += d_k[nb:]
415
446
  diag = np.zeros(n)
416
- diag[box_idx] = d_k[:nb] + prox
447
+ diag[box_idx] = d_k[:nb] + max(eq_prox_abs, box_scale * w_median)
417
448
  # Stationarity residual of the current point (for reporting).
418
449
  y_dual = np.zeros(nb + mb_rows.size)
419
450
  y_dual[il] += y_l
@@ -490,7 +521,8 @@ def solve_ipm_pd(
490
521
  il=il,
491
522
  iu=iu,
492
523
  )
493
- slope = merit.slope(lin, H @ dx, ds_l, ds_u)
524
+ hdx = H @ dx
525
+ slope = merit.slope(lin, hdx, ds_l, ds_u)
494
526
  if slope >= 0.0 and n_pairs:
495
527
  # The corrector's second-order term can spoil descent; the pure
496
528
  # centering direction is a Newton step on the merit and descends
@@ -498,9 +530,11 @@ def solve_ipm_pd(
498
530
  dx_c, ds_l_c, ds_u_c, dy_l_c, dy_u_c, dlam_c = lin.direction(
499
531
  target - s_l * y_l, target - s_u * y_u
500
532
  )
501
- slope_c = merit.slope(lin, H @ dx_c, ds_l_c, ds_u_c)
533
+ hdx_c = H @ dx_c
534
+ slope_c = merit.slope(lin, hdx_c, ds_l_c, ds_u_c)
502
535
  if slope_c < 0.0 and np.all(np.isfinite(dx_c)):
503
536
  dx, ds_l, ds_u, dy_l, dy_u, dlam = dx_c, ds_l_c, ds_u_c, dy_l_c, dy_u_c, dlam_c
537
+ hdx = hdx_c
504
538
  slope = slope_c
505
539
  n_centering += 1
506
540
 
@@ -521,6 +555,7 @@ def solve_ipm_pd(
521
555
  a_p = tr_radius / free_inf
522
556
  # The stopping test measures the Newton step, not how far the search cut it.
523
557
  step_inf = a_p * free_inf
558
+ fit_inf = a_p * float(np.max(np.sqrt(w_data) * np.abs(hdx))) if m else 0.0
524
559
  a_full = a_p
525
560
  r_new = None
526
561
  # A direction that does not descend (inconsistent slacks of a nonlinear
@@ -563,6 +598,11 @@ def solve_ipm_pd(
563
598
  eq_inf,
564
599
  feas_inf,
565
600
  )
601
+ if box_prox_max > box_prox:
602
+ if a_p < _PROX_SHORT:
603
+ box_scale = min(box_scale * _PROX_GROW, box_prox_max)
604
+ elif a_p >= _PROX_LONG:
605
+ box_scale = max(box_scale / _PROX_GROW, box_prox)
566
606
  x = x + a_p * dx
567
607
  s_l = s_l + a_p * ds_l
568
608
  s_u = s_u + a_p * ds_u
@@ -48,6 +48,7 @@ from gridstate.quality_summary import (
48
48
  top_worst_residuals,
49
49
  )
50
50
  from gridstate.result import MeasurementNormalizedResiduals, SEResult, extract_output_tables
51
+ from gridstate.solution_hints import low_voltage_hints, low_voltage_nodes, residual_hints
51
52
  from gridstate.state import (
52
53
  InitialPoint,
53
54
  StateLayout,
@@ -100,6 +101,7 @@ def estimate(
100
101
  quality_summary_rn_refine_below: float = SELINV_REFINE_BELOW,
101
102
  quality_summary_near_critical: float = NEAR_CRITICAL_THRESHOLD,
102
103
  success_policy: SuccessPolicy = "legacy",
104
+ implausible_v_ratio: float = 0.3,
103
105
  **ipm_kwargs: Any,
104
106
  ) -> SEResult:
105
107
  """Run state estimation for a model and telemetry.
@@ -181,6 +183,13 @@ def estimate(
181
183
  :func:`gridstate.algorithms.status.success_from_status`
182
184
  (``kkt`` or ``mu_schedule_done`` only). ``solve_status`` and
183
185
  ``objective_wls`` are reported under both policies.
186
+ implausible_v_ratio: a solved state with ``V/Vnom`` below it at any
187
+ node is not a usable estimate whatever the solver reports (e.g. the
188
+ low-voltage root at a node whose V is barely observable):
189
+ ``success`` becomes ``False``, the message names the nodes and a
190
+ WARNING lists hints (:mod:`gridstate.solution_hints`). ``0``
191
+ disables the check. A WLS solve that does not converge logs its
192
+ largest normalized residuals.
184
193
  **ipm_kwargs: IPM-mode knobs, ignored for WLS. ``trust_region_scope``
185
194
  (``"state"`` default | ``"all"``) selects the coordinates the IPM
186
195
  trust region is measured on (see ``_run_ipm``); everything else is
@@ -310,6 +319,30 @@ def estimate(
310
319
  )
311
320
  if success_policy == "status" and not success:
312
321
  message = f"solve status {solve_status.value}: {message}"
322
+ low_v = low_voltage_nodes(v_pu, implausible_v_ratio)
323
+ if low_v.size:
324
+ ids = network_pu.bus_ids[low_v][:5].tolist()
325
+ more = ", ..." if low_v.size > 5 else ""
326
+ note = (
327
+ f"implausible solution: V/Vnom < {implausible_v_ratio:g} at "
328
+ f"{low_v.size} node(s) {ids}{more}"
329
+ )
330
+ success = False
331
+ message = f"{note}; {message}" if message else note
332
+ hints = low_voltage_hints(
333
+ low_v, v_pu=v_pu, network_pu=network_pu, meas_index=meas_index, z=z
334
+ )
335
+ logger.warning("%s %s:\n%s", algorithm, note, "\n".join(hints))
336
+ elif not success and algorithm == "wls" and z.shape[0]:
337
+ algebra = BaseAlgebra(ybus, yf, yt, meas_index, layout, network_pu)
338
+ hints = residual_hints(
339
+ z - algebra.evaluate_h(v_pu, delta),
340
+ np.sqrt(floored_sigma2(r_matrix.diagonal())),
341
+ meas_index=meas_index,
342
+ network_pu=network_pu,
343
+ )
344
+ if hints:
345
+ logger.warning("wls did not converge (%s):\n%s", message, "\n".join(hints))
313
346
  write_results_to_model(model, v_pu, delta, network_pu, yf=yf, yt=yt, ybus=ybus)
314
347
  # 7. Post-processing of measurements: estimated_si/value/residual.
315
348
  write_measurement_estimates(
@@ -26,6 +26,7 @@ from gridstate.contract.tables import (
26
26
  BRANCHES,
27
27
  BRANCHES_OUTPUT,
28
28
  GENERATORS,
29
+ LOAD_CHARACTERISTIC_SEGMENTS,
29
30
  LOAD_CHARACTERISTICS,
30
31
  MEASUREMENTS,
31
32
  MEASUREMENTS_OUTPUT,
@@ -70,6 +71,7 @@ __all__ = [
70
71
  "CONTRACT_VERSION_KEY",
71
72
  "GENERATORS",
72
73
  "LOAD_CHARACTERISTICS",
74
+ "LOAD_CHARACTERISTIC_SEGMENTS",
73
75
  "MEASUREMENTS",
74
76
  "MEASUREMENTS_OUTPUT",
75
77
  "NODES",
@@ -670,6 +670,41 @@ LOAD_CHARACTERISTICS = TableSchema(
670
670
  ),
671
671
  )
672
672
 
673
+ # Piecewise segments of the static load characteristics, one row per segment.
674
+ # Optional: a (characteristic, quantity) pair with segments replaces its quadratic
675
+ # coefficients of LOAD_CHARACTERISTICS; a pair without segments keeps them.
676
+ LOAD_CHARACTERISTIC_SEGMENTS = TableSchema(
677
+ name="load_characteristic_segments",
678
+ key=("load_model_id", "quantity", "v_lo"),
679
+ doc=(
680
+ "Piecewise P(V)/Q(V) load characteristics: segment k of a (load_model_id, quantity) "
681
+ "pair applies for v_lo[k] < V ≤ v_lo[k+1] (V = |V|/Vnom; a value on a boundary "
682
+ "belongs to the lower segment); the first segment extends to V = 0 whatever its "
683
+ "v_lo. Factor = c0 + c1·V + c2·V² + c3·V³ + c4·V⁴ (1.0 = nominal)."
684
+ ),
685
+ columns=(
686
+ ColumnSpec(
687
+ "load_model_id",
688
+ "i4",
689
+ Role.KEY,
690
+ doc="Characteristic (load_characteristics.id, 0-based); must exist there.",
691
+ ),
692
+ ColumnSpec("quantity", "i1", Role.KEY, doc="0 = P, 1 = Q."),
693
+ ColumnSpec(
694
+ "v_lo",
695
+ "f8",
696
+ Role.KEY,
697
+ doc="Lower bound of |V|/Vnom; strictly increasing within a pair; "
698
+ "treated as -inf for the first segment.",
699
+ ),
700
+ ColumnSpec("c0", "f8", Role.INPUT, doc="Coefficient of V⁰."),
701
+ ColumnSpec("c1", "f8", Role.INPUT, doc="Coefficient of V¹."),
702
+ ColumnSpec("c2", "f8", Role.INPUT, doc="Coefficient of V²."),
703
+ ColumnSpec("c3", "f8", Role.INPUT, doc="Coefficient of V³."),
704
+ ColumnSpec("c4", "f8", Role.INPUT, doc="Coefficient of V⁴."),
705
+ ),
706
+ )
707
+
673
708
  # Shunt devices (reactors/capacitor banks), per device. The core aggregates the active ones
674
709
  # into node.shunt_g/b for the Y-bus (a numeric step, replacing apply_reactors_to_node_shunt) and
675
710
  # in the future switches steps (group_id/step — reserved).
@@ -708,7 +743,8 @@ SHUNTS = TableSchema(
708
743
  class SEInputSchema:
709
744
  """Input contract schema: a set of tables (roles KEY/INPUT/WORKING).
710
745
 
711
- The domain tables ``tap_steps``/``load_characteristics``/``shunts`` are
746
+ The domain tables ``tap_steps``/``load_characteristics``/``shunts``/
747
+ ``load_characteristic_segments`` are
712
748
  format-agnostic numeric inputs (tap changer position selection, unit conversion,
713
749
  aggregation of shunts are done in an external data source). Input-only
714
750
  (roles KEY/INPUT).
@@ -721,6 +757,7 @@ class SEInputSchema:
721
757
  tap_steps: TableSchema
722
758
  load_characteristics: TableSchema
723
759
  shunts: TableSchema
760
+ load_characteristic_segments: TableSchema
724
761
 
725
762
  def tables(self) -> tuple[TableSchema, ...]:
726
763
  """All tables in the order of field declaration (single source — the dataclass)."""
@@ -748,6 +785,7 @@ SE_INPUT = SEInputSchema(
748
785
  tap_steps=TAP_STEPS,
749
786
  load_characteristics=LOAD_CHARACTERISTICS,
750
787
  shunts=SHUNTS,
788
+ load_characteristic_segments=LOAD_CHARACTERISTIC_SEGMENTS,
751
789
  )
752
790
 
753
791
  SE_OUTPUT = SEOutputSchema(
@@ -18,6 +18,7 @@ from typing import Any
18
18
 
19
19
  from gridstate.contract.tables import SE_INPUT, Role, SEInputSchema
20
20
  from gridstate.contract.version import CONTRACT_VERSION_KEY, check_compatibility
21
+ from gridstate.load_characteristics import segment_problems
21
22
 
22
23
 
23
24
  @dataclass(frozen=True)
@@ -25,7 +26,7 @@ class ValidationIssue:
25
26
  """A single input validation problem."""
26
27
 
27
28
  table: str
28
- kind: str # "missing_column" | "missing_table" | "version_incompatible"
29
+ kind: str # "missing_column" | "missing_table" | "version_incompatible" | "invalid_rows"
29
30
  detail: str
30
31
  column: str | None = None
31
32
 
@@ -75,6 +76,20 @@ def _available_columns(source: Any, table_name: str) -> set[str] | None:
75
76
  return set(names or ())
76
77
 
77
78
 
79
+ #: Input tables a source may omit (added in a minor contract version).
80
+ _OPTIONAL_TABLES = frozenset({"load_characteristic_segments"})
81
+
82
+
83
+ def _table_array(source: Any, table_name: str) -> Any:
84
+ """The structured array of table ``table_name`` (``None`` if absent)."""
85
+ if isinstance(source, Mapping):
86
+ return source.get(table_name)
87
+ collection = getattr(source, table_name, None)
88
+ if collection is None or not hasattr(collection, "to_numpy"):
89
+ return None
90
+ return collection.to_numpy()
91
+
92
+
78
93
  def _data_version(source: Any, explicit: str | None) -> str | None:
79
94
  """Data contract version: explicit > ``metadata[contract_version]`` > None."""
80
95
  if explicit is not None:
@@ -128,6 +143,8 @@ def validate_input(
128
143
  for table in schema.tables():
129
144
  available = _available_columns(source, table.name)
130
145
  if available is None:
146
+ if table.name in _OPTIONAL_TABLES:
147
+ continue
131
148
  report.add(
132
149
  ValidationIssue(table.name, "missing_table", "table is missing from the input")
133
150
  )
@@ -143,6 +160,13 @@ def validate_input(
143
160
  )
144
161
  )
145
162
 
163
+ # --- row-level rules of the piecewise load characteristics ---
164
+ segments = _table_array(source, "load_characteristic_segments")
165
+ if segments is not None and len(segments) > 0:
166
+ lc = _table_array(source, "load_characteristics")
167
+ for problem in segment_problems(segments, 0 if lc is None else len(lc)):
168
+ report.add(ValidationIssue("load_characteristic_segments", "invalid_rows", problem))
169
+
146
170
  if strict:
147
171
  report.raise_if_invalid()
148
172
  return report
@@ -48,7 +48,9 @@ from dataclasses import dataclass
48
48
  # explicit legacy path ``load_se_input_npz(path, trusted=True)``.
49
49
  # 3.1.0 (MINOR): the optional derived table ``telemetry_bounds``
50
50
  # (``DerivedInputs.telemetry_bounds``).
51
- CONTRACT_VERSION = "3.1.0"
51
+ # 3.2.0 (MINOR): the optional input table ``load_characteristic_segments``
52
+ # (piecewise load characteristics).
53
+ CONTRACT_VERSION = "3.2.0"
52
54
 
53
55
  # Major of the data the explicit legacy path of the ``.npz`` loader still reads
54
56
  # (``load_se_input_npz(path, trusted=True)``).
@@ -25,34 +25,16 @@ import numpy as np
25
25
 
26
26
  from gridstate.algorithms.ipm import IPMResult
27
27
  from gridstate.preprocessing.ipm_setup import IPMSetup
28
+ from gridstate.solution_hints import KIND_NAMES, describe_row, residual_hints
28
29
  from gridstate.units import BASE_MVA, NetworkPU
29
30
  from gridstate.z_vector import (
30
31
  KIND_BOX_PRIOR_PGEN,
31
32
  KIND_BOX_PRIOR_PNAG,
32
33
  KIND_BOX_PRIOR_QGEN,
33
34
  KIND_BOX_PRIOR_QNAG,
34
- KIND_NODE_BALANCE_P,
35
- KIND_NODE_BALANCE_Q,
36
35
  )
37
36
 
38
37
 
39
- #: Row kinds by name (``MeasurementType`` plus the rows the IPM setup appends).
40
- _KIND_NAMES = {
41
- 0: "P flow",
42
- 1: "Q flow",
43
- 2: "V",
44
- 3: "I",
45
- 4: "P injection",
46
- 5: "Q injection",
47
- KIND_NODE_BALANCE_P: "P balance",
48
- KIND_NODE_BALANCE_Q: "Q balance",
49
- KIND_BOX_PRIOR_PGEN: "Pgen",
50
- KIND_BOX_PRIOR_QGEN: "Qgen",
51
- KIND_BOX_PRIOR_PNAG: "Pload",
52
- KIND_BOX_PRIOR_QNAG: "Qload",
53
- }
54
-
55
-
56
38
  def _box_sections(setup: IPMSetup) -> list[tuple[int, int, np.ndarray]]:
57
39
  lay = setup.layout
58
40
  return [
@@ -120,7 +102,7 @@ class _Context:
120
102
  for bkind, _off, _pos in _box_sections(self.setup):
121
103
  rows = self.rows(pos, bkind)
122
104
  if rows.size and not self.is_role[rows].any():
123
- free_parts.append(_KIND_NAMES[bkind])
105
+ free_parts.append(KIND_NAMES[bkind])
124
106
  label = f"node {node} {var} ({n_nbr} neighbour node{'s' if n_nbr != 1 else ''})"
125
107
  if n_nbr <= 1 and free_parts:
126
108
  hint = (
@@ -135,7 +117,7 @@ class _Context:
135
117
  lo = float(self.setup.box_lo[k]) * BASE_MVA if k is not None else float("nan")
136
118
  hi = float(self.setup.box_hi[k]) * BASE_MVA if k is not None else float("nan")
137
119
  measured, sigma = self.box_variable_state(pos, kind)
138
- name = _KIND_NAMES[kind]
120
+ name = KIND_NAMES[kind]
139
121
  label = f"node {node} {name} range [{lo:.1f}, {hi:.1f}]"
140
122
  if measured:
141
123
  label += ", measured"
@@ -151,17 +133,7 @@ class _Context:
151
133
 
152
134
  def describe_row(self, row: int) -> str:
153
135
  mi = self.setup.meas_index
154
- kind = int(mi.kind[row])
155
- name = _KIND_NAMES.get(kind, f"kind {kind}")
156
- okind, opos = int(mi.object_kind[row]), int(mi.object_pos[row])
157
- if okind == 1:
158
- side = "from" if int(mi.branch_side[row]) == 0 else "to"
159
- obj = f"branch {int(self.net.branch_ids[opos])} ({side})"
160
- else:
161
- obj = f"node {self.node_id(opos)}"
162
- mid = int(mi.meas_id[row])
163
- what = "part measurement" if self.is_role[row] else ("measurement" if mid > 0 else "row")
164
- return f"{name} {what} {mid} at {obj}" if mid > 0 else f"{name} {what} at {obj}"
136
+ return describe_row(mi, self.net, row, part=bool(self.is_role[row]))
165
137
 
166
138
 
167
139
  def _top(counts: Mapping[int, int], n: int, floor: float) -> list[tuple[int, int]]:
@@ -240,17 +212,18 @@ def ipm_convergence_hints(
240
212
  lines.extend(f" {label}" for _, label in weak[:top])
241
213
 
242
214
  sigma = ctx.sigma
243
- rn = np.abs(residual) / np.where(sigma > 0, sigma, np.inf)
244
- if setup.balance_tol is not None:
245
- # Equality rows: their σ is a penalty, not a measurement error.
246
- rn[setup.balance_rows] = 0.0
247
- order = np.argsort(-rn)[:top]
248
- if order.size and rn[order[0]] > 0:
249
- lines.append("largest normalized residuals |r|/σ:")
250
- for i in order.tolist():
251
- kind = int(setup.meas_index.kind[i])
252
- r_txt = f"{residual[i]:.4f} p.u." if kind in (2, 3) else f"{residual[i] * BASE_MVA:.2f}"
253
- lines.append(f" {ctx.describe_row(i)}: {rn[i]:.1f}σ (r = {r_txt})")
215
+ lines.extend(
216
+ residual_hints(
217
+ residual,
218
+ sigma,
219
+ meas_index=setup.meas_index,
220
+ network_pu=network_pu,
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,
223
+ part_rows=setup.role_rows,
224
+ top=top,
225
+ )
226
+ )
254
227
 
255
228
  outside: list[tuple[float, str]] = []
256
229
  for row in setup.role_rows.tolist():
@@ -0,0 +1,99 @@
1
+ """Piecewise static load characteristics (``load_characteristic_segments``).
2
+
3
+ A characteristic may define, separately for P and Q (``quantity`` 0 / 1), a set
4
+ of segments: segment ``k`` applies for ``v_lo[k] < V ≤ v_lo[k+1]``
5
+ (``V = |V|/Vnom``; a value on a boundary belongs to the lower segment), the
6
+ first one extends to ``V = 0`` whatever its ``v_lo``. The load factor on a
7
+ segment is ``c0 + c1·V + c2·V² + c3·V³ + c4·V⁴`` (1.0 = nominal).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from itertools import pairwise
13
+
14
+ import numpy as np
15
+
16
+
17
+ #: ``quantity`` codes of ``load_characteristic_segments``.
18
+ QUANTITY_P = 0
19
+ QUANTITY_Q = 1
20
+
21
+ #: ``(load_model_id, quantity) → (boundaries, coefficients)``: the upper
22
+ #: boundaries of all segments but the last (``v_lo[1:]``, increasing) and the
23
+ #: ``(n_segments, 5)`` coefficient matrix.
24
+ SegmentPlan = dict[tuple[int, int], tuple[np.ndarray, np.ndarray]]
25
+
26
+
27
+ def segment_problems(segments: np.ndarray, n_models: int) -> list[str]:
28
+ """Contract violations of a ``load_characteristic_segments`` table (empty = valid).
29
+
30
+ ``n_models`` is the number of rows of ``load_characteristics`` (ids are
31
+ 0-based positions).
32
+ """
33
+ problems: list[str] = []
34
+ if len(segments) == 0:
35
+ return problems
36
+ ids = np.asarray(segments["load_model_id"], dtype=np.int64)
37
+ qty = np.asarray(segments["quantity"], dtype=np.int64)
38
+ v_lo = np.asarray(segments["v_lo"], dtype=np.float64)
39
+ unknown = sorted(set(ids[(ids < 0) | (ids >= n_models)].tolist()))
40
+ if unknown:
41
+ problems.append(f"load_model_id not in load_characteristics: {unknown}")
42
+ bad_q = sorted(set(qty[(qty != QUANTITY_P) & (qty != QUANTITY_Q)].tolist()))
43
+ if bad_q:
44
+ problems.append(f"quantity must be 0 (P) or 1 (Q), got {bad_q}")
45
+ coeffs = np.column_stack([np.asarray(segments[f"c{k}"], dtype=np.float64) for k in range(5)])
46
+ if not np.all(np.isfinite(coeffs)):
47
+ problems.append("non-finite coefficients")
48
+ order = np.lexsort((v_lo, qty, ids))
49
+ for a, b in pairwise(order):
50
+ # The first segment's v_lo is ignored, so only later bounds must be
51
+ # finite; equal bounds make an empty segment.
52
+ same_pair = ids[a] == ids[b] and qty[a] == qty[b]
53
+ if same_pair and (not np.isfinite(v_lo[b]) or v_lo[b] <= v_lo[a]):
54
+ problems.append(
55
+ f"v_lo must be finite and strictly increasing within "
56
+ f"(load_model_id={ids[a]}, quantity={qty[a]})"
57
+ )
58
+ return problems
59
+
60
+
61
+ def segment_plan(segments: np.ndarray, n_models: int) -> SegmentPlan:
62
+ """Group the segments by ``(load_model_id, quantity)``.
63
+
64
+ Raises:
65
+ ValueError: the table violates the contract (see :func:`segment_problems`).
66
+ """
67
+ problems = segment_problems(segments, n_models)
68
+ if problems:
69
+ raise ValueError("invalid load_characteristic_segments: " + "; ".join(problems))
70
+ plan: SegmentPlan = {}
71
+ if len(segments) == 0:
72
+ return plan
73
+ ids = np.asarray(segments["load_model_id"], dtype=np.int64)
74
+ qty = np.asarray(segments["quantity"], dtype=np.int64)
75
+ v_lo = np.asarray(segments["v_lo"], dtype=np.float64)
76
+ coeffs = np.column_stack([np.asarray(segments[f"c{k}"], dtype=np.float64) for k in range(5)])
77
+ order = np.lexsort((v_lo, qty, ids))
78
+ for key in sorted(set(zip(ids.tolist(), qty.tolist(), strict=True))):
79
+ rows = order[(ids[order] == key[0]) & (qty[order] == key[1])]
80
+ plan[key] = (v_lo[rows][1:].copy(), coeffs[rows].copy())
81
+ return plan
82
+
83
+
84
+ def segment_factor(bounds: np.ndarray, coeffs: np.ndarray, v: float) -> float:
85
+ """Load factor at ``v`` (``bounds`` / ``coeffs`` of one plan entry)."""
86
+ # Number of upper bounds strictly below v: on a boundary, the lower segment.
87
+ k = int(np.searchsorted(bounds, v, side="left"))
88
+ c = coeffs[k]
89
+ return float(c[0] + v * (c[1] + v * (c[2] + v * (c[3] + v * c[4]))))
90
+
91
+
92
+ __all__ = [
93
+ "QUANTITY_P",
94
+ "QUANTITY_Q",
95
+ "SegmentPlan",
96
+ "segment_factor",
97
+ "segment_plan",
98
+ "segment_problems",
99
+ ]