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.
- {gridstate-2.0.0 → gridstate-2.2.0}/CHANGELOG.md +50 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/PKG-INFO +1 -1
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/__init__.py +2 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/_version.py +3 -3
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algorithms/ipm.py +70 -0
- gridstate-2.2.0/gridstate/algorithms/ipm_pd.py +456 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algorithms/kkt_solver.py +59 -18
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/api.py +149 -43
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/constants.py +11 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/contract/derived.py +9 -1
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/contract/derived_tables.py +38 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/contract/version.py +3 -1
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/pipeline/config.py +72 -4
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/pipeline/passes.py +8 -2
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/pipeline/preparation.py +8 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/post_processing.py +19 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/preprocessing/ipm_setup.py +321 -8
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/result.py +33 -2
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/apply_resolved.py +137 -26
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/z_vector.py +47 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate.egg-info/PKG-INFO +1 -1
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate.egg-info/SOURCES.txt +5 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate.egg-info/scm_file_list.json +5 -0
- gridstate-2.2.0/gridstate.egg-info/scm_version.json +8 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_derived_tables.py +6 -0
- gridstate-2.2.0/tests/test_ipm_balance_modes.py +177 -0
- gridstate-2.2.0/tests/test_ipm_injection_roles.py +275 -0
- gridstate-2.2.0/tests/test_ipm_pd.py +162 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_pipeline_config_validation.py +1 -1
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_result_quality_summary.py +36 -0
- gridstate-2.2.0/tests/test_telemetry_bounds.py +86 -0
- gridstate-2.0.0/gridstate.egg-info/scm_version.json +0 -8
- {gridstate-2.0.0 → gridstate-2.2.0}/.github/workflows/ci.yml +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/.github/workflows/release.yml +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/.gitignore +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/.pre-commit-config.yaml +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/LICENSE +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/Makefile +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/README.md +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/build_cython.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/__pyinstaller/__init__.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/__pyinstaller/hook-gridstate.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/_arrays.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/adapters/__init__.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/adapters/from_pandapower.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algebra/__init__.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algebra/_selinv_kernels.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algebra/base.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algebra/estimators.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algebra/jacobian_pattern.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algebra/normalized_residuals.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algorithms/__init__.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algorithms/_gram_numba.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algorithms/status.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/algorithms/wls.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/bad_data_repass.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/bounds.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/contract/__init__.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/contract/_legacy.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/contract/containers.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/contract/serialize.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/contract/tables.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/contract/validate.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/losses.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/measurement_ids.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/pipeline/__init__.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/pipeline/context.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/pipeline/manifest.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/pipeline/orchestration.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/pipeline/runner.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/pipeline/steps.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/preprocessing/__init__.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/preprocessing/_scan.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/preprocessing/meas_rows.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/preprocessing/mirror_voltage.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/preprocessing/node_props.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/preprocessing/pseudo_measurements.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/py.typed +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/quality_summary.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/resolve_pass.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/runtime.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/shunt_sanity.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/state.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/__init__.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/_filters.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/generators.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/kinds.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/load_only_injection.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/loss_filter.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/measurements.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/noise.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/on_line.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/quality.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/rpn.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/shunts.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/sigma_floor.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/units.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/voltage_filter.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/telemetry/voltage_nominal.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/topology.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/units.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/utils.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/v_mirror.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/v_refine.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/validation/__init__.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/validation/_diagnostics.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/validation/bad_data.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/validation/chi2_test.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/validation/observability.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/working.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate/ybus.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate.egg-info/dependency_links.txt +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate.egg-info/entry_points.txt +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate.egg-info/requires.txt +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/gridstate.egg-info/top_level.txt +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/pyproject.toml +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/setup.cfg +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/__init__.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/_golden.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/conftest.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_adapters_lazy_import.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_aggregate_generators.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_algebra_base.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_anti_overshoot.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_arrays_helpers.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_bad_data.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_bad_data_repass.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_bounds.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_boxed_balance.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_breaker_reactance.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_chain_mirror.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_contract.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_contract_edge_cases.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_contract_runtime.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case118.json +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case118.npz +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case118_degraded.json +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case118_degraded.npz +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case14.json +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case14.npz +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case14_degraded.json +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case14_degraded.npz +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case30.json +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case30.npz +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case30_degraded.json +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/case30_degraded.npz +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/golden/manifest.json +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/ieee/case118.npz +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/ieee/case118_truth.npz +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/ieee/case14.npz +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/ieee/case14_truth.npz +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/ieee/case30.npz +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_data/ieee/case30_truth.npz +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_enum_codes.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_flow_sigma_floor.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_from_pandapower.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_generator_shunt_kernels.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_golden.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_gram_numba.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_ieee_cases.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_ipm.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_ipm_integration.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_jacobian_pattern.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_kkt_solver.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_load_only_injection.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_losses_breakdown.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_losses_q_convention.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_losses_trafo_classification.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_materialize_kernel.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_measurement_filter_kernels.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_measurement_ids.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_mirror_voltage_kernel.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_near_critical_ranking.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_neutral_wording.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_noise_model.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_normalized_residuals.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_numpy_only_runtime.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_observability.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_on_line_kernel.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_perf_synthetic_network.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_pipeline.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_pipeline_idempotent.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_pipeline_invariant_errors.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_pipeline_step_order.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_pipeline_warm_start.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_pipeline_working_layer.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_post_processing.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_preparation_steps.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_prepare_network.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_pseudo_unknown_load.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_pseudo_v_unobservable.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_quality_summary_paths.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_reconcile_balance.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_refine_merge.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_resolve_pass.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_rn_selected_inversion.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_serialize_npz.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_shunt_sanity.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_solve_status.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_state.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_state_range.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_telemetry_apply_kernel.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_telemetry_physics_kernels.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_tm_code_quality.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_topology_kernels.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_topology_port.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_transformer_from_shunt.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_units.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_units_array_kernels.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_v_mirror.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_v_refine.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_variance_charging.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_version.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_voltage_calibration_gen_nodes.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_voltage_range_filter_fallback.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_wls.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_wls_robustness.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_working_container.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_ybus.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tests/test_z_vector.py +0 -0
- {gridstate-2.0.0 → gridstate-2.2.0}/tools/gen_ieee_fixtures.py +0 -0
- {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
|
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (2,
|
|
21
|
+
__version__ = version = '2.2.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 2, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
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"]
|