gridstate 2.8.0__tar.gz → 2.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. {gridstate-2.8.0 → gridstate-2.9.0}/.gitignore +3 -0
  2. {gridstate-2.8.0 → gridstate-2.9.0}/CHANGELOG.md +24 -0
  3. {gridstate-2.8.0 → gridstate-2.9.0}/PKG-INFO +1 -1
  4. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/_version.py +3 -3
  5. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/pipeline/config.py +31 -8
  6. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/pipeline/manifest.py +5 -0
  7. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/pipeline/preparation.py +1 -0
  8. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/preprocessing/pseudo_measurements.py +54 -3
  9. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate.egg-info/PKG-INFO +1 -1
  10. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate.egg-info/SOURCES.txt +1 -0
  11. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate.egg-info/scm_file_list.json +1 -0
  12. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate.egg-info/scm_version.json +2 -2
  13. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_pipeline.py +24 -0
  14. gridstate-2.9.0/tests/test_pseudo_v_scope.py +196 -0
  15. {gridstate-2.8.0 → gridstate-2.9.0}/.github/workflows/ci.yml +0 -0
  16. {gridstate-2.8.0 → gridstate-2.9.0}/.github/workflows/release.yml +0 -0
  17. {gridstate-2.8.0 → gridstate-2.9.0}/.pre-commit-config.yaml +0 -0
  18. {gridstate-2.8.0 → gridstate-2.9.0}/LICENSE +0 -0
  19. {gridstate-2.8.0 → gridstate-2.9.0}/Makefile +0 -0
  20. {gridstate-2.8.0 → gridstate-2.9.0}/README.md +0 -0
  21. {gridstate-2.8.0 → gridstate-2.9.0}/build_cython.py +0 -0
  22. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/__init__.py +0 -0
  23. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/__pyinstaller/__init__.py +0 -0
  24. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/__pyinstaller/hook-gridstate.py +0 -0
  25. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/_arrays.py +0 -0
  26. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/adapters/__init__.py +0 -0
  27. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/adapters/from_pandapower.py +0 -0
  28. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/algebra/__init__.py +0 -0
  29. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/algebra/_selinv_kernels.py +0 -0
  30. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/algebra/base.py +0 -0
  31. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/algebra/estimators.py +0 -0
  32. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/algebra/jacobian_pattern.py +0 -0
  33. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/algebra/normalized_residuals.py +0 -0
  34. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/algorithms/__init__.py +0 -0
  35. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/algorithms/_gram_numba.py +0 -0
  36. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/algorithms/ipm.py +0 -0
  37. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/algorithms/ipm_pd.py +0 -0
  38. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/algorithms/kkt_solver.py +0 -0
  39. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/algorithms/status.py +0 -0
  40. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/algorithms/wls.py +0 -0
  41. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/api.py +0 -0
  42. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/bad_data_repass.py +0 -0
  43. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/bounds.py +0 -0
  44. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/constants.py +0 -0
  45. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/contract/__init__.py +0 -0
  46. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/contract/_legacy.py +0 -0
  47. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/contract/containers.py +0 -0
  48. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/contract/derived.py +0 -0
  49. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/contract/derived_tables.py +0 -0
  50. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/contract/serialize.py +0 -0
  51. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/contract/tables.py +0 -0
  52. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/contract/validate.py +0 -0
  53. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/contract/version.py +0 -0
  54. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/ipm_hints.py +0 -0
  55. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/load_characteristics.py +0 -0
  56. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/losses.py +0 -0
  57. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/measurement_ids.py +0 -0
  58. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/pipeline/__init__.py +0 -0
  59. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/pipeline/context.py +0 -0
  60. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/pipeline/orchestration.py +0 -0
  61. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/pipeline/passes.py +0 -0
  62. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/pipeline/runner.py +0 -0
  63. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/pipeline/steps.py +0 -0
  64. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/post_processing.py +0 -0
  65. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/preprocessing/__init__.py +0 -0
  66. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/preprocessing/_scan.py +0 -0
  67. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/preprocessing/ipm_setup.py +0 -0
  68. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/preprocessing/meas_rows.py +0 -0
  69. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/preprocessing/mirror_voltage.py +0 -0
  70. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/preprocessing/node_props.py +0 -0
  71. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/py.typed +0 -0
  72. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/quality_summary.py +0 -0
  73. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/resolve_pass.py +0 -0
  74. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/result.py +0 -0
  75. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/runtime.py +0 -0
  76. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/shunt_sanity.py +0 -0
  77. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/solution_hints.py +0 -0
  78. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/state.py +0 -0
  79. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/telemetry/__init__.py +0 -0
  80. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/telemetry/_filters.py +0 -0
  81. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/telemetry/apply_resolved.py +0 -0
  82. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/telemetry/generators.py +0 -0
  83. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/telemetry/kinds.py +0 -0
  84. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/telemetry/load_only_injection.py +0 -0
  85. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/telemetry/loss_filter.py +0 -0
  86. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/telemetry/measurements.py +0 -0
  87. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/telemetry/noise.py +0 -0
  88. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/telemetry/on_line.py +0 -0
  89. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/telemetry/quality.py +0 -0
  90. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/telemetry/rpn.py +0 -0
  91. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/telemetry/shunts.py +0 -0
  92. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/telemetry/sigma_floor.py +0 -0
  93. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/telemetry/units.py +0 -0
  94. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/telemetry/voltage_filter.py +0 -0
  95. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/telemetry/voltage_nominal.py +0 -0
  96. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/topology.py +0 -0
  97. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/units.py +0 -0
  98. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/utils.py +0 -0
  99. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/v_mirror.py +0 -0
  100. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/v_refine.py +0 -0
  101. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/validation/__init__.py +0 -0
  102. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/validation/_diagnostics.py +0 -0
  103. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/validation/bad_data.py +0 -0
  104. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/validation/chi2_test.py +0 -0
  105. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/validation/observability.py +0 -0
  106. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/working.py +0 -0
  107. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/ybus.py +0 -0
  108. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate/z_vector.py +0 -0
  109. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate.egg-info/dependency_links.txt +0 -0
  110. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate.egg-info/entry_points.txt +0 -0
  111. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate.egg-info/requires.txt +0 -0
  112. {gridstate-2.8.0 → gridstate-2.9.0}/gridstate.egg-info/top_level.txt +0 -0
  113. {gridstate-2.8.0 → gridstate-2.9.0}/pyproject.toml +0 -0
  114. {gridstate-2.8.0 → gridstate-2.9.0}/setup.cfg +0 -0
  115. {gridstate-2.8.0 → gridstate-2.9.0}/tests/__init__.py +0 -0
  116. {gridstate-2.8.0 → gridstate-2.9.0}/tests/_golden.py +0 -0
  117. {gridstate-2.8.0 → gridstate-2.9.0}/tests/conftest.py +0 -0
  118. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_adapters_lazy_import.py +0 -0
  119. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_aggregate_generators.py +0 -0
  120. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_algebra_base.py +0 -0
  121. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_anti_overshoot.py +0 -0
  122. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_arrays_helpers.py +0 -0
  123. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_bad_data.py +0 -0
  124. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_bad_data_repass.py +0 -0
  125. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_bounds.py +0 -0
  126. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_boxed_balance.py +0 -0
  127. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_breaker_reactance.py +0 -0
  128. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_chain_mirror.py +0 -0
  129. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_contract.py +0 -0
  130. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_contract_edge_cases.py +0 -0
  131. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_contract_runtime.py +0 -0
  132. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_data/golden/case118.json +0 -0
  133. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_data/golden/case118.npz +0 -0
  134. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_data/golden/case118_degraded.json +0 -0
  135. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_data/golden/case118_degraded.npz +0 -0
  136. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_data/golden/case14.json +0 -0
  137. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_data/golden/case14.npz +0 -0
  138. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_data/golden/case14_degraded.json +0 -0
  139. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_data/golden/case14_degraded.npz +0 -0
  140. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_data/golden/case30.json +0 -0
  141. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_data/golden/case30.npz +0 -0
  142. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_data/golden/case30_degraded.json +0 -0
  143. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_data/golden/case30_degraded.npz +0 -0
  144. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_data/golden/manifest.json +0 -0
  145. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_data/ieee/case118.npz +0 -0
  146. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_data/ieee/case118_truth.npz +0 -0
  147. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_data/ieee/case14.npz +0 -0
  148. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_data/ieee/case14_truth.npz +0 -0
  149. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_data/ieee/case30.npz +0 -0
  150. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_data/ieee/case30_truth.npz +0 -0
  151. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_derived_tables.py +0 -0
  152. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_enum_codes.py +0 -0
  153. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_flow_sigma_floor.py +0 -0
  154. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_from_pandapower.py +0 -0
  155. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_generator_shunt_kernels.py +0 -0
  156. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_golden.py +0 -0
  157. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_gram_numba.py +0 -0
  158. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_ieee_cases.py +0 -0
  159. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_ipm.py +0 -0
  160. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_ipm_balance_modes.py +0 -0
  161. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_ipm_hints.py +0 -0
  162. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_ipm_injection_roles.py +0 -0
  163. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_ipm_integration.py +0 -0
  164. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_ipm_pd.py +0 -0
  165. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_jacobian_pattern.py +0 -0
  166. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_kkt_solver.py +0 -0
  167. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_load_characteristic_segments.py +0 -0
  168. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_load_only_injection.py +0 -0
  169. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_losses_breakdown.py +0 -0
  170. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_losses_q_convention.py +0 -0
  171. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_losses_trafo_classification.py +0 -0
  172. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_materialize_kernel.py +0 -0
  173. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_measurement_filter_kernels.py +0 -0
  174. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_measurement_ids.py +0 -0
  175. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_mirror_voltage_kernel.py +0 -0
  176. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_near_critical_ranking.py +0 -0
  177. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_neutral_wording.py +0 -0
  178. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_noise_model.py +0 -0
  179. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_normalized_residuals.py +0 -0
  180. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_numpy_only_runtime.py +0 -0
  181. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_observability.py +0 -0
  182. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_on_line_kernel.py +0 -0
  183. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_perf_synthetic_network.py +0 -0
  184. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_pipeline_config_validation.py +0 -0
  185. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_pipeline_idempotent.py +0 -0
  186. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_pipeline_invariant_errors.py +0 -0
  187. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_pipeline_step_order.py +0 -0
  188. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_pipeline_warm_start.py +0 -0
  189. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_pipeline_working_layer.py +0 -0
  190. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_post_processing.py +0 -0
  191. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_preparation_steps.py +0 -0
  192. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_prepare_network.py +0 -0
  193. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_pseudo_unknown_load.py +0 -0
  194. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_pseudo_v_unobservable.py +0 -0
  195. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_quality_summary_paths.py +0 -0
  196. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_reconcile_balance.py +0 -0
  197. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_refine_merge.py +0 -0
  198. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_resolve_pass.py +0 -0
  199. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_result_quality_summary.py +0 -0
  200. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_rn_selected_inversion.py +0 -0
  201. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_serialize_npz.py +0 -0
  202. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_shunt_sanity.py +0 -0
  203. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_solution_hints.py +0 -0
  204. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_solve_status.py +0 -0
  205. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_state.py +0 -0
  206. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_state_range.py +0 -0
  207. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_telemetry_apply_kernel.py +0 -0
  208. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_telemetry_bounds.py +0 -0
  209. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_telemetry_physics_kernels.py +0 -0
  210. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_tm_code_quality.py +0 -0
  211. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_topology_kernels.py +0 -0
  212. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_topology_port.py +0 -0
  213. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_transformer_from_shunt.py +0 -0
  214. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_units.py +0 -0
  215. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_units_array_kernels.py +0 -0
  216. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_v_mirror.py +0 -0
  217. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_v_refine.py +0 -0
  218. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_variance_charging.py +0 -0
  219. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_version.py +0 -0
  220. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_voltage_calibration_gen_nodes.py +0 -0
  221. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_voltage_range_filter_fallback.py +0 -0
  222. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_wls.py +0 -0
  223. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_wls_robustness.py +0 -0
  224. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_working_container.py +0 -0
  225. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_ybus.py +0 -0
  226. {gridstate-2.8.0 → gridstate-2.9.0}/tests/test_z_vector.py +0 -0
  227. {gridstate-2.8.0 → gridstate-2.9.0}/tools/gen_ieee_fixtures.py +0 -0
  228. {gridstate-2.8.0 → gridstate-2.9.0}/tools/golden.py +0 -0
@@ -100,3 +100,6 @@ tests/test_data/**/*.log
100
100
 
101
101
  # Local-only specs / extracted material
102
102
  .specs/
103
+
104
+ # Library: no lock file in the repository
105
+ uv.lock
@@ -9,6 +9,30 @@ 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.9.0 (2026-10-18)
13
+
14
+ ### Feat
15
+
16
+ - `pseudo_voltage_scope` (`add_pseudo_measurements(voltage_scope=...)`): the
17
+ nodes whose pseudo-V prior keeps its σ. `"all"` (default) keeps the current
18
+ behavior. `"generation"` keeps it only at the slack and the nodes with
19
+ generation (`exist_gen` or an active generator); elsewhere the prior variance
20
+ is multiplied by `v_out_of_scope_loose_factor` (default 1e4, σ × 100) and the
21
+ prior stays for observability only. A prior at the nominal V on a load node
22
+ without a V measurement pulls its estimate down, and the solver makes up the
23
+ difference with the load Q. `"none"` loosens every pseudo-V prior, the slack
24
+ and the generation nodes included: V is then held by the network, the
25
+ measurements and the voltage limits (meant for `ipm` / `ipm_pd`). The step
26
+ reports `v_priors_loose` in both modes.
27
+
28
+ ### Fix
29
+
30
+ - The UI manifest showed the IPM settings (balance modes, balance weights and
31
+ the BUS-equivalent prior) only for `algorithm="ipm"`, although `ipm_pd` uses
32
+ them too. Their `depends` is now `{"algorithm": ["ipm", "ipm_pd"]}`: a list
33
+ value in `depends` means "one of". `ipm_trust_region_scope` acts in the
34
+ primal solver only and keeps `{"algorithm": "ipm"}`.
35
+
12
36
  ## v2.8.0 (2026-10-18)
13
37
 
14
38
  ### Behavior changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gridstate
3
- Version: 2.8.0
3
+ Version: 2.9.0
4
4
  Summary: Power system State Estimation: pure-Python WLS/IPM on numpy/scipy, input via a versioned file contract (npz).
5
5
  Author-email: Evgeny Istomin <gena@alpisto.eu>
6
6
  License: MIT
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.8.0'
22
- __version_tuple__ = version_tuple = (2, 8, 0)
21
+ __version__ = version = '2.9.0'
22
+ __version_tuple__ = version_tuple = (2, 9, 0)
23
23
 
24
- __commit_id__ = commit_id = 'g89cfd10b4'
24
+ __commit_id__ = commit_id = 'gaa9a1ec6e'
@@ -10,7 +10,9 @@ the UI manifest (:func:`gridstate.pipeline.manifest`):
10
10
  * ``help``: hint text;
11
11
  * ``control``: UI control hint (``"switch"`` / ``"number"`` / ``"select"``);
12
12
  * ``min`` / ``max``: bounds (numbers); ``choices``: allowed values (select);
13
- * ``depends``: show only when another field equals a value (e.g. ``algorithm=ipm``).
13
+ * ``depends``: show only when every named field matches: a scalar value means
14
+ equality, a list means one of its values (e.g. ``algorithm`` in
15
+ ``["ipm", "ipm_pd"]``).
14
16
 
15
17
  ``choices`` / ``min`` / ``max`` are enforced: :meth:`PipelineConfig.validate`
16
18
  runs on construction and at the start of a run.
@@ -79,6 +81,8 @@ _G_MODE = "Режим (нагрузка/генерация)"
79
81
  _G_CASCADE = "Каскад и статусы"
80
82
  _G_PSEUDO = "Псевдо-измерения"
81
83
  _G_EST = "Оценка состояния"
84
+ # Algorithms that read the IPM settings (``depends`` of the IPM group).
85
+ _IPM_SOLVERS = ["ipm", "ipm_pd"]
82
86
  _G_IPM = "IPM"
83
87
  _G_POST = "Пост-обработка"
84
88
 
@@ -313,6 +317,24 @@ class PipelineConfig:
313
317
  "measurements."
314
318
  ),
315
319
  )
320
+ pseudo_voltage_scope: Literal["all", "generation", "none"] = _param(
321
+ "all",
322
+ group=_G_PSEUDO,
323
+ label="Узлы с псевдо-U",
324
+ control="select",
325
+ choices=["all", "generation", "none"],
326
+ depends={"add_pseudo": True},
327
+ help="Nodes without a V measurement whose pseudo-V prior keeps its σ. "
328
+ "all — every node (default); generation — only the slack and the nodes "
329
+ "with generation (exist_gen or an active generator), which can hold their "
330
+ "voltage. Elsewhere the prior is loosened (σ × 100) and stays for "
331
+ "observability only: a prior at the nominal V on a load node pulls its "
332
+ "estimate down and the solver makes up the difference with the load Q. "
333
+ "none — every pseudo-V prior is loosened, the slack and the generation "
334
+ "nodes included: V is held by the network, the measurements and the "
335
+ "voltage limits. Meant for ipm/ipm_pd (their barrier keeps V within the "
336
+ "limits); with WLS V of an unmeasured area rests on the loosened prior.",
337
+ )
316
338
  unobservable_v_sigma_frac: float = _param(
317
339
  0.02,
318
340
  group=_G_PSEUDO,
@@ -502,7 +524,7 @@ class PipelineConfig:
502
524
  control="number",
503
525
  min=1e-4,
504
526
  max=1e6,
505
- depends={"algorithm": "ipm"},
527
+ depends={"algorithm": _IPM_SOLVERS},
506
528
  help="Меньше → balance мягче. default 0.1. Soft balance only (ipm_balance_mode=soft).",
507
529
  )
508
530
  ipm_balance_mode: str = _param(
@@ -511,7 +533,7 @@ class PipelineConfig:
511
533
  label="Balance mode",
512
534
  control="select",
513
535
  choices=["soft", "hard"],
514
- depends={"algorithm": "ipm"},
536
+ depends={"algorithm": _IPM_SOLVERS},
515
537
  help="soft — the nodal P/Q balance is a weighted pseudo-measurement "
516
538
  "(ipm_balance_weight_factor); hard — an equality constraint enforced with "
517
539
  "Lagrange multipliers (ipm: method of multipliers; ipm_pd: regularised KKT) "
@@ -603,7 +625,7 @@ class PipelineConfig:
603
625
  control="number",
604
626
  min=0.0,
605
627
  max=1e6,
606
- depends={"algorithm": "ipm"},
628
+ depends={"algorithm": _IPM_SOLVERS},
607
629
  help="Nodes whose injection range is fully set by their generation/load boxes get a "
608
630
  "balance row with variance median(σ²_data) / factor. The box carries the node's "
609
631
  "freedom; a loose balance only hides data conflicts as an imbalance. 0 — off.",
@@ -615,7 +637,7 @@ class PipelineConfig:
615
637
  control="number",
616
638
  min=0.0,
617
639
  max=1e6,
618
- depends={"algorithm": "ipm"},
640
+ depends={"algorithm": _IPM_SOLVERS},
619
641
  help="Transit nodes (no exist_gen/exist_load) get a balance row with variance "
620
642
  "median(σ²_data) / factor. Their injection is known to be zero; a loose balance "
621
643
  "turns data conflicts into a fictitious injection. 0 — off.",
@@ -626,7 +648,7 @@ class PipelineConfig:
626
648
  label="Balance mode of transit nodes",
627
649
  control="select",
628
650
  choices=["soft", "hard"],
629
- depends={"algorithm": "ipm"},
651
+ depends={"algorithm": _IPM_SOLVERS},
630
652
  help="soft — the balance of a transit node (no exist_gen/exist_load) is a "
631
653
  "weighted row like the others (ipm_transit_balance_weight_factor); hard — an "
632
654
  "equality constraint to ipm_transit_balance_tol_p_mw / _q_mvar, the other "
@@ -659,7 +681,7 @@ class PipelineConfig:
659
681
  control="number",
660
682
  min=0.0,
661
683
  max=10.0,
662
- depends={"algorithm": "ipm"},
684
+ depends={"algorithm": _IPM_SOLVERS},
663
685
  help="default 0.01 p.u.².",
664
686
  )
665
687
  ipm_trust_region_scope: str = _param(
@@ -669,7 +691,8 @@ class PipelineConfig:
669
691
  control="select",
670
692
  choices=["all", "state"],
671
693
  depends={"algorithm": "ipm"},
672
- help="Coordinates whose Newton step is bounded by the trust region (0.5 p.u.). "
694
+ help="Primal ipm only (with ipm_pd: its fallback solve). "
695
+ "Coordinates whose Newton step is bounded by the trust region (0.5 p.u.). "
673
696
  "all — the whole state vector, generation/load box variables included "
674
697
  "(the previous default); state (default) — V and δ only: a box variable "
675
698
  "that has to move far no longer shrinks the V/δ step (boxes stay protected "
@@ -26,6 +26,11 @@ def manifest(config: PipelineConfig | type[PipelineConfig] | None = None) -> dic
26
26
  toggle list), grouped by ``group``. It takes the defaults from here instead
27
27
  of hard-coding them.
28
28
 
29
+ ``depends`` maps field names to the values under which the control is shown;
30
+ every entry must match. A scalar value matches by equality, a list matches
31
+ any of its values: ``{"algorithm": ["ipm", "ipm_pd"]}`` shows the control
32
+ for both IPM solvers.
33
+
29
34
  ``config`` selects the source of defaults and of the iterated fields:
30
35
 
31
36
  * ``None`` (default) -- ``PipelineConfig()``, the library defaults;
@@ -236,6 +236,7 @@ def _s_add_pseudo(ctx: _Ctx) -> dict:
236
236
  unmeasured_gen_p_nodes=ctx.released_injections.get("P", frozenset()),
237
237
  unmeasured_gen_q_nodes=ctx.released_injections.get("Q", frozenset()),
238
238
  loosen_unknown_load=ctx.cfg.loosen_unknown_load_pseudo,
239
+ voltage_scope=ctx.cfg.pseudo_voltage_scope,
239
240
  )
240
241
  or {}
241
242
  )
@@ -5,7 +5,10 @@ unobservable, so the gain matrix becomes singular. To avoid this, for every
5
5
  active node without a `V/P_inj/Q_inj` measurement we add a weak prior:
6
6
 
7
7
  * `V ≈ V_nominal` (either `V_magnitude` from the loaded model, or `vzd`
8
- for a PV node) with σ=10% of Vnom (slack 0.1%, PV 5%).
8
+ for a PV node) with σ=10% of Vnom (slack 0.1%, PV 5%). With
9
+ ``voltage_scope="generation"`` the V prior of a node that cannot hold its
10
+ voltage (no generation, not the slack) is loosened to an observability prior;
11
+ with ``voltage_scope="none"`` every V prior is.
9
12
  * `P_inj = pg - pn`, `Q_inj = qg - qn` from the node table (for transit
10
13
  nodes this gives 0). σ depends on the node type and empty-model detection.
11
14
  """
@@ -13,7 +16,7 @@ active node without a `V/P_inj/Q_inj` measurement we add a weak prior:
13
16
  from __future__ import annotations
14
17
 
15
18
  from dataclasses import dataclass
16
- from typing import TYPE_CHECKING
19
+ from typing import TYPE_CHECKING, Literal
17
20
 
18
21
  import numpy as np
19
22
 
@@ -70,6 +73,11 @@ class PseudoMeasConfig:
70
73
  # Loosen the P/Q injection prior of a node with load but no load data (no
71
74
  # box, nothing materialized) when the injection is fixed by measurements.
72
75
  loosen_unknown_load: bool = False
76
+ # Nodes whose V prior keeps its σ (generation and slack nodes, or none);
77
+ # ``None`` = every node. Outside it the V prior variance is multiplied by
78
+ # ``v_out_of_scope_loose_factor``.
79
+ v_scope_nodes: frozenset[int] | None = None
80
+ v_out_of_scope_loose_factor: float = 1e4
73
81
 
74
82
 
75
83
  def _generation_range_variance(row: np.void, lo_col: str, hi_col: str, fallback: float) -> float:
@@ -216,6 +224,7 @@ def _build_pseudo_rows(
216
224
  n_zinj_added = 0
217
225
  n_unobs_v = 0
218
226
  n_unknown_load = 0
227
+ n_v_loose = 0
219
228
 
220
229
  new_rows: list[dict] = []
221
230
  for row in nodes_arr:
@@ -261,6 +270,11 @@ def _build_pseudo_rows(
261
270
  # nodes (degree=1, empty-model P_inj σ=100) 10% allowed the SE
262
271
  # to drag V down to 0.3-0.5 p.u.: the pseudo-V did not hold the anchor.
263
272
  sigma = (0.05 * vn) ** 2
273
+ if config.v_scope_nodes is not None and nid not in config.v_scope_nodes:
274
+ # Outside the scope a prior at the nominal or input V would pull
275
+ # the estimate there; keep it for observability only.
276
+ sigma *= config.v_out_of_scope_loose_factor
277
+ n_v_loose += 1
264
278
  new_rows.append(
265
279
  pseudo_node_measurement(ids.next(), nid, KIND_VOLTAGE, v_prior_value, float(sigma))
266
280
  )
@@ -335,6 +349,8 @@ def _build_pseudo_rows(
335
349
  "unobservable_v_tight": n_unobs_v,
336
350
  "unknown_load_loose": n_unknown_load,
337
351
  }
352
+ if config.v_scope_nodes is not None:
353
+ stats["v_priors_loose"] = n_v_loose
338
354
  return new_rows, stats
339
355
 
340
356
 
@@ -411,6 +427,8 @@ def add_pseudo_measurements(
411
427
  unmeasured_gen_p_nodes: frozenset[int] = frozenset(),
412
428
  unmeasured_gen_q_nodes: frozenset[int] = frozenset(),
413
429
  loosen_unknown_load: bool = False,
430
+ voltage_scope: Literal["all", "generation", "none"] = "all",
431
+ v_out_of_scope_loose_factor: float = 1e4,
414
432
  ) -> dict:
415
433
  """Augment the model with pseudo-measurements to remove under-observability.
416
434
 
@@ -480,10 +498,26 @@ def add_pseudo_measurements(
480
498
  at both ends of every incident branch) that prior's variance is
481
499
  multiplied by ``boundary_inj_loose_factor``. Typical nodes: cut ends of
482
500
  lines leaving the model and substations of neighbouring systems.
501
+ voltage_scope: nodes whose V prior keeps its σ. ``"all"`` (default):
502
+ every node. ``"generation"``: only the nodes that can hold their
503
+ voltage, i.e. the slack and the nodes with generation (``exist_gen``
504
+ or an active generator); elsewhere the V prior variance is multiplied
505
+ by ``v_out_of_scope_loose_factor``. A V prior at the nominal (or
506
+ input) V on a load node without a V measurement pulls the estimate
507
+ there, and the solver makes up the difference with the load Q.
508
+ ``"none"``: every V prior is loosened, the slack and the generation
509
+ nodes included; V is then held by the network, the measurements and
510
+ the voltage limits. Meant for ``ipm`` / ``ipm_pd``, whose barrier
511
+ keeps V within the limits; WLS has no such bound, and V of a node seen
512
+ by no measurement rests on the loosened prior alone.
513
+ v_out_of_scope_loose_factor: variance multiplier of the V prior outside
514
+ ``voltage_scope``. Default 1e4 (σ × 100) keeps the prior in the
515
+ system only for observability.
483
516
 
484
517
  Returns:
485
518
  ``{"v_priors_added": N, "zero_inj_added": N, "unobservable_v_tight": N,
486
- "unknown_load_loose": N}`` — counters.
519
+ "unknown_load_loose": N}`` — counters, plus ``"v_priors_loose": N`` with
520
+ ``voltage_scope`` other than ``"all"``.
487
521
  """
488
522
  nodes_arr = model.nodes.to_numpy()
489
523
  meas_arr = model.measurements.to_numpy()
@@ -496,6 +530,21 @@ def add_pseudo_measurements(
496
530
  ):
497
531
  node_load_props = extract_node_load_props_from_model(model)
498
532
 
533
+ if voltage_scope not in ("all", "generation", "none"):
534
+ raise ValueError(
535
+ f"voltage_scope={voltage_scope!r} is not one of ['all', 'generation', 'none']"
536
+ )
537
+ v_scope_nodes: frozenset[int] | None = None
538
+ if voltage_scope == "generation":
539
+ gen = model.generators.to_numpy()
540
+ holds_v = nodes_arr["exist_gen"].astype(bool)
541
+ holds_v |= nodes_arr["node_type"] == int(NodeType.SLACK)
542
+ v_scope_nodes = frozenset(
543
+ nodes_arr["id"][holds_v].tolist() + gen["node_id"][gen["status"].astype(bool)].tolist()
544
+ )
545
+ elif voltage_scope == "none":
546
+ v_scope_nodes = frozenset()
547
+
499
548
  config = PseudoMeasConfig(
500
549
  add_zero_injections=add_zero_injections,
501
550
  slack_voltage_sigma_frac=slack_voltage_sigma_frac,
@@ -510,6 +559,8 @@ def add_pseudo_measurements(
510
559
  unmeasured_gen_p_nodes=frozenset(unmeasured_gen_p_nodes),
511
560
  unmeasured_gen_q_nodes=frozenset(unmeasured_gen_q_nodes),
512
561
  loosen_unknown_load=loosen_unknown_load,
562
+ v_scope_nodes=v_scope_nodes,
563
+ v_out_of_scope_loose_factor=v_out_of_scope_loose_factor,
513
564
  )
514
565
 
515
566
  new_rows, stats = _add_pseudo_measurements_on_arrays(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gridstate
3
- Version: 2.8.0
3
+ Version: 2.9.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
@@ -169,6 +169,7 @@ tests/test_post_processing.py
169
169
  tests/test_preparation_steps.py
170
170
  tests/test_prepare_network.py
171
171
  tests/test_pseudo_unknown_load.py
172
+ tests/test_pseudo_v_scope.py
172
173
  tests/test_pseudo_v_unobservable.py
173
174
  tests/test_quality_summary_paths.py
174
175
  tests/test_reconcile_balance.py
@@ -181,6 +181,7 @@
181
181
  "tests/test_preparation_steps.py",
182
182
  "tests/test_prepare_network.py",
183
183
  "tests/test_pseudo_unknown_load.py",
184
+ "tests/test_pseudo_v_scope.py",
184
185
  "tests/test_pseudo_v_unobservable.py",
185
186
  "tests/test_quality_summary_paths.py",
186
187
  "tests/test_reconcile_balance.py",
@@ -1,7 +1,7 @@
1
1
  {
2
- "tag": "2.8.0",
2
+ "tag": "2.9.0",
3
3
  "distance": 0,
4
- "node": "g89cfd10b4b3ddbc5a57cc8c2c174af3419272c8b",
4
+ "node": "gaa9a1ec6eb30dd6708ee50ded169f77c97463212",
5
5
  "dirty": false,
6
6
  "branch": "HEAD",
7
7
  "node_date": "2026-10-18"
@@ -49,6 +49,30 @@ def test_manifest_params_have_required_fields():
49
49
  assert p["type"] in {"bool", "int", "float", "str"}
50
50
 
51
51
 
52
+ def test_manifest_depends_reference_real_fields_and_values():
53
+ params = {p["name"]: p for p in P.manifest()["params"]}
54
+ for p in params.values():
55
+ for name, value in p.get("depends", {}).items():
56
+ assert name in params, f"{p['name']}: depends on unknown field {name}"
57
+ values = value if isinstance(value, list) else [value]
58
+ assert values, f"{p['name']}: empty depends list for {name}"
59
+ choices = params[name].get("choices")
60
+ if choices is not None:
61
+ assert set(values) <= set(choices), f"{p['name']}: {values} not in {choices}"
62
+
63
+
64
+ def test_manifest_ipm_group_is_shown_for_ipm_pd():
65
+ # Primal-only: the trust region scope acts in solve_ipm (the ipm_pd fallback).
66
+ primal_only = {"ipm_trust_region_scope"}
67
+ for p in P.manifest()["params"]:
68
+ algo = p.get("depends", {}).get("algorithm")
69
+ if algo is None:
70
+ continue
71
+ values = algo if isinstance(algo, list) else [algo]
72
+ assert "ipm" in values, p["name"]
73
+ assert ("ipm_pd" in values) == (p["name"] not in primal_only), p["name"]
74
+
75
+
52
76
  def test_every_step_toggle_references_real_config_field():
53
77
  cfg_fields = {f.name for f in fields(P.PipelineConfig)}
54
78
  for step in P.STEPS:
@@ -0,0 +1,196 @@
1
+ """Tests of the pseudo-V scope.
2
+
3
+ ``add_pseudo_measurements(voltage_scope="generation")`` keeps the σ of the V
4
+ prior only at the slack and the nodes with generation (``exist_gen`` or an
5
+ active generator); elsewhere the prior is loosened by
6
+ ``v_out_of_scope_loose_factor``. ``"all"`` (default) leaves every prior as is.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import numpy as np
12
+ import pytest
13
+
14
+ from gridstate.constants import BranchType, NodeType
15
+ from gridstate.pipeline import PipelineConfig, manifest
16
+ from gridstate.pipeline import run as pipeline_run
17
+ from gridstate.preprocessing import add_pseudo_measurements
18
+ from gridstate.working import Working
19
+ from gridstate.z_vector import KIND_POWER_P, KIND_VOLTAGE, OBJ_BRANCH, OBJ_NODE
20
+
21
+
22
+ VN = 110.0
23
+
24
+
25
+ def _build_model() -> Working:
26
+ """6 nodes on a chain, no V measurement except at node 6.
27
+
28
+ * 1 slack;
29
+ * 2 ``exist_gen``;
30
+ * 3 load (``exist_load``);
31
+ * 4 transit with an inactive generator;
32
+ * 5 an active generator, ``exist_gen`` unset;
33
+ * 6 load with a real V measurement.
34
+ """
35
+ m = Working.empty()
36
+ spec = [
37
+ (1, NodeType.SLACK, False, False),
38
+ (2, NodeType.PQ, True, False),
39
+ (3, NodeType.PQ, False, True),
40
+ (4, NodeType.PQ, False, False),
41
+ (5, NodeType.PQ, False, False),
42
+ (6, NodeType.PQ, False, True),
43
+ ]
44
+ for nid, ntype, gen, load in spec:
45
+ m.nodes.add(
46
+ {
47
+ "id": nid,
48
+ "voltage_nominal": VN,
49
+ "voltage_magnitude": VN,
50
+ "voltage_angle": 0.0,
51
+ "status": True,
52
+ "node_type": int(ntype),
53
+ "exist_gen": gen,
54
+ "exist_load": load,
55
+ "generation_p": 20.0 if gen else 0.0,
56
+ "load_p": 10.0 if load else 0.0,
57
+ "load_q": 3.0 if load else 0.0,
58
+ }
59
+ )
60
+ for bid, (fr, to) in enumerate(((1, 2), (2, 3), (3, 4), (4, 5), (5, 6)), start=100):
61
+ m.branches.add(
62
+ {
63
+ "id": bid,
64
+ "from_node": fr,
65
+ "to_node": to,
66
+ "resistance": 2.0,
67
+ "reactance": 10.0,
68
+ "tap_ratio": 1.0,
69
+ "status": True,
70
+ "branch_type": int(BranchType.LINE),
71
+ }
72
+ )
73
+ for gid, nid, on in ((40, 4, False), (50, 5, True)):
74
+ m.generators.add(
75
+ {
76
+ "id": gid,
77
+ "node_id": nid,
78
+ "power_output": 5.0,
79
+ "reactive_output": 0.0,
80
+ "status": on,
81
+ }
82
+ )
83
+ next_id = 1
84
+ for bid in range(100, 105):
85
+ m.measurements.add(
86
+ {
87
+ "id": next_id,
88
+ "object_type": OBJ_BRANCH,
89
+ "object_id": bid,
90
+ "measurement_type": KIND_POWER_P,
91
+ "value": 5.0,
92
+ "variance": 1.0,
93
+ "status": True,
94
+ "is_pseudo": False,
95
+ }
96
+ )
97
+ next_id += 1
98
+ m.measurements.add(
99
+ {
100
+ "id": next_id,
101
+ "object_type": OBJ_NODE,
102
+ "object_id": 6,
103
+ "measurement_type": KIND_VOLTAGE,
104
+ "value": 116.0,
105
+ "variance": 1.0,
106
+ "status": True,
107
+ "is_pseudo": False,
108
+ }
109
+ )
110
+ return m
111
+
112
+
113
+ def _pseudo_v_sigma_frac(model: Working) -> dict[int, float]:
114
+ """``node id → σ / Vnom`` of its pseudo-V prior."""
115
+ m = model.measurements.to_numpy()
116
+ sel = m["is_pseudo"].astype(bool) & (m["object_type"] == OBJ_NODE)
117
+ sel &= m["measurement_type"] == KIND_VOLTAGE
118
+ return {
119
+ int(nid): float(np.sqrt(var)) / VN
120
+ for nid, var in zip(m["object_id"][sel], m["variance"][sel], strict=True)
121
+ }
122
+
123
+
124
+ def test_all_is_the_default():
125
+ a, b = _build_model(), _build_model()
126
+ stats_default = add_pseudo_measurements(a)
127
+ stats_all = add_pseudo_measurements(b, voltage_scope="all")
128
+ assert stats_all == stats_default
129
+ assert "v_priors_loose" not in stats_all
130
+ ma, mb = a.measurements.to_numpy(), b.measurements.to_numpy()
131
+ assert ma.tobytes() == mb.tobytes()
132
+
133
+
134
+ def test_generation_loosens_the_nodes_without_generation():
135
+ m = _build_model()
136
+ stats = add_pseudo_measurements(m, voltage_scope="generation")
137
+ sigma = _pseudo_v_sigma_frac(m)
138
+ assert sigma[1] == pytest.approx(0.001) # slack
139
+ assert sigma[2] == pytest.approx(0.05) # exist_gen
140
+ assert sigma[5] == pytest.approx(0.05) # active generator
141
+ assert sigma[3] == pytest.approx(5.0) # load: σ × 100
142
+ assert sigma[4] == pytest.approx(5.0) # transit, its generator is off
143
+ assert 6 not in sigma # real V measurement
144
+ assert stats["v_priors_loose"] == 2
145
+
146
+
147
+ def test_generation_loose_factor():
148
+ m = _build_model()
149
+ add_pseudo_measurements(m, voltage_scope="generation", v_out_of_scope_loose_factor=4.0)
150
+ sigma = _pseudo_v_sigma_frac(m)
151
+ assert sigma[3] == pytest.approx(0.1)
152
+ assert sigma[2] == pytest.approx(0.05)
153
+
154
+
155
+ def test_none_loosens_every_v_prior():
156
+ m = _build_model()
157
+ stats = add_pseudo_measurements(m, voltage_scope="none")
158
+ sigma = _pseudo_v_sigma_frac(m)
159
+ assert sigma[1] == pytest.approx(0.1) # slack: 0.001 × 100
160
+ for nid in (2, 3, 4, 5):
161
+ assert sigma[nid] == pytest.approx(5.0)
162
+ assert 6 not in sigma
163
+ assert stats["v_priors_loose"] == 5
164
+
165
+
166
+ def test_unknown_scope_raises():
167
+ with pytest.raises(ValueError, match="voltage_scope"):
168
+ add_pseudo_measurements(_build_model(), voltage_scope="load") # type: ignore[arg-type]
169
+
170
+
171
+ def test_pipeline_field_and_manifest():
172
+ assert PipelineConfig().pseudo_voltage_scope == "all"
173
+ with pytest.raises(ValueError):
174
+ PipelineConfig(pseudo_voltage_scope="load") # type: ignore[arg-type]
175
+ entry = next(p for p in manifest()["params"] if p["name"] == "pseudo_voltage_scope")
176
+ assert entry["choices"] == ["all", "generation", "none"]
177
+ assert entry["depends"] == {"add_pseudo": True}
178
+
179
+
180
+ def test_pipeline_runs_with_generation_scope():
181
+ model = _build_model()
182
+ base = pipeline_run(model, config=PipelineConfig())
183
+ same = pipeline_run(model, config=PipelineConfig(pseudo_voltage_scope="all"))
184
+ assert np.array_equal(base.v_pu, same.v_pu, equal_nan=True)
185
+ gen = pipeline_run(model, config=PipelineConfig(pseudo_voltage_scope="generation"))
186
+ assert gen.success
187
+ assert np.all(np.isfinite(gen.v_pu))
188
+
189
+
190
+ def test_ipm_pd_converges_without_v_priors():
191
+ out = pipeline_run(
192
+ _build_model(), config=PipelineConfig(algorithm="ipm_pd", pseudo_voltage_scope="none")
193
+ )
194
+ assert out.success
195
+ assert np.all(np.isfinite(out.v_pu))
196
+ assert np.all((out.v_pu > 0.7) & (out.v_pu < 1.5))
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes