gridstate 2.0.0__tar.gz → 2.2.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 (222) hide show
  1. {gridstate-2.0.0 → gridstate-2.2.0}/CHANGELOG.md +50 -0
  2. {gridstate-2.0.0 → gridstate-2.2.0}/PKG-INFO +1 -1
  3. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/__init__.py +2 -0
  4. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/_version.py +3 -3
  5. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algorithms/ipm.py +70 -0
  6. gridstate-2.2.0/gridstate/algorithms/ipm_pd.py +456 -0
  7. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algorithms/kkt_solver.py +59 -18
  8. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/api.py +149 -43
  9. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/constants.py +11 -0
  10. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/contract/derived.py +9 -1
  11. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/contract/derived_tables.py +38 -0
  12. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/contract/version.py +3 -1
  13. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/pipeline/config.py +72 -4
  14. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/pipeline/passes.py +8 -2
  15. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/pipeline/preparation.py +8 -0
  16. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/post_processing.py +19 -0
  17. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/preprocessing/ipm_setup.py +321 -8
  18. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/result.py +33 -2
  19. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/apply_resolved.py +137 -26
  20. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/z_vector.py +47 -0
  21. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate.egg-info/PKG-INFO +1 -1
  22. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate.egg-info/SOURCES.txt +5 -0
  23. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate.egg-info/scm_file_list.json +5 -0
  24. gridstate-2.2.0/gridstate.egg-info/scm_version.json +8 -0
  25. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_derived_tables.py +6 -0
  26. gridstate-2.2.0/tests/test_ipm_balance_modes.py +177 -0
  27. gridstate-2.2.0/tests/test_ipm_injection_roles.py +275 -0
  28. gridstate-2.2.0/tests/test_ipm_pd.py +162 -0
  29. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_pipeline_config_validation.py +1 -1
  30. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_result_quality_summary.py +36 -0
  31. gridstate-2.2.0/tests/test_telemetry_bounds.py +86 -0
  32. gridstate-2.0.0/gridstate.egg-info/scm_version.json +0 -8
  33. {gridstate-2.0.0 → gridstate-2.2.0}/.github/workflows/ci.yml +0 -0
  34. {gridstate-2.0.0 → gridstate-2.2.0}/.github/workflows/release.yml +0 -0
  35. {gridstate-2.0.0 → gridstate-2.2.0}/.gitignore +0 -0
  36. {gridstate-2.0.0 → gridstate-2.2.0}/.pre-commit-config.yaml +0 -0
  37. {gridstate-2.0.0 → gridstate-2.2.0}/LICENSE +0 -0
  38. {gridstate-2.0.0 → gridstate-2.2.0}/Makefile +0 -0
  39. {gridstate-2.0.0 → gridstate-2.2.0}/README.md +0 -0
  40. {gridstate-2.0.0 → gridstate-2.2.0}/build_cython.py +0 -0
  41. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/__pyinstaller/__init__.py +0 -0
  42. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/__pyinstaller/hook-gridstate.py +0 -0
  43. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/_arrays.py +0 -0
  44. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/adapters/__init__.py +0 -0
  45. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/adapters/from_pandapower.py +0 -0
  46. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algebra/__init__.py +0 -0
  47. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algebra/_selinv_kernels.py +0 -0
  48. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algebra/base.py +0 -0
  49. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algebra/estimators.py +0 -0
  50. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algebra/jacobian_pattern.py +0 -0
  51. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algebra/normalized_residuals.py +0 -0
  52. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algorithms/__init__.py +0 -0
  53. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algorithms/_gram_numba.py +0 -0
  54. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algorithms/status.py +0 -0
  55. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algorithms/wls.py +0 -0
  56. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/bad_data_repass.py +0 -0
  57. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/bounds.py +0 -0
  58. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/contract/__init__.py +0 -0
  59. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/contract/_legacy.py +0 -0
  60. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/contract/containers.py +0 -0
  61. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/contract/serialize.py +0 -0
  62. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/contract/tables.py +0 -0
  63. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/contract/validate.py +0 -0
  64. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/losses.py +0 -0
  65. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/measurement_ids.py +0 -0
  66. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/pipeline/__init__.py +0 -0
  67. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/pipeline/context.py +0 -0
  68. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/pipeline/manifest.py +0 -0
  69. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/pipeline/orchestration.py +0 -0
  70. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/pipeline/runner.py +0 -0
  71. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/pipeline/steps.py +0 -0
  72. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/preprocessing/__init__.py +0 -0
  73. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/preprocessing/_scan.py +0 -0
  74. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/preprocessing/meas_rows.py +0 -0
  75. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/preprocessing/mirror_voltage.py +0 -0
  76. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/preprocessing/node_props.py +0 -0
  77. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/preprocessing/pseudo_measurements.py +0 -0
  78. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/py.typed +0 -0
  79. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/quality_summary.py +0 -0
  80. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/resolve_pass.py +0 -0
  81. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/runtime.py +0 -0
  82. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/shunt_sanity.py +0 -0
  83. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/state.py +0 -0
  84. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/__init__.py +0 -0
  85. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/_filters.py +0 -0
  86. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/generators.py +0 -0
  87. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/kinds.py +0 -0
  88. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/load_only_injection.py +0 -0
  89. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/loss_filter.py +0 -0
  90. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/measurements.py +0 -0
  91. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/noise.py +0 -0
  92. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/on_line.py +0 -0
  93. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/quality.py +0 -0
  94. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/rpn.py +0 -0
  95. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/shunts.py +0 -0
  96. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/sigma_floor.py +0 -0
  97. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/units.py +0 -0
  98. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/voltage_filter.py +0 -0
  99. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/voltage_nominal.py +0 -0
  100. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/topology.py +0 -0
  101. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/units.py +0 -0
  102. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/utils.py +0 -0
  103. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/v_mirror.py +0 -0
  104. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/v_refine.py +0 -0
  105. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/validation/__init__.py +0 -0
  106. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/validation/_diagnostics.py +0 -0
  107. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/validation/bad_data.py +0 -0
  108. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/validation/chi2_test.py +0 -0
  109. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/validation/observability.py +0 -0
  110. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/working.py +0 -0
  111. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/ybus.py +0 -0
  112. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate.egg-info/dependency_links.txt +0 -0
  113. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate.egg-info/entry_points.txt +0 -0
  114. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate.egg-info/requires.txt +0 -0
  115. {gridstate-2.0.0 → gridstate-2.2.0}/gridstate.egg-info/top_level.txt +0 -0
  116. {gridstate-2.0.0 → gridstate-2.2.0}/pyproject.toml +0 -0
  117. {gridstate-2.0.0 → gridstate-2.2.0}/setup.cfg +0 -0
  118. {gridstate-2.0.0 → gridstate-2.2.0}/tests/__init__.py +0 -0
  119. {gridstate-2.0.0 → gridstate-2.2.0}/tests/_golden.py +0 -0
  120. {gridstate-2.0.0 → gridstate-2.2.0}/tests/conftest.py +0 -0
  121. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_adapters_lazy_import.py +0 -0
  122. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_aggregate_generators.py +0 -0
  123. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_algebra_base.py +0 -0
  124. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_anti_overshoot.py +0 -0
  125. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_arrays_helpers.py +0 -0
  126. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_bad_data.py +0 -0
  127. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_bad_data_repass.py +0 -0
  128. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_bounds.py +0 -0
  129. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_boxed_balance.py +0 -0
  130. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_breaker_reactance.py +0 -0
  131. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_chain_mirror.py +0 -0
  132. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_contract.py +0 -0
  133. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_contract_edge_cases.py +0 -0
  134. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_contract_runtime.py +0 -0
  135. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case118.json +0 -0
  136. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case118.npz +0 -0
  137. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case118_degraded.json +0 -0
  138. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case118_degraded.npz +0 -0
  139. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case14.json +0 -0
  140. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case14.npz +0 -0
  141. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case14_degraded.json +0 -0
  142. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case14_degraded.npz +0 -0
  143. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case30.json +0 -0
  144. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case30.npz +0 -0
  145. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case30_degraded.json +0 -0
  146. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case30_degraded.npz +0 -0
  147. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/manifest.json +0 -0
  148. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/ieee/case118.npz +0 -0
  149. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/ieee/case118_truth.npz +0 -0
  150. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/ieee/case14.npz +0 -0
  151. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/ieee/case14_truth.npz +0 -0
  152. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/ieee/case30.npz +0 -0
  153. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/ieee/case30_truth.npz +0 -0
  154. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_enum_codes.py +0 -0
  155. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_flow_sigma_floor.py +0 -0
  156. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_from_pandapower.py +0 -0
  157. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_generator_shunt_kernels.py +0 -0
  158. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_golden.py +0 -0
  159. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_gram_numba.py +0 -0
  160. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_ieee_cases.py +0 -0
  161. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_ipm.py +0 -0
  162. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_ipm_integration.py +0 -0
  163. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_jacobian_pattern.py +0 -0
  164. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_kkt_solver.py +0 -0
  165. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_load_only_injection.py +0 -0
  166. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_losses_breakdown.py +0 -0
  167. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_losses_q_convention.py +0 -0
  168. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_losses_trafo_classification.py +0 -0
  169. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_materialize_kernel.py +0 -0
  170. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_measurement_filter_kernels.py +0 -0
  171. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_measurement_ids.py +0 -0
  172. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_mirror_voltage_kernel.py +0 -0
  173. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_near_critical_ranking.py +0 -0
  174. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_neutral_wording.py +0 -0
  175. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_noise_model.py +0 -0
  176. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_normalized_residuals.py +0 -0
  177. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_numpy_only_runtime.py +0 -0
  178. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_observability.py +0 -0
  179. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_on_line_kernel.py +0 -0
  180. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_perf_synthetic_network.py +0 -0
  181. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_pipeline.py +0 -0
  182. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_pipeline_idempotent.py +0 -0
  183. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_pipeline_invariant_errors.py +0 -0
  184. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_pipeline_step_order.py +0 -0
  185. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_pipeline_warm_start.py +0 -0
  186. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_pipeline_working_layer.py +0 -0
  187. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_post_processing.py +0 -0
  188. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_preparation_steps.py +0 -0
  189. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_prepare_network.py +0 -0
  190. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_pseudo_unknown_load.py +0 -0
  191. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_pseudo_v_unobservable.py +0 -0
  192. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_quality_summary_paths.py +0 -0
  193. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_reconcile_balance.py +0 -0
  194. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_refine_merge.py +0 -0
  195. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_resolve_pass.py +0 -0
  196. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_rn_selected_inversion.py +0 -0
  197. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_serialize_npz.py +0 -0
  198. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_shunt_sanity.py +0 -0
  199. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_solve_status.py +0 -0
  200. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_state.py +0 -0
  201. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_state_range.py +0 -0
  202. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_telemetry_apply_kernel.py +0 -0
  203. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_telemetry_physics_kernels.py +0 -0
  204. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_tm_code_quality.py +0 -0
  205. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_topology_kernels.py +0 -0
  206. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_topology_port.py +0 -0
  207. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_transformer_from_shunt.py +0 -0
  208. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_units.py +0 -0
  209. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_units_array_kernels.py +0 -0
  210. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_v_mirror.py +0 -0
  211. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_v_refine.py +0 -0
  212. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_variance_charging.py +0 -0
  213. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_version.py +0 -0
  214. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_voltage_calibration_gen_nodes.py +0 -0
  215. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_voltage_range_filter_fallback.py +0 -0
  216. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_wls.py +0 -0
  217. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_wls_robustness.py +0 -0
  218. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_working_container.py +0 -0
  219. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_ybus.py +0 -0
  220. {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_z_vector.py +0 -0
  221. {gridstate-2.0.0 → gridstate-2.2.0}/tools/gen_ieee_fixtures.py +0 -0
  222. {gridstate-2.0.0 → gridstate-2.2.0}/tools/golden.py +0 -0
@@ -9,6 +9,56 @@ 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.2.0 (2026-10-17)
13
+
14
+ All new behaviour is opt-in; with the defaults the results are unchanged.
15
+
16
+ ### Feat
17
+
18
+ - `algorithm="ipm_pd"`: Mehrotra predictor-corrector interior-point solver;
19
+ predictor and corrector share one factorization per iteration
20
+ (`KKTSolver.factor_normal`). About 5× less solver time than `ipm` on large
21
+ models, with closely matching estimates. A solve that does not reach its
22
+ KKT test is redone by the primal IPM from the same start (#103)
23
+ - `balance_mode="hard"` (`ipm_balance_mode`): node balance rows become
24
+ equality constraints in both IPM solvers (method of multipliers,
25
+ quasi-definite step) with P / Q tolerances `balance_tol_p_mw` /
26
+ `balance_tol_q_mvar`. Experimental: it does not converge on every real
27
+ model yet (#103)
28
+ - `fixed_injection_from_input` (`ipm_fixed_injection_from_input`): the input
29
+ injection of node parts without a box variable enters the balance as a
30
+ fixed term. Experimental (#103)
31
+ - `measurement_bounds` (`ipm_measurement_bounds`, `ipm_pd` only):
32
+ `min_value` / `max_value` bound the estimated value of any measurement as
33
+ an inequality (#103)
34
+ - `injection_roles` (`ipm_injection_roles`, `ipm` / `ipm_pd`): node injection
35
+ rows tagged `source_code` `"inj_gen"` / `"inj_load"`
36
+ (`SOURCE_INJ_GEN` / `SOURCE_INJ_LOAD`) measure the generation / load box
37
+ variable directly; the node box is narrowed by the measurement box, the
38
+ unmeasured part of a measured node gets a prior, and the net row stays only
39
+ where a measured part has no box variable. The telemetry kernel emits the
40
+ part rows when the mode is on; `release_load_only_injections` and
41
+ `widen_generation_only_injections` are skipped in it (#103)
42
+ - `DerivedInputs.telemetry_bounds` (derived table `telemetry_bounds`):
43
+ optional plausible ranges of telemetry values, written by the telemetry
44
+ kernel into `min_value` / `max_value` (summed over components for
45
+ injection rows, generation minus load for the net row). Data contract
46
+ 3.1.0, additive (#103)
47
+ - `ipm_balance_weight_factor` accepts values up to 1e6 (was 10) (#103)
48
+
49
+ ## v2.1.0 (2026-10-14)
50
+
51
+ ### Feat
52
+
53
+ - `SEResult.normalized_residuals` (`MeasurementNormalizedResiduals`, exported
54
+ from the package root): `r_N` and `Ω_ii / σ²_i` of every z-vector
55
+ measurement by measurement id, in z-vector order. The quality summary keeps
56
+ the vector it already computes; `worst_residuals` and
57
+ `near_critical_measurements` still rank and trim it. Rows outside the
58
+ residual scope (pseudo-measurements under `"real"`) hold `nan`, critical
59
+ measurements `inf`; `None` when the summary is not computed. Additive, no
60
+ data contract change (#101)
61
+
12
62
  ## v2.0.0 (2026-10-10)
13
63
 
14
64
  ### Breaking changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gridstate
3
- Version: 2.0.0
3
+ Version: 2.2.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
@@ -34,6 +34,7 @@ from gridstate.pipeline import (
34
34
  from gridstate.result import (
35
35
  Chi2Summary,
36
36
  ImbalanceRow,
37
+ MeasurementNormalizedResiduals,
37
38
  NearCriticalRow,
38
39
  ResidualRow,
39
40
  SEResult,
@@ -84,6 +85,7 @@ __all__ = [
84
85
  "Chi2Summary",
85
86
  "ImbalanceRow",
86
87
  "InitialPoint",
88
+ "MeasurementNormalizedResiduals",
87
89
  "NearCriticalRow",
88
90
  "NormalizedResidualReport",
89
91
  "ObservabilityReport",
@@ -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.0.0'
22
- __version_tuple__ = version_tuple = (2, 0, 0)
21
+ __version__ = version = '2.2.0'
22
+ __version_tuple__ = version_tuple = (2, 2, 0)
23
23
 
24
- __commit_id__ = commit_id = 'g145919a37'
24
+ __commit_id__ = commit_id = 'g06a6dcf24'
@@ -33,6 +33,12 @@ changing the sparse structure. The step is limited to ``α_max·(1−ε)`` so th
33
33
 
34
34
  Sanity property: with ``box_idx=[]`` the IPM is equivalent to Gauss-Newton WLS
35
35
  (the barrier is zero, the outer loop makes a single pass).
36
+
37
+ Equality rows (``eq_idx``, e.g. hard nodal balance) are handled by the method
38
+ of multipliers: such a row ``c = h − z = −r`` enters Φ as
39
+ ``½ρ (r − λ/ρ)²`` (``ρ`` — its ``r_inv_diag`` entry, ``λ`` — its multiplier),
40
+ and after every outer step ``λ ← λ + ρ·c``. At the fixed point ``c = 0``
41
+ exactly, whatever ``ρ``; the Newton systems stay the normal equations.
36
42
  """
37
43
 
38
44
  from __future__ import annotations
@@ -98,6 +104,8 @@ class IPMResult:
98
104
  message: str = ""
99
105
  status: str = "error"
100
106
  solve_status: SolveStatus = SolveStatus.ERROR
107
+ # max |c| over the equality rows at the final point (p.u.); nan without them.
108
+ eq_inf_final: float = float("nan")
101
109
 
102
110
 
103
111
  def _project_to_interior(
@@ -269,6 +277,10 @@ def solve_ipm(
269
277
  huber_adaptive_k: float = 6.0,
270
278
  huber_warmup_iters: int = 5,
271
279
  kkt_solver: KKTSolver | None = None,
280
+ eq_idx: np.ndarray | None = None,
281
+ eq_tol: np.ndarray | None = None,
282
+ eq_extra_outer: int = 10,
283
+ eq_prox: float = 1e-9,
272
284
  ) -> IPMResult:
273
285
  """Primal log-barrier IPM for box-constrained WLS.
274
286
 
@@ -331,6 +343,17 @@ def solve_ipm(
331
343
  symbolic factorization between iterations (see
332
344
  ``gridstate.algorithms.kkt_solver``). ``None`` — scipy spsolve
333
345
  (the former behaviour, bit for bit).
346
+ eq_idx: rows of ``r`` that are equality constraints ``h = z`` (method of
347
+ multipliers, see the module docstring). ``None`` — none.
348
+ eq_tol: admissible ``|c|`` per row of ``eq_idx`` (p.u.). Once the μ
349
+ schedule is done, up to ``eq_extra_outer`` further outer steps at
350
+ ``μ_min`` update the multipliers until every row is within it.
351
+ eq_prox: with equality rows, a proximal term ``eq_prox·max(ρ)`` added to
352
+ the diagonal of the box coordinates of the Newton matrix. Equalities
353
+ on the net injection leave the common level of a node's generation
354
+ and load to the weak barrier curvature, which makes the matrix
355
+ numerically singular; the term shortens such steps only and does not
356
+ move the stationary point.
334
357
 
335
358
  Returns:
336
359
  ``IPMResult``.
@@ -354,6 +377,23 @@ def solve_ipm(
354
377
  raise ValueError("box_lo/box_hi contain pairs with lo >= hi")
355
378
 
356
379
  has_box = box_idx.size > 0
380
+ eq_rows = np.asarray(eq_idx, dtype=np.int64) if eq_idx is not None else None
381
+ has_eq = eq_rows is not None and eq_rows.size > 0
382
+ if has_eq:
383
+ assert eq_rows is not None
384
+ if eq_tol is None or np.shape(eq_tol) != eq_rows.shape:
385
+ raise ValueError("eq_tol must give one tolerance per row of eq_idx")
386
+ eq_tol_arr = np.asarray(eq_tol, dtype=np.float64)
387
+ # Target shift λ/ρ of the equality rows; residuals below are r − shift.
388
+ eq_shift = np.zeros(eq_rows.size, dtype=np.float64)
389
+ raw_residual_fn = residual_fn
390
+ eq_prox_diag = np.zeros(x_init.shape[0], dtype=np.float64)
391
+ eq_prox_diag[box_idx] = eq_prox * float(np.max(r_inv_diag[eq_rows]))
392
+
393
+ def residual_fn(x: np.ndarray) -> np.ndarray:
394
+ r_eff: np.ndarray = np.array(raw_residual_fn(x), dtype=np.float64)
395
+ r_eff[eq_rows] -= eq_shift
396
+ return r_eff
357
397
 
358
398
  solver = kkt_solver if kkt_solver is not None else KKTSolver("scipy")
359
399
 
@@ -399,6 +439,8 @@ def solve_ipm(
399
439
  mu = 0.0
400
440
 
401
441
  outer_iter = 0
442
+ eq_extra = 0
443
+ eq_inf = float("nan")
402
444
  schedule_done = False
403
445
  # Termination status only (never fed back into the iteration).
404
446
  outer_exhausted = False
@@ -442,6 +484,8 @@ def solve_ipm(
442
484
  grad_data = -np.asarray(HtRinv @ r, dtype=np.float64).ravel()
443
485
 
444
486
  grad_b, hess_b = _barrier_grad_hess(x, box_idx, box_lo, box_hi, mu)
487
+ if has_eq:
488
+ hess_b = hess_b + eq_prox_diag
445
489
  grad = grad_data + grad_b
446
490
  grad_inf = float(np.max(np.abs(grad))) if grad.size else 0.0
447
491
 
@@ -552,6 +596,24 @@ def solve_ipm(
552
596
  r_at_x = r_accepted
553
597
  any_inner_step = True
554
598
 
599
+ if has_eq:
600
+ assert eq_rows is not None and r_at_x is not None
601
+ # Constraint value c = −r_raw = −(r_eff + shift); λ/ρ ← λ/ρ + c.
602
+ raw_eq = r_at_x[eq_rows] + eq_shift
603
+ eq_inf = float(np.max(np.abs(raw_eq)))
604
+ eq_ok = bool(np.all(np.abs(raw_eq) <= eq_tol_arr))
605
+ if (mu <= mu_min or not has_box) and (eq_ok or eq_extra >= eq_extra_outer):
606
+ schedule_done = True
607
+ break
608
+ if not any_inner_step and not inner_converged:
609
+ break # stuck: the multiplier update would not move x either
610
+ eq_shift = eq_shift - raw_eq
611
+ r_at_x = r_at_x.copy()
612
+ r_at_x[eq_rows] = raw_eq - eq_shift
613
+ if mu <= mu_min or not has_box:
614
+ eq_extra += 1
615
+ continue
616
+
555
617
  if not has_box:
556
618
  break # WLS mode: a single outer pass.
557
619
 
@@ -575,6 +637,11 @@ def solve_ipm(
575
637
 
576
638
  # Final residual/objective
577
639
  r_final = r_at_x if r_at_x is not None else residual_fn(x)
640
+ if has_eq:
641
+ assert eq_rows is not None
642
+ r_final = r_final.copy()
643
+ r_final[eq_rows] += eq_shift # report the true residual of the equality rows
644
+ eq_inf = float(np.max(np.abs(r_final[eq_rows])))
578
645
  obj_data = 0.5 * float(r_final @ (R_inv @ r_final))
579
646
 
580
647
  kkt = grad_inf < max(1e-2, inner_tol * 100)
@@ -596,6 +663,8 @@ def solve_ipm(
596
663
  f"outer={outer_iter + 1}, inner_total={iter_inner_total}, "
597
664
  f"μ_final={mu:.2e}, |grad|∞={grad_inf:.2e}"
598
665
  )
666
+ if has_eq:
667
+ detail += f", |c_eq|∞={eq_inf:.2e}"
599
668
  if status == "kkt":
600
669
  message = f"converged (kkt): {detail}"
601
670
  elif status == "completed":
@@ -614,6 +683,7 @@ def solve_ipm(
614
683
  message=message,
615
684
  status=status,
616
685
  solve_status=solve_status,
686
+ eq_inf_final=eq_inf,
617
687
  )
618
688
 
619
689
 
@@ -0,0 +1,456 @@
1
+ """Primal-dual predictor-corrector (Mehrotra) Interior-Point Method for constrained WLS.
2
+
3
+ The problem of :mod:`gridstate.algorithms.ipm` with two generalisations::
4
+
5
+ min_x ½ Σ_i w_i r_i(x)² (data rows, r = z − h(x))
6
+ s.t. c(x) = h_E(x) − z_E = 0 (equality rows E, e.g. hard balance)
7
+ lo_v ≤ x_v ≤ hi_v (variable boxes)
8
+ lo_j ≤ h_j(x) − z_j ≤ hi_j (boxes on measurement values)
9
+
10
+ Every inequality is ``l_k ≤ g_k(x) ≤ u_k`` with ``g_k = x_v`` (variable box)
11
+ or ``g_k = −r_j`` (measurement box; either side may be infinite). Slacks
12
+ ``s_l = g − l``, ``s_u = u − g`` and duals ``y_l, y_u ≥ 0`` turn it into the
13
+ perturbed KKT system (Gauss-Newton Hessian ``K = Hᵀ W H``)::
14
+
15
+ −Hᵀ W r + Aᵀ λ − G_lᵀ y_l + G_uᵀ y_u = 0
16
+ c(x) = 0, g − l − s_l = 0, u − g − s_u = 0
17
+ s_l·y_l = σμ, s_u·y_u = σμ
18
+
19
+ Newton step. The slack/dual blocks are eliminated per inequality, adding
20
+ ``Gᵀ D G`` (``D = y_l/s_l + y_u/s_u``) to ``K``: on the diagonal for a variable
21
+ box, as extra weight of row ``j`` for a measurement box. The equality block is
22
+ the regularised (quasi-definite) KKT ``[K' Aᵀ; A −δI]``; eliminating ``Δλ``
23
+ gives the SPD system ``(K' + ρ AᵀA)·Δx = rhs`` with ``ρ = 1/δ`` and
24
+ ``Δλ = ρ·(A Δx + c)``. Its fixed point satisfies ``c = 0`` exactly; ``ρ`` only
25
+ sets how fast the equality is reached. So every step is one normal-equations
26
+ factorization of the usual pattern (:meth:`KKTSolver.factor_normal`).
27
+
28
+ Iteration (one factorization, two solves)::
29
+
30
+ μ = (s_lᵀy_l + s_uᵀy_u) / N
31
+ affine predictor: complementarity target 0 → Δ_aff, α_aff
32
+ μ_aff = complementarity after the affine step
33
+ σ = clamp((μ_aff/μ)³, σ_min, 1)
34
+ corrector: target σμ − Δs_aff·Δy_aff, same factor → Δ
35
+ αp, αd = separate fraction-to-boundary steps (τ = 0.99995) for (x, s) and (λ, y)
36
+
37
+ Start on the central path: ``s`` = half-range of a two-sided constraint (the
38
+ distance for a one-sided one), ``y = μ0/s``; the initial point need not satisfy
39
+ ``g − l = s``.
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ import logging
45
+ from collections.abc import Callable
46
+ from dataclasses import dataclass
47
+
48
+ import numpy as np
49
+ from scipy.sparse import csr_matrix
50
+
51
+ from gridstate.algebra.estimators import HuberReweighter
52
+ from gridstate.algorithms.ipm import IPMResult, _project_to_interior
53
+ from gridstate.algorithms.kkt_solver import KKTSolver
54
+ from gridstate.algorithms.status import SolveStatus
55
+
56
+
57
+ logger = logging.getLogger(__name__)
58
+
59
+
60
+ def _max_step(v: np.ndarray, dv: np.ndarray) -> float:
61
+ """Largest ``α ∈ [0, 1]`` with ``v + α·dv ≥ 0`` (``v > 0``)."""
62
+ neg = dv < 0
63
+ if not np.any(neg):
64
+ return 1.0
65
+ return min(1.0, float(np.min(-v[neg] / dv[neg])))
66
+
67
+
68
+ Direction = tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]
69
+
70
+
71
+ @dataclass
72
+ class _Linearization:
73
+ """One iteration's linear system: the factorized matrix and the point it was built at.
74
+
75
+ Index sets: ``il`` / ``iu`` — constraints with a lower / upper side (into the
76
+ concatenation of the variable boxes, ``box_idx``, and the measurement boxes,
77
+ ``mb_rows``); ``eq_rows`` — equality rows. ``y_dual = y_l − y_u`` scattered
78
+ over all constraints.
79
+ """
80
+
81
+ H: csr_matrix
82
+ solve: Callable[[np.ndarray], np.ndarray]
83
+ r: np.ndarray
84
+ w_data: np.ndarray
85
+ c: np.ndarray
86
+ lam: np.ndarray
87
+ rho: np.ndarray
88
+ r_l: np.ndarray
89
+ r_u: np.ndarray
90
+ s_l: np.ndarray
91
+ s_u: np.ndarray
92
+ y_l: np.ndarray
93
+ y_u: np.ndarray
94
+ y_dual: np.ndarray
95
+ il: np.ndarray
96
+ iu: np.ndarray
97
+ box_idx: np.ndarray
98
+ mb_rows: np.ndarray
99
+ eq_rows: np.ndarray
100
+
101
+ def direction(self, rc_l: np.ndarray, rc_u: np.ndarray) -> Direction:
102
+ """``(Δx, Δs_l, Δs_u, Δy_l, Δy_u, Δλ)`` for the complementarity targets ``rc``."""
103
+ nb = self.box_idx.size
104
+ v_k = self.y_dual.copy()
105
+ v_k[self.il] += (rc_l - self.y_l * self.r_l) / self.s_l
106
+ v_k[self.iu] -= (rc_u - self.y_u * self.r_u) / self.s_u
107
+ rows = self.w_data * self.r
108
+ rows[self.eq_rows] = -self.lam - self.rho * self.c
109
+ rows[self.mb_rows] += v_k[nb:]
110
+ rhs = np.asarray(self.H.T @ rows, dtype=np.float64).ravel()
111
+ rhs[self.box_idx] += v_k[:nb]
112
+ dx = self.solve(rhs)
113
+ hdx = np.asarray(self.H @ dx, dtype=np.float64).ravel()
114
+ gdx = np.concatenate([dx[self.box_idx], hdx[self.mb_rows]])
115
+ ds_l = gdx[self.il] + self.r_l
116
+ ds_u = -gdx[self.iu] + self.r_u
117
+ dy_l = (rc_l - self.y_l * ds_l) / self.s_l
118
+ dy_u = (rc_u - self.y_u * ds_u) / self.s_u
119
+ dlam = self.rho * (hdx[self.eq_rows] + self.c)
120
+ return dx, ds_l, ds_u, dy_l, dy_u, dlam
121
+
122
+
123
+ def solve_ipm_pd(
124
+ x_init: np.ndarray,
125
+ residual_fn: Callable[[np.ndarray], np.ndarray],
126
+ jacobian_fn: Callable[[np.ndarray], csr_matrix],
127
+ r_inv_diag: np.ndarray,
128
+ *,
129
+ box_idx: np.ndarray | None = None,
130
+ box_lo: np.ndarray | None = None,
131
+ box_hi: np.ndarray | None = None,
132
+ meas_box_rows: np.ndarray | None = None,
133
+ meas_box_lo: np.ndarray | None = None,
134
+ meas_box_hi: np.ndarray | None = None,
135
+ eq_idx: np.ndarray | None = None,
136
+ eq_tol: np.ndarray | None = None,
137
+ mu_init: float = 0.1,
138
+ mu_min: float = 1e-6,
139
+ sigma_min: float = 1e-3,
140
+ fraction_to_boundary: float = 0.99995,
141
+ step_tol: float = 1e-4,
142
+ feas_tol: float = 1e-6,
143
+ max_iter: int = 100,
144
+ box_eps: float = 1e-3,
145
+ tr_radius: float = 0.5,
146
+ huber_c: float = 0.0,
147
+ huber_mask: np.ndarray | None = None,
148
+ huber_w_floor: float = 0.05,
149
+ huber_use_mad: bool = False,
150
+ huber_adaptive_k: float = 6.0,
151
+ huber_warmup_iters: int = 5,
152
+ kkt_solver: KKTSolver | None = None,
153
+ eq_prox: float = 1e-9,
154
+ box_prox: float = 1e-6,
155
+ ) -> IPMResult:
156
+ """Mehrotra predictor-corrector IPM (see the module docstring).
157
+
158
+ Args:
159
+ x_init, residual_fn, jacobian_fn, r_inv_diag: as in
160
+ :func:`gridstate.algorithms.ipm.solve_ipm`; ``r_inv_diag`` of an
161
+ equality row is its penalty ``ρ``.
162
+ box_idx, box_lo, box_hi: variable boxes (``lo < hi``, finite).
163
+ meas_box_rows: rows ``j`` of ``r`` whose value ``h_j − z_j = −r_j`` is
164
+ bounded by ``[meas_box_lo, meas_box_hi]`` (a side may be ±inf, not
165
+ both). Rows of ``eq_idx`` are not allowed.
166
+ eq_idx, eq_tol: equality rows ``h = z`` and their admissible ``|c|`` (p.u.).
167
+ mu_init: μ0 of the central-path start.
168
+ mu_min: stop once the complementarity μ is below it (with the other tests).
169
+ sigma_min: lower clamp of the centering parameter σ.
170
+ fraction_to_boundary: τ of the step-length rule.
171
+ step_tol: stop once the last primal step ``|αp·Δx|∞`` over the coordinates
172
+ outside ``box_idx`` (V/δ) is below it. Box variables may still move
173
+ along directions the data do not see (the common level of a node's
174
+ generation and load), which change neither the state nor the residuals.
175
+ feas_tol: admissible primal infeasibility of the inequalities (p.u.).
176
+ max_iter: iteration limit (one factorization each).
177
+ box_eps: margin from the bounds when projecting ``x_init``.
178
+ tr_radius: cap on ``|αp·Δx|∞`` over the coordinates outside ``box_idx``.
179
+ huber_*: SHGM-IRLS reweighting of the rows of ``huber_mask``, as in
180
+ ``solve_ipm``; the adaptive scale is recomputed every iteration
181
+ (μ changes every iteration).
182
+ kkt_solver: Newton-system solver (``None`` — scipy).
183
+ eq_prox: with equality rows, a proximal term ``eq_prox·max(ρ)`` on the
184
+ diagonal of the box coordinates. A hard balance fixes only the net
185
+ injection of a node, so the common level of its generation and load
186
+ is set by the weak barrier curvature alone and the system is
187
+ numerically singular; the term regularises the step only, the
188
+ fixed point is unchanged.
189
+ box_prox: a proximal term ``box_prox·median(r_inv_diag)`` on the diagonal
190
+ of the box coordinates (with or without equality rows; the larger of
191
+ the two terms applies). A box variable seen only through a balance row
192
+ has no curvature along that common level but the barrier's, so a
193
+ cold start produces huge box steps cut to tiny ``αp`` by the boundary
194
+ rule; the term bounds those steps without moving the fixed point.
195
+
196
+ Returns:
197
+ :class:`~gridstate.algorithms.ipm.IPMResult`; ``status`` is ``"kkt"`` when
198
+ every stopping test holds, ``"stalled"`` at ``max_iter``, ``"error"`` on a
199
+ failed or non-finite solve. ``iterations_outer`` = ``iterations_inner_total``
200
+ = the number of iterations.
201
+ """
202
+ x = np.asarray(x_init, dtype=np.float64)
203
+ n = x.shape[0]
204
+ m = int(r_inv_diag.shape[0])
205
+ solver = kkt_solver if kkt_solver is not None else KKTSolver("scipy")
206
+
207
+ box_idx = np.asarray(box_idx if box_idx is not None else [], dtype=np.int64)
208
+ if box_idx.size:
209
+ if box_lo is None or box_hi is None:
210
+ raise ValueError("box_lo and box_hi are required when box_idx is given")
211
+ b_lo = np.asarray(box_lo, dtype=np.float64)
212
+ b_hi = np.asarray(box_hi, dtype=np.float64)
213
+ if np.any(b_lo >= b_hi):
214
+ raise ValueError("box_lo/box_hi contain pairs with lo >= hi")
215
+ else:
216
+ b_lo = b_hi = np.zeros(0)
217
+ mb_rows = np.asarray(meas_box_rows if meas_box_rows is not None else [], dtype=np.int64)
218
+ if mb_rows.size:
219
+ if meas_box_lo is None or meas_box_hi is None:
220
+ raise ValueError("meas_box_lo and meas_box_hi are required with meas_box_rows")
221
+ m_lo = np.asarray(meas_box_lo, dtype=np.float64)
222
+ m_hi = np.asarray(meas_box_hi, dtype=np.float64)
223
+ if np.any(~np.isfinite(m_lo) & ~np.isfinite(m_hi)) or np.any(m_lo >= m_hi):
224
+ raise ValueError("every measurement box needs one finite side and lo < hi")
225
+ else:
226
+ m_lo = m_hi = np.zeros(0)
227
+ eq_rows = np.asarray(eq_idx if eq_idx is not None else [], dtype=np.int64)
228
+ if eq_rows.size:
229
+ if eq_tol is None or np.shape(eq_tol) != eq_rows.shape:
230
+ raise ValueError("eq_tol must give one tolerance per row of eq_idx")
231
+ if np.intersect1d(eq_rows, mb_rows).size:
232
+ raise ValueError("an equality row cannot carry a measurement box")
233
+ eq_tol_arr = np.asarray(eq_tol, dtype=np.float64)
234
+ else:
235
+ eq_tol_arr = np.zeros(0)
236
+
237
+ nb = box_idx.size
238
+ lo = np.concatenate([b_lo, m_lo])
239
+ hi = np.concatenate([b_hi, m_hi])
240
+ il = np.flatnonzero(np.isfinite(lo)) # constraints with a lower side
241
+ iu = np.flatnonzero(np.isfinite(hi)) # ... with an upper side
242
+ n_pairs = il.size + iu.size
243
+
244
+ w_base = np.asarray(r_inv_diag, dtype=np.float64).copy()
245
+ is_eq = np.zeros(m, dtype=bool)
246
+ is_eq[eq_rows] = True
247
+ rho = w_base[eq_rows]
248
+ prox = max(
249
+ eq_prox * float(np.max(rho)) if rho.size else 0.0,
250
+ box_prox * float(np.median(w_base[~is_eq])) if (~is_eq).any() else 0.0,
251
+ )
252
+
253
+ huber_mask_arr = (
254
+ np.asarray(huber_mask, dtype=bool) if huber_mask is not None else np.zeros(m, dtype=bool)
255
+ )
256
+ use_huber = huber_c > 0.0 and huber_mask_arr.any()
257
+ reweighter = HuberReweighter(
258
+ c=huber_c,
259
+ mask=huber_mask_arr,
260
+ sigma=(
261
+ 1.0 / np.sqrt(np.maximum(w_base, 1e-30)) if use_huber else np.ones(m, dtype=np.float64)
262
+ ),
263
+ w_floor=huber_w_floor,
264
+ adaptive_k=huber_adaptive_k,
265
+ use_mad=huber_use_mad,
266
+ )
267
+
268
+ tr_mask = np.ones(n, dtype=bool)
269
+ tr_mask[box_idx] = False
270
+
271
+ def g_of(xv: np.ndarray, r: np.ndarray) -> np.ndarray:
272
+ return np.concatenate([xv[box_idx], -r[mb_rows]])
273
+
274
+ # ---- start on the central path ----
275
+ x = _project_to_interior(x, box_idx, b_lo, b_hi, box_eps)
276
+ r = residual_fn(x)
277
+ g = g_of(x, r)
278
+ # Two-sided constraints start at the half-range whatever x is (an infeasible
279
+ # start, g − l − s_l ≠ 0): an initial point pinned near a bound would
280
+ # otherwise give a tiny slack and cut every early step to a tiny αp.
281
+ # One-sided ones start at their distance, floored.
282
+ two_sided = np.isfinite(lo) & np.isfinite(hi)
283
+ half = np.where(two_sided, 0.5 * (hi - lo), 0.0)
284
+ s_l = np.where(two_sided[il], half[il], np.maximum(g[il] - lo[il], 1e-2))
285
+ s_u = np.where(two_sided[iu], half[iu], np.maximum(hi[iu] - g[iu], 1e-2))
286
+ mu0 = float(mu_init)
287
+ y_l = mu0 / s_l
288
+ y_u = mu0 / s_u
289
+ lam = np.zeros(eq_rows.size, dtype=np.float64)
290
+
291
+ w = w_base
292
+ mu = mu0 if n_pairs else 0.0
293
+ step_inf = float("inf")
294
+ eq_inf = 0.0
295
+ grad_inf = float("inf")
296
+ status = "stalled"
297
+ message = ""
298
+ it = 0
299
+ for it in range(max_iter):
300
+ if use_huber and it >= huber_warmup_iters:
301
+ reweighter.reset_adaptive()
302
+ w = w_base * reweighter.weights(r)
303
+ w_data = np.where(is_eq, 0.0, w)
304
+ c = -r[eq_rows]
305
+ r_l = g[il] - lo[il] - s_l
306
+ r_u = hi[iu] - g[iu] - s_u
307
+ mu = (float(s_l @ y_l) + float(s_u @ y_u)) / n_pairs if n_pairs else 0.0
308
+ eq_inf = float(np.max(np.abs(c))) if c.size else 0.0
309
+ feas_inf = float(np.max(np.abs(np.concatenate([r_l, r_u])))) if n_pairs else 0.0
310
+ if (
311
+ it > 0
312
+ and mu <= mu_min
313
+ and feas_inf <= feas_tol
314
+ and bool(np.all(np.abs(c) <= eq_tol_arr))
315
+ and step_inf <= step_tol
316
+ ):
317
+ status = "kkt"
318
+ break
319
+ H = jacobian_fn(x)
320
+ # Row weights of the condensed matrix: data, equality penalty ρ,
321
+ # measurement-box curvature; box curvature goes on the diagonal.
322
+ d_k = np.zeros(nb + mb_rows.size)
323
+ d_k[il] += y_l / s_l
324
+ d_k[iu] += y_u / s_u
325
+ w_mat = w_data.copy()
326
+ w_mat[eq_rows] = rho
327
+ w_mat[mb_rows] += d_k[nb:]
328
+ diag = np.zeros(n)
329
+ diag[box_idx] = d_k[:nb] + prox
330
+ # Stationarity residual of the current point (for reporting).
331
+ y_dual = np.zeros(nb + mb_rows.size)
332
+ y_dual[il] += y_l
333
+ y_dual[iu] -= y_u
334
+ row_vec = w_data * r
335
+ row_vec[eq_rows] -= lam
336
+ row_vec[mb_rows] += y_dual[nb:]
337
+ grad = np.asarray(H.T @ row_vec, dtype=np.float64).ravel()
338
+ grad[box_idx] += y_dual[:nb]
339
+ grad_inf = float(np.max(np.abs(grad))) if grad.size else 0.0
340
+ try:
341
+ solve = solver.factor_normal(H, w_mat, diag=diag if nb else None)
342
+ except Exception as exc:
343
+ logger.warning("ipm_pd: kkt factorization failed at iteration %d: %s", it, exc)
344
+ status, message = "error", f"kkt solve failure (iteration {it})"
345
+ break
346
+
347
+ lin = _Linearization(
348
+ H=H,
349
+ solve=solve,
350
+ r=r,
351
+ w_data=w_data,
352
+ c=c,
353
+ lam=lam,
354
+ rho=rho,
355
+ r_l=r_l,
356
+ r_u=r_u,
357
+ s_l=s_l,
358
+ s_u=s_u,
359
+ y_l=y_l,
360
+ y_u=y_u,
361
+ y_dual=y_dual,
362
+ il=il,
363
+ iu=iu,
364
+ box_idx=box_idx,
365
+ mb_rows=mb_rows,
366
+ eq_rows=eq_rows,
367
+ )
368
+
369
+ # ---- predictor (affine scaling) ----
370
+ dx, ds_l, ds_u, dy_l, dy_u, dlam = lin.direction(-s_l * y_l, -s_u * y_u)
371
+ sigma = 0.0
372
+ if n_pairs:
373
+ a_p = min(_max_step(s_l, ds_l), _max_step(s_u, ds_u))
374
+ a_d = min(_max_step(y_l, dy_l), _max_step(y_u, dy_u))
375
+ mu_aff = (
376
+ float((s_l + a_p * ds_l) @ (y_l + a_d * dy_l))
377
+ + float((s_u + a_p * ds_u) @ (y_u + a_d * dy_u))
378
+ ) / n_pairs
379
+ sigma = min(1.0, max(sigma_min, (mu_aff / mu) ** 3)) if mu > 0 else sigma_min
380
+ # ---- corrector on the same factorization ----
381
+ # Never centre below the stopping level: complementarity far below
382
+ # μ_min only drives slacks to zero and the curvature y/s to overflow.
383
+ target = max(sigma * mu, 0.1 * mu_min)
384
+ dx, ds_l, ds_u, dy_l, dy_u, dlam = lin.direction(
385
+ target - s_l * y_l - ds_l * dy_l, target - s_u * y_u - ds_u * dy_u
386
+ )
387
+ if not np.all(np.isfinite(dx)):
388
+ status, message = "error", f"non-finite step (iteration {it})"
389
+ break
390
+
391
+ tau = fraction_to_boundary
392
+ a_p = min(_max_step(s_l, ds_l / tau), _max_step(s_u, ds_u / tau)) if n_pairs else 1.0
393
+ a_d = min(_max_step(y_l, dy_l / tau), _max_step(y_u, dy_u / tau)) if n_pairs else 1.0
394
+ free_inf = float(np.max(np.abs(dx[tr_mask]))) if tr_mask.any() else 0.0
395
+ a_bound = a_p
396
+ if free_inf * a_p > tr_radius:
397
+ a_p = tr_radius / free_inf
398
+
399
+ logger.debug(
400
+ "ipm_pd it=%d μ=%.2e σ=%.2e αp=%.3f (bound %.3f) αd=%.3f |Δx_free|∞=%.2e "
401
+ "|Δx|∞=%.2e |c|∞=%.2e feas=%.2e",
402
+ it,
403
+ mu,
404
+ sigma,
405
+ a_p,
406
+ a_bound,
407
+ a_d,
408
+ free_inf,
409
+ float(np.max(np.abs(dx))) if dx.size else 0.0,
410
+ eq_inf,
411
+ feas_inf,
412
+ )
413
+ x = x + a_p * dx
414
+ s_l = s_l + a_p * ds_l
415
+ s_u = s_u + a_p * ds_u
416
+ y_l = y_l + a_d * dy_l
417
+ y_u = y_u + a_d * dy_u
418
+ lam = lam + a_d * dlam
419
+ step_inf = a_p * free_inf
420
+ r = residual_fn(x)
421
+ g = g_of(x, r)
422
+ else:
423
+ it = max_iter
424
+
425
+ w_rep = w.copy()
426
+ obj = 0.5 * float(r @ (w_rep * r))
427
+ eq_inf = float(np.max(np.abs(r[eq_rows]))) if eq_rows.size else float("nan")
428
+ iterations = it if status == "kkt" else min(it + 1, max_iter)
429
+ detail = f"it={iterations}, μ={mu:.2e}, |step|∞={step_inf:.2e}, |grad|∞={grad_inf:.2e}" + (
430
+ f", |c_eq|∞={eq_inf:.2e}" if eq_rows.size else ""
431
+ )
432
+ if status == "kkt":
433
+ message = f"converged (kkt): {detail}"
434
+ solve_status = SolveStatus.KKT
435
+ elif status == "error":
436
+ message = f"{message}: {detail}"
437
+ solve_status = SolveStatus.ERROR
438
+ else:
439
+ message = f"iteration limit: {detail}"
440
+ solve_status = SolveStatus.MAX_ITER
441
+ return IPMResult(
442
+ x=x,
443
+ success=status == "kkt",
444
+ objective_data=obj,
445
+ iterations_outer=iterations,
446
+ iterations_inner_total=iterations,
447
+ mu_final=mu,
448
+ grad_inf_final=grad_inf,
449
+ message=message,
450
+ status=status,
451
+ solve_status=solve_status,
452
+ eq_inf_final=eq_inf,
453
+ )
454
+
455
+
456
+ __all__ = ["solve_ipm_pd"]