gridstate 2.2.0__tar.gz → 2.4.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.2.0 → gridstate-2.4.0}/CHANGELOG.md +47 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/PKG-INFO +1 -1
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/_version.py +3 -3
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/algorithms/ipm.py +39 -1
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/algorithms/ipm_pd.py +166 -5
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/api.py +77 -5
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/contract/__init__.py +2 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/contract/tables.py +39 -1
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/contract/validate.py +25 -1
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/contract/version.py +3 -1
- gridstate-2.4.0/gridstate/ipm_hints.py +255 -0
- gridstate-2.4.0/gridstate/load_characteristics.py +99 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/pipeline/config.py +46 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/pipeline/passes.py +3 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/pipeline/preparation.py +1 -1
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/post_processing.py +34 -0
- gridstate-2.4.0/gridstate/solution_hints.py +171 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/telemetry/shunts.py +25 -12
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/working.py +19 -5
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate.egg-info/PKG-INFO +1 -1
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate.egg-info/SOURCES.txt +6 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate.egg-info/scm_file_list.json +6 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate.egg-info/scm_version.json +2 -2
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_breaker_reactance.py +13 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_data/golden/case118.json +6 -6
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_data/golden/case118.npz +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_data/golden/case118_degraded.json +6 -6
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_data/golden/case118_degraded.npz +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_data/golden/case14.json +6 -6
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_data/golden/case14.npz +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_data/golden/case14_degraded.json +6 -6
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_data/golden/case14_degraded.npz +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_data/golden/case30.json +6 -6
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_data/golden/case30.npz +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_data/golden/case30_degraded.json +6 -6
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_data/golden/case30_degraded.npz +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_data/golden/manifest.json +75 -12
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_ipm_balance_modes.py +25 -0
- gridstate-2.4.0/tests/test_ipm_hints.py +113 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_ipm_pd.py +86 -1
- gridstate-2.4.0/tests/test_load_characteristic_segments.py +159 -0
- gridstate-2.4.0/tests/test_solution_hints.py +40 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/.github/workflows/ci.yml +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/.github/workflows/release.yml +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/.gitignore +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/.pre-commit-config.yaml +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/LICENSE +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/Makefile +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/README.md +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/build_cython.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/__init__.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/__pyinstaller/__init__.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/__pyinstaller/hook-gridstate.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/_arrays.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/adapters/__init__.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/adapters/from_pandapower.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/algebra/__init__.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/algebra/_selinv_kernels.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/algebra/base.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/algebra/estimators.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/algebra/jacobian_pattern.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/algebra/normalized_residuals.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/algorithms/__init__.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/algorithms/_gram_numba.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/algorithms/kkt_solver.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/algorithms/status.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/algorithms/wls.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/bad_data_repass.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/bounds.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/constants.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/contract/_legacy.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/contract/containers.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/contract/derived.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/contract/derived_tables.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/contract/serialize.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/losses.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/measurement_ids.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/pipeline/__init__.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/pipeline/context.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/pipeline/manifest.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/pipeline/orchestration.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/pipeline/runner.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/pipeline/steps.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/preprocessing/__init__.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/preprocessing/_scan.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/preprocessing/ipm_setup.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/preprocessing/meas_rows.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/preprocessing/mirror_voltage.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/preprocessing/node_props.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/preprocessing/pseudo_measurements.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/py.typed +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/quality_summary.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/resolve_pass.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/result.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/runtime.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/shunt_sanity.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/state.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/telemetry/__init__.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/telemetry/_filters.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/telemetry/apply_resolved.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/telemetry/generators.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/telemetry/kinds.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/telemetry/load_only_injection.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/telemetry/loss_filter.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/telemetry/measurements.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/telemetry/noise.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/telemetry/on_line.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/telemetry/quality.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/telemetry/rpn.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/telemetry/sigma_floor.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/telemetry/units.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/telemetry/voltage_filter.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/telemetry/voltage_nominal.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/topology.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/units.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/utils.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/v_mirror.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/v_refine.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/validation/__init__.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/validation/_diagnostics.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/validation/bad_data.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/validation/chi2_test.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/validation/observability.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/ybus.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate/z_vector.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate.egg-info/dependency_links.txt +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate.egg-info/entry_points.txt +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate.egg-info/requires.txt +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/gridstate.egg-info/top_level.txt +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/pyproject.toml +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/setup.cfg +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/__init__.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/_golden.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/conftest.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_adapters_lazy_import.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_aggregate_generators.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_algebra_base.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_anti_overshoot.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_arrays_helpers.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_bad_data.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_bad_data_repass.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_bounds.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_boxed_balance.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_chain_mirror.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_contract.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_contract_edge_cases.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_contract_runtime.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_data/ieee/case118.npz +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_data/ieee/case118_truth.npz +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_data/ieee/case14.npz +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_data/ieee/case14_truth.npz +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_data/ieee/case30.npz +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_data/ieee/case30_truth.npz +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_derived_tables.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_enum_codes.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_flow_sigma_floor.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_from_pandapower.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_generator_shunt_kernels.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_golden.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_gram_numba.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_ieee_cases.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_ipm.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_ipm_injection_roles.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_ipm_integration.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_jacobian_pattern.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_kkt_solver.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_load_only_injection.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_losses_breakdown.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_losses_q_convention.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_losses_trafo_classification.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_materialize_kernel.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_measurement_filter_kernels.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_measurement_ids.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_mirror_voltage_kernel.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_near_critical_ranking.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_neutral_wording.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_noise_model.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_normalized_residuals.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_numpy_only_runtime.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_observability.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_on_line_kernel.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_perf_synthetic_network.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_pipeline.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_pipeline_config_validation.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_pipeline_idempotent.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_pipeline_invariant_errors.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_pipeline_step_order.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_pipeline_warm_start.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_pipeline_working_layer.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_post_processing.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_preparation_steps.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_prepare_network.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_pseudo_unknown_load.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_pseudo_v_unobservable.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_quality_summary_paths.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_reconcile_balance.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_refine_merge.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_resolve_pass.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_result_quality_summary.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_rn_selected_inversion.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_serialize_npz.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_shunt_sanity.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_solve_status.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_state.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_state_range.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_telemetry_apply_kernel.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_telemetry_bounds.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_telemetry_physics_kernels.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_tm_code_quality.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_topology_kernels.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_topology_port.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_transformer_from_shunt.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_units.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_units_array_kernels.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_v_mirror.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_v_refine.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_variance_charging.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_version.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_voltage_calibration_gen_nodes.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_voltage_range_filter_fallback.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_wls.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_wls_robustness.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_working_container.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_ybus.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tests/test_z_vector.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tools/gen_ieee_fixtures.py +0 -0
- {gridstate-2.2.0 → gridstate-2.4.0}/tools/golden.py +0 -0
|
@@ -9,6 +9,53 @@ types of the merged pull requests; breaking and behavior-changing items are
|
|
|
9
9
|
listed separately under "Breaking changes" and "Behavior changes". A release is
|
|
10
10
|
cut by tagging the merged main commit; the version is derived from that tag.
|
|
11
11
|
|
|
12
|
+
## v2.4.0 (2026-10-17)
|
|
13
|
+
|
|
14
|
+
### Behavior changes
|
|
15
|
+
|
|
16
|
+
- Breaker branches marked by the input format (R = 0, X = 1 Ohm) get
|
|
17
|
+
X = 110·min_x·Vn Ohm with min_x = 4e-6 (≈ 0.044/Vn p.u.) instead of a fixed
|
|
18
|
+
1e-3 p.u.; `breaker_x_pu` (new) sets a fixed p.u. value instead. Estimates
|
|
19
|
+
move on models carrying the marker (≤ 4.5e-3 p.u. V with `ipm_pd` on the
|
|
20
|
+
test models) (#115)
|
|
21
|
+
- A solved state with V/Vnom below `implausible_v_ratio` (new, default 0.3)
|
|
22
|
+
at any node is marked not converged whatever the solver reports, with a
|
|
23
|
+
WARNING listing the nodes and hints; a non-converged WLS solve logs its
|
|
24
|
+
largest normalized residuals (#114, #115)
|
|
25
|
+
|
|
26
|
+
### Feat
|
|
27
|
+
|
|
28
|
+
- Piecewise static load characteristics: optional input table
|
|
29
|
+
`load_characteristic_segments` (contract 3.2.0) with per-segment
|
|
30
|
+
coefficients up to V⁴ for P and Q; `apply_load_characteristic` uses it in
|
|
31
|
+
place of the quadratic row; without the table nothing changes (#113, #115)
|
|
32
|
+
|
|
33
|
+
## v2.3.0 (2026-10-17)
|
|
34
|
+
|
|
35
|
+
### Behavior changes
|
|
36
|
+
|
|
37
|
+
- `ipm_pd` uses a fixed Huber threshold `c_eff = huber_c` by default
|
|
38
|
+
(`ipm_huber_threshold="fixed"`). The previous per-iteration adaptive raise
|
|
39
|
+
made the estimate depend on the solver path; `"adaptive"` restores it. On
|
|
40
|
+
regional models the quality is unchanged; `ipm_pd` estimates move slightly
|
|
41
|
+
(≤ 6e-5 p.u. V on the test models). `wls` / `ipm` are unaffected (#111)
|
|
42
|
+
- `ipm_pd` selects its step by Armijo backtracking on a merit function
|
|
43
|
+
(barrier objective, augmented Lagrangian of the equality rows, slack
|
|
44
|
+
penalty). It now converges on weakly determined cases (e.g. unmeasured
|
|
45
|
+
units with a wide box under `injection_roles`) that needed the primal
|
|
46
|
+
fallback; `ipm_pd` estimates can move slightly. The pipeline's `ipm_pd`
|
|
47
|
+
iteration limit is 300 (100 under a hard balance); the solve stops after
|
|
48
|
+
`max_search_failures` iterations without merit decrease (#106)
|
|
49
|
+
|
|
50
|
+
### Feat
|
|
51
|
+
|
|
52
|
+
- Convergence hints: a solve that does not converge, falls back from
|
|
53
|
+
`ipm_pd`, or takes more than `ipm_hint_iterations` iterations with a
|
|
54
|
+
dominant step limiter logs a WARNING naming the limiting nodes / variables
|
|
55
|
+
(with a suggestion), loose unmeasured box variables, the largest normalized
|
|
56
|
+
residuals and part measurements outside their range. `IPMResult` gains
|
|
57
|
+
`step_limiters` / `limiting_rows` (#110)
|
|
58
|
+
|
|
12
59
|
## v2.2.0 (2026-10-17)
|
|
13
60
|
|
|
14
61
|
All new behaviour is opt-in; with the defaults the results are unchanged.
|
|
@@ -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.4.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 4, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g81767e831'
|
|
@@ -46,7 +46,7 @@ from __future__ import annotations
|
|
|
46
46
|
import logging
|
|
47
47
|
import math
|
|
48
48
|
from collections.abc import Callable
|
|
49
|
-
from dataclasses import dataclass
|
|
49
|
+
from dataclasses import dataclass, field
|
|
50
50
|
|
|
51
51
|
import numpy as np
|
|
52
52
|
from scipy.sparse import csr_matrix
|
|
@@ -106,6 +106,26 @@ class IPMResult:
|
|
|
106
106
|
solve_status: SolveStatus = SolveStatus.ERROR
|
|
107
107
|
# max |c| over the equality rows at the final point (p.u.); nan without them.
|
|
108
108
|
eq_inf_final: float = float("nan")
|
|
109
|
+
# Diagnostics of slow solves (see :mod:`gridstate.ipm_hints`): state index →
|
|
110
|
+
# number of iterations whose step that coordinate limited (it set the
|
|
111
|
+
# trust-region clip or the boundary rule, or dominated a step the line
|
|
112
|
+
# search cut).
|
|
113
|
+
step_limiters: dict[int, int] = field(default_factory=dict)
|
|
114
|
+
# ipm_pd only: measurement-box row → iterations whose step its bound limited.
|
|
115
|
+
limiting_rows: dict[int, int] = field(default_factory=dict)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _count(counts: dict[int, int], key: int) -> None:
|
|
119
|
+
counts[key] = counts.get(key, 0) + 1
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _blocking(cur: np.ndarray, step: np.ndarray, lo: np.ndarray, hi: np.ndarray) -> int:
|
|
123
|
+
"""Position of the coordinate that first reaches its bound along ``step`` (−1: none)."""
|
|
124
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
125
|
+
ratio = np.where(step < 0, (lo - cur) / step, np.where(step > 0, (hi - cur) / step, np.inf))
|
|
126
|
+
ratio = np.where(ratio > 0, ratio, np.inf)
|
|
127
|
+
k = int(np.argmin(ratio)) if ratio.size else -1
|
|
128
|
+
return k if k >= 0 and np.isfinite(ratio[k]) else -1
|
|
109
129
|
|
|
110
130
|
|
|
111
131
|
def _project_to_interior(
|
|
@@ -450,6 +470,15 @@ def solve_ipm(
|
|
|
450
470
|
# it, so the next iteration (and the final objective) reuse it instead of
|
|
451
471
|
# calling residual_fn again at the same point.
|
|
452
472
|
r_at_x: np.ndarray | None = None
|
|
473
|
+
limiters: dict[int, int] = {}
|
|
474
|
+
tr_pos = np.flatnonzero(tr_mask) if tr_mask is not None else None
|
|
475
|
+
|
|
476
|
+
def _dominant(step: np.ndarray) -> int:
|
|
477
|
+
"""State index of the largest step component inside the trust region."""
|
|
478
|
+
if tr_pos is None:
|
|
479
|
+
return int(np.argmax(np.abs(step)))
|
|
480
|
+
return int(tr_pos[np.argmax(np.abs(step[tr_pos]))]) if tr_pos.size else 0
|
|
481
|
+
|
|
453
482
|
for outer_iter in range(outer_max):
|
|
454
483
|
any_inner_step = False # Lazy outer: if no step was made → break.
|
|
455
484
|
# inner exited by tolerance (grad/step < inner_tol) — a stationary
|
|
@@ -536,6 +565,7 @@ def solve_ipm(
|
|
|
536
565
|
step_inf_raw = float(np.max(np.abs(dx_tr))) if dx_tr.size else 0.0
|
|
537
566
|
if step_inf_raw > tr_radius:
|
|
538
567
|
dx = dx * (tr_radius / step_inf_raw)
|
|
568
|
+
_count(limiters, _dominant(dx))
|
|
539
569
|
step_inf = float(np.max(np.abs(dx))) if dx.size else 0.0
|
|
540
570
|
if step_inf < inner_tol:
|
|
541
571
|
inner_converged = True
|
|
@@ -551,6 +581,11 @@ def solve_ipm(
|
|
|
551
581
|
fraction_to_boundary,
|
|
552
582
|
)
|
|
553
583
|
alpha = min(alpha_max, 1.0) if math.isfinite(alpha_max) else 1.0
|
|
584
|
+
if alpha < 1.0:
|
|
585
|
+
k = _blocking(x[box_idx], dx[box_idx], box_lo, box_hi)
|
|
586
|
+
if k >= 0:
|
|
587
|
+
_count(limiters, int(box_idx[k]))
|
|
588
|
+
alpha_first = alpha
|
|
554
589
|
|
|
555
590
|
phi_cur = 0.5 * float(r @ (R_inv @ r)) + _barrier_value(
|
|
556
591
|
x,
|
|
@@ -584,6 +619,8 @@ def solve_ipm(
|
|
|
584
619
|
break
|
|
585
620
|
|
|
586
621
|
iter_inner_total += 1
|
|
622
|
+
if not accepted or alpha < alpha_first:
|
|
623
|
+
_count(limiters, _dominant(dx))
|
|
587
624
|
if not accepted:
|
|
588
625
|
inner_stalled = True
|
|
589
626
|
# No admissible step found — the outer loop will increase μ and try again.
|
|
@@ -684,6 +721,7 @@ def solve_ipm(
|
|
|
684
721
|
status=status,
|
|
685
722
|
solve_status=solve_status,
|
|
686
723
|
eq_inf_final=eq_inf,
|
|
724
|
+
step_limiters=limiters,
|
|
687
725
|
)
|
|
688
726
|
|
|
689
727
|
|
|
@@ -33,6 +33,22 @@ Iteration (one factorization, two solves)::
|
|
|
33
33
|
σ = clamp((μ_aff/μ)³, σ_min, 1)
|
|
34
34
|
corrector: target σμ − Δs_aff·Δy_aff, same factor → Δ
|
|
35
35
|
αp, αd = separate fraction-to-boundary steps (τ = 0.99995) for (x, s) and (λ, y)
|
|
36
|
+
αp ← Armijo backtracking on the merit φ (below)
|
|
37
|
+
|
|
38
|
+
Step selection. The fraction-to-boundary rule alone lets the iteration cycle
|
|
39
|
+
on weakly determined directions (e.g. an unmeasured unit with a wide box):
|
|
40
|
+
long steps along them change neither the state estimate nor the residuals by
|
|
41
|
+
much, yet they never settle. The primal step is therefore backtracked on the
|
|
42
|
+
merit of the iteration (weights, ``λ`` and the target ``σμ`` fixed)::
|
|
43
|
+
|
|
44
|
+
φ(x, s) = ½ Σ w r² + λᵀc + ½ Σ ρ c² − σμ Σ log s + ν (‖g − l − s_l‖₁ + ‖u − g − s_u‖₁)
|
|
45
|
+
|
|
46
|
+
with ``ν ≥ 2·max y`` kept non-decreasing. At consistent slacks the pure
|
|
47
|
+
centering direction (target ``σμ − s·y``) is the Newton step on ``φ`` with the
|
|
48
|
+
positive definite primal-dual Hessian, hence a descent direction; the
|
|
49
|
+
corrector's second-order term may spoil descent, and then the centering
|
|
50
|
+
direction is taken instead (one more solve on the same factor). The dual step
|
|
51
|
+
keeps its own fraction-to-boundary length.
|
|
36
52
|
|
|
37
53
|
Start on the central path: ``s`` = half-range of a two-sided constraint (the
|
|
38
54
|
distance for a one-sided one), ``y = μ0/s``; the initial point need not satisfy
|
|
@@ -42,6 +58,7 @@ distance for a one-sided one), ``y = μ0/s``; the initial point need not satisfy
|
|
|
42
58
|
from __future__ import annotations
|
|
43
59
|
|
|
44
60
|
import logging
|
|
61
|
+
import math
|
|
45
62
|
from collections.abc import Callable
|
|
46
63
|
from dataclasses import dataclass
|
|
47
64
|
|
|
@@ -49,7 +66,7 @@ import numpy as np
|
|
|
49
66
|
from scipy.sparse import csr_matrix
|
|
50
67
|
|
|
51
68
|
from gridstate.algebra.estimators import HuberReweighter
|
|
52
|
-
from gridstate.algorithms.ipm import IPMResult, _project_to_interior
|
|
69
|
+
from gridstate.algorithms.ipm import IPMResult, _count, _project_to_interior
|
|
53
70
|
from gridstate.algorithms.kkt_solver import KKTSolver
|
|
54
71
|
from gridstate.algorithms.status import SolveStatus
|
|
55
72
|
|
|
@@ -65,6 +82,20 @@ def _max_step(v: np.ndarray, dv: np.ndarray) -> float:
|
|
|
65
82
|
return min(1.0, float(np.min(-v[neg] / dv[neg])))
|
|
66
83
|
|
|
67
84
|
|
|
85
|
+
def _blocking_constraint(
|
|
86
|
+
s_l: np.ndarray, ds_l: np.ndarray, s_u: np.ndarray, ds_u: np.ndarray
|
|
87
|
+
) -> tuple[int, bool]:
|
|
88
|
+
"""Position (in ``il`` or ``iu``) of the slack that first reaches zero, and whether it is lower."""
|
|
89
|
+
with np.errstate(divide="ignore"):
|
|
90
|
+
rl = np.where(ds_l < 0, -s_l / ds_l, np.inf)
|
|
91
|
+
ru = np.where(ds_u < 0, -s_u / ds_u, np.inf)
|
|
92
|
+
kl = int(np.argmin(rl)) if rl.size else -1
|
|
93
|
+
ku = int(np.argmin(ru)) if ru.size else -1
|
|
94
|
+
if ku < 0 or (kl >= 0 and rl[kl] <= ru[ku]):
|
|
95
|
+
return kl, True
|
|
96
|
+
return ku, False
|
|
97
|
+
|
|
98
|
+
|
|
68
99
|
Direction = tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]
|
|
69
100
|
|
|
70
101
|
|
|
@@ -120,6 +151,44 @@ class _Linearization:
|
|
|
120
151
|
return dx, ds_l, ds_u, dy_l, dy_u, dlam
|
|
121
152
|
|
|
122
153
|
|
|
154
|
+
@dataclass
|
|
155
|
+
class _Merit:
|
|
156
|
+
"""The merit ``φ`` of one iteration (see the module docstring); ``mu`` is the target ``σμ``."""
|
|
157
|
+
|
|
158
|
+
w_data: np.ndarray
|
|
159
|
+
lam: np.ndarray
|
|
160
|
+
rho: np.ndarray
|
|
161
|
+
eq_rows: np.ndarray
|
|
162
|
+
mu: float
|
|
163
|
+
nu: float
|
|
164
|
+
lo_l: np.ndarray
|
|
165
|
+
hi_u: np.ndarray
|
|
166
|
+
il: np.ndarray
|
|
167
|
+
iu: np.ndarray
|
|
168
|
+
|
|
169
|
+
def value(self, r: np.ndarray, g: np.ndarray, s_l: np.ndarray, s_u: np.ndarray) -> float:
|
|
170
|
+
c = -r[self.eq_rows]
|
|
171
|
+
phi = 0.5 * float(r @ (self.w_data * r)) + float(self.lam @ c)
|
|
172
|
+
phi += 0.5 * float(c @ (self.rho * c))
|
|
173
|
+
if self.mu > 0.0:
|
|
174
|
+
phi -= self.mu * (float(np.sum(np.log(s_l))) + float(np.sum(np.log(s_u))))
|
|
175
|
+
phi += self.nu * (
|
|
176
|
+
float(np.sum(np.abs(g[self.il] - self.lo_l - s_l)))
|
|
177
|
+
+ float(np.sum(np.abs(self.hi_u - g[self.iu] - s_u)))
|
|
178
|
+
)
|
|
179
|
+
return phi
|
|
180
|
+
|
|
181
|
+
def slope(
|
|
182
|
+
self, lin: _Linearization, hdx: np.ndarray, ds_l: np.ndarray, ds_u: np.ndarray
|
|
183
|
+
) -> float:
|
|
184
|
+
"""Directional derivative of :meth:`value` along a direction at the linearization point."""
|
|
185
|
+
d = -float((self.w_data * lin.r) @ hdx)
|
|
186
|
+
d += float((self.lam + self.rho * lin.c) @ hdx[self.eq_rows])
|
|
187
|
+
d -= self.mu * (float(np.sum(ds_l / lin.s_l)) + float(np.sum(ds_u / lin.s_u)))
|
|
188
|
+
d -= self.nu * (float(np.sum(np.abs(lin.r_l))) + float(np.sum(np.abs(lin.r_u))))
|
|
189
|
+
return d
|
|
190
|
+
|
|
191
|
+
|
|
123
192
|
def solve_ipm_pd(
|
|
124
193
|
x_init: np.ndarray,
|
|
125
194
|
residual_fn: Callable[[np.ndarray], np.ndarray],
|
|
@@ -138,6 +207,10 @@ def solve_ipm_pd(
|
|
|
138
207
|
mu_min: float = 1e-6,
|
|
139
208
|
sigma_min: float = 1e-3,
|
|
140
209
|
fraction_to_boundary: float = 0.99995,
|
|
210
|
+
armijo_c: float = 1e-4,
|
|
211
|
+
armijo_backtrack: float = 0.5,
|
|
212
|
+
armijo_max: int = 10,
|
|
213
|
+
max_search_failures: int = 20,
|
|
141
214
|
step_tol: float = 1e-4,
|
|
142
215
|
feas_tol: float = 1e-6,
|
|
143
216
|
max_iter: int = 100,
|
|
@@ -168,6 +241,12 @@ def solve_ipm_pd(
|
|
|
168
241
|
mu_min: stop once the complementarity μ is below it (with the other tests).
|
|
169
242
|
sigma_min: lower clamp of the centering parameter σ.
|
|
170
243
|
fraction_to_boundary: τ of the step-length rule.
|
|
244
|
+
armijo_c, armijo_backtrack, armijo_max: Armijo backtracking of the
|
|
245
|
+
primal step on the merit (see the module docstring); at most
|
|
246
|
+
``armijo_max`` trial points, ``0`` disables the search.
|
|
247
|
+
max_search_failures: stop (``"stalled"``) once this many iterations found
|
|
248
|
+
no sufficient decrease: the iteration is not converging and the
|
|
249
|
+
remaining budget would be spent for nothing.
|
|
171
250
|
step_tol: stop once the last primal step ``|αp·Δx|∞`` over the coordinates
|
|
172
251
|
outside ``box_idx`` (V/δ) is below it. Box variables may still move
|
|
173
252
|
along directions the data do not see (the common level of a node's
|
|
@@ -195,7 +274,8 @@ def solve_ipm_pd(
|
|
|
195
274
|
|
|
196
275
|
Returns:
|
|
197
276
|
:class:`~gridstate.algorithms.ipm.IPMResult`; ``status`` is ``"kkt"`` when
|
|
198
|
-
every stopping test holds, ``"stalled"`` at ``max_iter
|
|
277
|
+
every stopping test holds, ``"stalled"`` at ``max_iter`` or after
|
|
278
|
+
``max_search_failures``, ``"error"`` on a
|
|
199
279
|
failed or non-finite solve. ``iterations_outer`` = ``iterations_inner_total``
|
|
200
280
|
= the number of iterations.
|
|
201
281
|
"""
|
|
@@ -267,6 +347,9 @@ def solve_ipm_pd(
|
|
|
267
347
|
|
|
268
348
|
tr_mask = np.ones(n, dtype=bool)
|
|
269
349
|
tr_mask[box_idx] = False
|
|
350
|
+
tr_pos = np.flatnonzero(tr_mask)
|
|
351
|
+
limiters: dict[int, int] = {}
|
|
352
|
+
limiting_rows: dict[int, int] = {}
|
|
270
353
|
|
|
271
354
|
def g_of(xv: np.ndarray, r: np.ndarray) -> np.ndarray:
|
|
272
355
|
return np.concatenate([xv[box_idx], -r[mb_rows]])
|
|
@@ -287,6 +370,10 @@ def solve_ipm_pd(
|
|
|
287
370
|
y_l = mu0 / s_l
|
|
288
371
|
y_u = mu0 / s_u
|
|
289
372
|
lam = np.zeros(eq_rows.size, dtype=np.float64)
|
|
373
|
+
nu = 0.0
|
|
374
|
+
n_centering = 0
|
|
375
|
+
n_ls_fail = 0
|
|
376
|
+
stop_search = False
|
|
290
377
|
|
|
291
378
|
w = w_base
|
|
292
379
|
mu = mu0 if n_pairs else 0.0
|
|
@@ -388,21 +475,87 @@ def solve_ipm_pd(
|
|
|
388
475
|
status, message = "error", f"non-finite step (iteration {it})"
|
|
389
476
|
break
|
|
390
477
|
|
|
478
|
+
# ---- merit backtracking ----
|
|
479
|
+
mu_t = target if n_pairs else 0.0
|
|
480
|
+
nu = max(nu, 2.0 * float(max(np.max(y_l, initial=0.0), np.max(y_u, initial=0.0))))
|
|
481
|
+
merit = _Merit(
|
|
482
|
+
w_data=w_data,
|
|
483
|
+
lam=lam,
|
|
484
|
+
rho=rho,
|
|
485
|
+
eq_rows=eq_rows,
|
|
486
|
+
mu=mu_t,
|
|
487
|
+
nu=nu,
|
|
488
|
+
lo_l=lo[il],
|
|
489
|
+
hi_u=hi[iu],
|
|
490
|
+
il=il,
|
|
491
|
+
iu=iu,
|
|
492
|
+
)
|
|
493
|
+
slope = merit.slope(lin, H @ dx, ds_l, ds_u)
|
|
494
|
+
if slope >= 0.0 and n_pairs:
|
|
495
|
+
# The corrector's second-order term can spoil descent; the pure
|
|
496
|
+
# centering direction is a Newton step on the merit and descends
|
|
497
|
+
# whenever the slacks are consistent.
|
|
498
|
+
dx_c, ds_l_c, ds_u_c, dy_l_c, dy_u_c, dlam_c = lin.direction(
|
|
499
|
+
target - s_l * y_l, target - s_u * y_u
|
|
500
|
+
)
|
|
501
|
+
slope_c = merit.slope(lin, H @ dx_c, ds_l_c, ds_u_c)
|
|
502
|
+
if slope_c < 0.0 and np.all(np.isfinite(dx_c)):
|
|
503
|
+
dx, ds_l, ds_u, dy_l, dy_u, dlam = dx_c, ds_l_c, ds_u_c, dy_l_c, dy_u_c, dlam_c
|
|
504
|
+
slope = slope_c
|
|
505
|
+
n_centering += 1
|
|
506
|
+
|
|
391
507
|
tau = fraction_to_boundary
|
|
392
508
|
a_p = min(_max_step(s_l, ds_l / tau), _max_step(s_u, ds_u / tau)) if n_pairs else 1.0
|
|
393
509
|
a_d = min(_max_step(y_l, dy_l / tau), _max_step(y_u, dy_u / tau)) if n_pairs else 1.0
|
|
394
510
|
free_inf = float(np.max(np.abs(dx[tr_mask]))) if tr_mask.any() else 0.0
|
|
395
511
|
a_bound = a_p
|
|
512
|
+
if a_bound < 1.0:
|
|
513
|
+
k, lower = _blocking_constraint(s_l, ds_l, s_u, ds_u)
|
|
514
|
+
if k >= 0:
|
|
515
|
+
kc = int(il[k] if lower else iu[k])
|
|
516
|
+
if kc < nb:
|
|
517
|
+
_count(limiters, int(box_idx[kc]))
|
|
518
|
+
else:
|
|
519
|
+
_count(limiting_rows, int(mb_rows[kc - nb]))
|
|
396
520
|
if free_inf * a_p > tr_radius:
|
|
397
521
|
a_p = tr_radius / free_inf
|
|
522
|
+
# The stopping test measures the Newton step, not how far the search cut it.
|
|
523
|
+
step_inf = a_p * free_inf
|
|
524
|
+
a_full = a_p
|
|
525
|
+
r_new = None
|
|
526
|
+
# A direction that does not descend (inconsistent slacks of a nonlinear
|
|
527
|
+
# measurement box) is taken unsearched, as without the merit.
|
|
528
|
+
if slope < 0.0 and armijo_max > 0:
|
|
529
|
+
phi0 = merit.value(r, g, s_l, s_u)
|
|
530
|
+
for _ in range(armijo_max):
|
|
531
|
+
x_try = x + a_p * dx
|
|
532
|
+
r_try = residual_fn(x_try)
|
|
533
|
+
s_l_try = s_l + a_p * ds_l
|
|
534
|
+
s_u_try = s_u + a_p * ds_u
|
|
535
|
+
phi = merit.value(r_try, g_of(x_try, r_try), s_l_try, s_u_try)
|
|
536
|
+
r_new = r_try
|
|
537
|
+
if math.isfinite(phi) and phi <= phi0 + armijo_c * a_p * slope:
|
|
538
|
+
break
|
|
539
|
+
a_p *= armijo_backtrack
|
|
540
|
+
r_new = None
|
|
541
|
+
else:
|
|
542
|
+
n_ls_fail += 1
|
|
543
|
+
if n_ls_fail >= max_search_failures:
|
|
544
|
+
stop_search = True
|
|
545
|
+
if r_new is None:
|
|
546
|
+
# No sufficient decrease: keep the last (shortest) trial step.
|
|
547
|
+
a_p /= armijo_backtrack
|
|
548
|
+
if (a_p < a_full or a_full < a_bound) and tr_pos.size:
|
|
549
|
+
_count(limiters, int(tr_pos[np.argmax(np.abs(dx[tr_pos]))]))
|
|
398
550
|
|
|
399
551
|
logger.debug(
|
|
400
|
-
"ipm_pd it=%d μ=%.2e σ=%.2e αp=%.3f (bound %.3f) αd=%.3f |Δx_free|∞=%.2e "
|
|
552
|
+
"ipm_pd it=%d μ=%.2e σ=%.2e αp=%.3f (before search %.3f, bound %.3f) αd=%.3f |Δx_free|∞=%.2e "
|
|
401
553
|
"|Δx|∞=%.2e |c|∞=%.2e feas=%.2e",
|
|
402
554
|
it,
|
|
403
555
|
mu,
|
|
404
556
|
sigma,
|
|
405
557
|
a_p,
|
|
558
|
+
a_full,
|
|
406
559
|
a_bound,
|
|
407
560
|
a_d,
|
|
408
561
|
free_inf,
|
|
@@ -416,9 +569,10 @@ def solve_ipm_pd(
|
|
|
416
569
|
y_l = y_l + a_d * dy_l
|
|
417
570
|
y_u = y_u + a_d * dy_u
|
|
418
571
|
lam = lam + a_d * dlam
|
|
419
|
-
|
|
420
|
-
r = residual_fn(x)
|
|
572
|
+
r = residual_fn(x) if r_new is None else r_new
|
|
421
573
|
g = g_of(x, r)
|
|
574
|
+
if stop_search:
|
|
575
|
+
break
|
|
422
576
|
else:
|
|
423
577
|
it = max_iter
|
|
424
578
|
|
|
@@ -429,12 +583,17 @@ def solve_ipm_pd(
|
|
|
429
583
|
detail = f"it={iterations}, μ={mu:.2e}, |step|∞={step_inf:.2e}, |grad|∞={grad_inf:.2e}" + (
|
|
430
584
|
f", |c_eq|∞={eq_inf:.2e}" if eq_rows.size else ""
|
|
431
585
|
)
|
|
586
|
+
if n_centering or n_ls_fail:
|
|
587
|
+
detail += f", centering={n_centering}, search failures={n_ls_fail}"
|
|
432
588
|
if status == "kkt":
|
|
433
589
|
message = f"converged (kkt): {detail}"
|
|
434
590
|
solve_status = SolveStatus.KKT
|
|
435
591
|
elif status == "error":
|
|
436
592
|
message = f"{message}: {detail}"
|
|
437
593
|
solve_status = SolveStatus.ERROR
|
|
594
|
+
elif stop_search:
|
|
595
|
+
message = f"no merit decrease: {detail}"
|
|
596
|
+
solve_status = SolveStatus.STALLED
|
|
438
597
|
else:
|
|
439
598
|
message = f"iteration limit: {detail}"
|
|
440
599
|
solve_status = SolveStatus.MAX_ITER
|
|
@@ -450,6 +609,8 @@ def solve_ipm_pd(
|
|
|
450
609
|
status=status,
|
|
451
610
|
solve_status=solve_status,
|
|
452
611
|
eq_inf_final=eq_inf,
|
|
612
|
+
step_limiters=limiters,
|
|
613
|
+
limiting_rows=limiting_rows,
|
|
453
614
|
)
|
|
454
615
|
|
|
455
616
|
|
|
@@ -28,6 +28,7 @@ from gridstate.algorithms.status import (
|
|
|
28
28
|
success_from_status,
|
|
29
29
|
)
|
|
30
30
|
from gridstate.algorithms.wls import solve_wls
|
|
31
|
+
from gridstate.ipm_hints import has_dominant_limiter, ipm_convergence_hints
|
|
31
32
|
from gridstate.post_processing import (
|
|
32
33
|
apply_load_characteristic,
|
|
33
34
|
reconcile_node_balance,
|
|
@@ -47,6 +48,7 @@ from gridstate.quality_summary import (
|
|
|
47
48
|
top_worst_residuals,
|
|
48
49
|
)
|
|
49
50
|
from gridstate.result import MeasurementNormalizedResiduals, SEResult, extract_output_tables
|
|
51
|
+
from gridstate.solution_hints import low_voltage_hints, low_voltage_nodes, residual_hints
|
|
50
52
|
from gridstate.state import (
|
|
51
53
|
InitialPoint,
|
|
52
54
|
StateLayout,
|
|
@@ -99,6 +101,7 @@ def estimate(
|
|
|
99
101
|
quality_summary_rn_refine_below: float = SELINV_REFINE_BELOW,
|
|
100
102
|
quality_summary_near_critical: float = NEAR_CRITICAL_THRESHOLD,
|
|
101
103
|
success_policy: SuccessPolicy = "legacy",
|
|
104
|
+
implausible_v_ratio: float = 0.3,
|
|
102
105
|
**ipm_kwargs: Any,
|
|
103
106
|
) -> SEResult:
|
|
104
107
|
"""Run state estimation for a model and telemetry.
|
|
@@ -180,6 +183,13 @@ def estimate(
|
|
|
180
183
|
:func:`gridstate.algorithms.status.success_from_status`
|
|
181
184
|
(``kkt`` or ``mu_schedule_done`` only). ``solve_status`` and
|
|
182
185
|
``objective_wls`` are reported under both policies.
|
|
186
|
+
implausible_v_ratio: a solved state with ``V/Vnom`` below it at any
|
|
187
|
+
node is not a usable estimate whatever the solver reports (e.g. the
|
|
188
|
+
low-voltage root at a node whose V is barely observable):
|
|
189
|
+
``success`` becomes ``False``, the message names the nodes and a
|
|
190
|
+
WARNING lists hints (:mod:`gridstate.solution_hints`). ``0``
|
|
191
|
+
disables the check. A WLS solve that does not converge logs its
|
|
192
|
+
largest normalized residuals.
|
|
183
193
|
**ipm_kwargs: IPM-mode knobs, ignored for WLS. ``trust_region_scope``
|
|
184
194
|
(``"state"`` default | ``"all"``) selects the coordinates the IPM
|
|
185
195
|
trust region is measured on (see ``_run_ipm``); everything else is
|
|
@@ -309,6 +319,30 @@ def estimate(
|
|
|
309
319
|
)
|
|
310
320
|
if success_policy == "status" and not success:
|
|
311
321
|
message = f"solve status {solve_status.value}: {message}"
|
|
322
|
+
low_v = low_voltage_nodes(v_pu, implausible_v_ratio)
|
|
323
|
+
if low_v.size:
|
|
324
|
+
ids = network_pu.bus_ids[low_v][:5].tolist()
|
|
325
|
+
more = ", ..." if low_v.size > 5 else ""
|
|
326
|
+
note = (
|
|
327
|
+
f"implausible solution: V/Vnom < {implausible_v_ratio:g} at "
|
|
328
|
+
f"{low_v.size} node(s) {ids}{more}"
|
|
329
|
+
)
|
|
330
|
+
success = False
|
|
331
|
+
message = f"{note}; {message}" if message else note
|
|
332
|
+
hints = low_voltage_hints(
|
|
333
|
+
low_v, v_pu=v_pu, network_pu=network_pu, meas_index=meas_index, z=z
|
|
334
|
+
)
|
|
335
|
+
logger.warning("%s %s:\n%s", algorithm, note, "\n".join(hints))
|
|
336
|
+
elif not success and algorithm == "wls" and z.shape[0]:
|
|
337
|
+
algebra = BaseAlgebra(ybus, yf, yt, meas_index, layout, network_pu)
|
|
338
|
+
hints = residual_hints(
|
|
339
|
+
z - algebra.evaluate_h(v_pu, delta),
|
|
340
|
+
np.sqrt(floored_sigma2(r_matrix.diagonal())),
|
|
341
|
+
meas_index=meas_index,
|
|
342
|
+
network_pu=network_pu,
|
|
343
|
+
)
|
|
344
|
+
if hints:
|
|
345
|
+
logger.warning("wls did not converge (%s):\n%s", message, "\n".join(hints))
|
|
312
346
|
write_results_to_model(model, v_pu, delta, network_pu, yf=yf, yt=yt, ybus=ybus)
|
|
313
347
|
# 7. Post-processing of measurements: estimated_si/value/residual.
|
|
314
348
|
write_measurement_estimates(
|
|
@@ -676,6 +710,8 @@ def _run_ipm(
|
|
|
676
710
|
measurement_bounds: bool = False,
|
|
677
711
|
injection_roles: bool = False,
|
|
678
712
|
role_estimates: dict[int, float] | None = None,
|
|
713
|
+
hint_iterations: int = 100,
|
|
714
|
+
huber_threshold: str = "fixed",
|
|
679
715
|
**ipm_kwargs: Any,
|
|
680
716
|
) -> IPMResult:
|
|
681
717
|
"""IPM mode: extends the state vector with box-vars and solves the primal log-barrier WLS.
|
|
@@ -710,7 +746,18 @@ def _run_ipm(
|
|
|
710
746
|
variables; ``role_estimates`` (when given) receives the estimate of each
|
|
711
747
|
part row in MW / MVAr by measurement id, ``nan`` for a part row folded into a
|
|
712
748
|
net injection.
|
|
749
|
+
|
|
750
|
+
A solve that does not converge, falls back from ``ipm_pd`` or takes more
|
|
751
|
+
than ``hint_iterations`` iterations (``0`` — no iteration trigger) logs a
|
|
752
|
+
WARNING with convergence hints (:mod:`gridstate.ipm_hints`).
|
|
753
|
+
|
|
754
|
+
``huber_threshold`` (``"ipm_pd"`` only): ``"fixed"`` keeps the Huber
|
|
755
|
+
threshold at ``huber_c``; ``"adaptive"`` raises it to ``k·median|r/σ|``,
|
|
756
|
+
recomputed every iteration, so the estimate follows the solver path. The
|
|
757
|
+
primal IPM keeps its adaptive threshold (recomputed once per outer step).
|
|
713
758
|
"""
|
|
759
|
+
if huber_threshold not in ("fixed", "adaptive"):
|
|
760
|
+
raise ValueError(f"huber_threshold={huber_threshold!r} is not one of fixed/adaptive")
|
|
714
761
|
if measurement_bounds and solver != "ipm_pd":
|
|
715
762
|
raise ValueError("measurement_bounds requires algorithm='ipm_pd'")
|
|
716
763
|
if trust_region_scope not in ("all", "state"):
|
|
@@ -780,6 +827,7 @@ def _run_ipm(
|
|
|
780
827
|
return algebra.evaluate_jacobian(v, delta)
|
|
781
828
|
|
|
782
829
|
eq_idx = setup.balance_rows if setup.balance_tol is not None else None
|
|
830
|
+
pd_result: IPMResult | None = None
|
|
783
831
|
if solver == "ipm_pd":
|
|
784
832
|
mb_rows = mb_lo = mb_hi = None
|
|
785
833
|
if measurement_bounds and measurements is not None:
|
|
@@ -807,20 +855,24 @@ def _run_ipm(
|
|
|
807
855
|
eq_tol=setup.balance_tol,
|
|
808
856
|
mu_init=1.0,
|
|
809
857
|
step_tol=max(tolerance, 1e-4),
|
|
810
|
-
|
|
858
|
+
# Weakly determined units need up to ~260 iterations with separate
|
|
859
|
+
# injection parts; a hard balance does not converge in that budget
|
|
860
|
+
# either way, so it keeps the short one before the fallback.
|
|
861
|
+
max_iter=max(max_iterations, 100 if eq_idx is not None else 300),
|
|
811
862
|
tr_radius=0.5,
|
|
812
863
|
huber_c=huber_c,
|
|
813
864
|
huber_mask=huber_mask,
|
|
814
865
|
huber_w_floor=huber_w_floor,
|
|
815
866
|
huber_use_mad=huber_use_mad,
|
|
816
867
|
huber_warmup_iters=2,
|
|
817
|
-
huber_adaptive_k=2.0,
|
|
868
|
+
huber_adaptive_k=0.0 if huber_threshold == "fixed" else 2.0,
|
|
818
869
|
kkt_solver=kkt,
|
|
819
870
|
)
|
|
871
|
+
pd_result = result
|
|
820
872
|
if result.solve_status is not SolveStatus.KKT:
|
|
821
|
-
#
|
|
822
|
-
#
|
|
823
|
-
#
|
|
873
|
+
# Safety net: on a weakly determined direction (e.g. an unmeasured
|
|
874
|
+
# unit with a wide box) the primal-dual iteration can still crawl
|
|
875
|
+
# past its limit. Measurement boxes have no primal counterpart.
|
|
824
876
|
logger.warning(
|
|
825
877
|
"ipm_pd did not converge (%s), falling back to the primal IPM", result.message
|
|
826
878
|
)
|
|
@@ -863,6 +915,26 @@ def _run_ipm(
|
|
|
863
915
|
if solver == "ipm_pd":
|
|
864
916
|
result.message = f"ipm_pd fallback to ipm ({pd_message}): {result.message}"
|
|
865
917
|
|
|
918
|
+
slow: IPMResult | None = None
|
|
919
|
+
if pd_result is not None and pd_result is not result:
|
|
920
|
+
slow, reason = pd_result, "ipm_pd fell back to the primal IPM"
|
|
921
|
+
elif not result.success:
|
|
922
|
+
slow, reason = result, f"not converged ({result.status})"
|
|
923
|
+
elif 0 < hint_iterations < result.iterations_inner_total and has_dominant_limiter(
|
|
924
|
+
result, result.iterations_inner_total
|
|
925
|
+
):
|
|
926
|
+
slow, reason = result, f"{result.iterations_inner_total} iterations > {hint_iterations}"
|
|
927
|
+
if slow is not None:
|
|
928
|
+
hints = ipm_convergence_hints(
|
|
929
|
+
setup=setup,
|
|
930
|
+
network_pu=network_pu,
|
|
931
|
+
result=slow,
|
|
932
|
+
residual=residual_fn(result.x),
|
|
933
|
+
iterations=slow.iterations_inner_total,
|
|
934
|
+
)
|
|
935
|
+
if hints:
|
|
936
|
+
logger.warning("%s convergence hints (%s):\n%s", solver, reason, "\n".join(hints))
|
|
937
|
+
|
|
866
938
|
# Extract the box-vars and write them into the node table (MW/MVAr = p.u. * BASE_MVA).
|
|
867
939
|
_, _, pgen, qgen, pnag, qnag = unpack_full(result.x, layout_ipm)
|
|
868
940
|
|
|
@@ -26,6 +26,7 @@ from gridstate.contract.tables import (
|
|
|
26
26
|
BRANCHES,
|
|
27
27
|
BRANCHES_OUTPUT,
|
|
28
28
|
GENERATORS,
|
|
29
|
+
LOAD_CHARACTERISTIC_SEGMENTS,
|
|
29
30
|
LOAD_CHARACTERISTICS,
|
|
30
31
|
MEASUREMENTS,
|
|
31
32
|
MEASUREMENTS_OUTPUT,
|
|
@@ -70,6 +71,7 @@ __all__ = [
|
|
|
70
71
|
"CONTRACT_VERSION_KEY",
|
|
71
72
|
"GENERATORS",
|
|
72
73
|
"LOAD_CHARACTERISTICS",
|
|
74
|
+
"LOAD_CHARACTERISTIC_SEGMENTS",
|
|
73
75
|
"MEASUREMENTS",
|
|
74
76
|
"MEASUREMENTS_OUTPUT",
|
|
75
77
|
"NODES",
|