gridstate 2.7.0__tar.gz → 2.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gridstate-2.7.0 → gridstate-2.9.0}/.gitignore +3 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/CHANGELOG.md +56 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/PKG-INFO +1 -1
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/_version.py +3 -3
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/algorithms/ipm_pd.py +86 -2
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/api.py +73 -2
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/pipeline/config.py +41 -8
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/pipeline/manifest.py +5 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/pipeline/passes.py +1 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/pipeline/preparation.py +1 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/preprocessing/pseudo_measurements.py +54 -3
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/state.py +52 -1
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate.egg-info/PKG-INFO +1 -1
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate.egg-info/SOURCES.txt +1 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate.egg-info/scm_file_list.json +1 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate.egg-info/scm_version.json +2 -2
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_data/golden/case118.json +29 -29
- gridstate-2.9.0/tests/test_data/golden/case118.npz +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_data/golden/case118_degraded.json +40 -40
- gridstate-2.9.0/tests/test_data/golden/case118_degraded.npz +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_data/golden/case14.json +29 -29
- gridstate-2.9.0/tests/test_data/golden/case14.npz +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_data/golden/case14_degraded.json +36 -36
- gridstate-2.9.0/tests/test_data/golden/case14_degraded.npz +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_data/golden/manifest.json +52 -3
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_ipm_balance_modes.py +21 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_ipm_pd.py +57 -1
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_pipeline.py +24 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_pipeline_warm_start.py +2 -2
- gridstate-2.9.0/tests/test_pseudo_v_scope.py +196 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_state.py +54 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_state_range.py +5 -5
- gridstate-2.7.0/tests/test_data/golden/case118.npz +0 -0
- gridstate-2.7.0/tests/test_data/golden/case118_degraded.npz +0 -0
- gridstate-2.7.0/tests/test_data/golden/case14.npz +0 -0
- gridstate-2.7.0/tests/test_data/golden/case14_degraded.npz +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/.github/workflows/ci.yml +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/.github/workflows/release.yml +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/.pre-commit-config.yaml +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/LICENSE +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/Makefile +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/README.md +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/build_cython.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/__init__.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/__pyinstaller/__init__.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/__pyinstaller/hook-gridstate.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/_arrays.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/adapters/__init__.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/adapters/from_pandapower.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/algebra/__init__.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/algebra/_selinv_kernels.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/algebra/base.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/algebra/estimators.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/algebra/jacobian_pattern.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/algebra/normalized_residuals.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/algorithms/__init__.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/algorithms/_gram_numba.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/algorithms/ipm.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/algorithms/kkt_solver.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/algorithms/status.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/algorithms/wls.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/bad_data_repass.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/bounds.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/constants.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/contract/__init__.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/contract/_legacy.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/contract/containers.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/contract/derived.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/contract/derived_tables.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/contract/serialize.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/contract/tables.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/contract/validate.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/contract/version.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/ipm_hints.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/load_characteristics.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/losses.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/measurement_ids.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/pipeline/__init__.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/pipeline/context.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/pipeline/orchestration.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/pipeline/runner.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/pipeline/steps.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/post_processing.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/preprocessing/__init__.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/preprocessing/_scan.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/preprocessing/ipm_setup.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/preprocessing/meas_rows.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/preprocessing/mirror_voltage.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/preprocessing/node_props.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/py.typed +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/quality_summary.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/resolve_pass.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/result.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/runtime.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/shunt_sanity.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/solution_hints.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/telemetry/__init__.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/telemetry/_filters.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/telemetry/apply_resolved.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/telemetry/generators.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/telemetry/kinds.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/telemetry/load_only_injection.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/telemetry/loss_filter.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/telemetry/measurements.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/telemetry/noise.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/telemetry/on_line.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/telemetry/quality.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/telemetry/rpn.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/telemetry/shunts.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/telemetry/sigma_floor.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/telemetry/units.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/telemetry/voltage_filter.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/telemetry/voltage_nominal.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/topology.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/units.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/utils.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/v_mirror.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/v_refine.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/validation/__init__.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/validation/_diagnostics.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/validation/bad_data.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/validation/chi2_test.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/validation/observability.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/working.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/ybus.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate/z_vector.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate.egg-info/dependency_links.txt +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate.egg-info/entry_points.txt +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate.egg-info/requires.txt +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/gridstate.egg-info/top_level.txt +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/pyproject.toml +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/setup.cfg +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/__init__.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/_golden.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/conftest.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_adapters_lazy_import.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_aggregate_generators.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_algebra_base.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_anti_overshoot.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_arrays_helpers.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_bad_data.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_bad_data_repass.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_bounds.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_boxed_balance.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_breaker_reactance.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_chain_mirror.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_contract.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_contract_edge_cases.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_contract_runtime.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_data/golden/case30.json +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_data/golden/case30.npz +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_data/golden/case30_degraded.json +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_data/golden/case30_degraded.npz +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_data/ieee/case118.npz +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_data/ieee/case118_truth.npz +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_data/ieee/case14.npz +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_data/ieee/case14_truth.npz +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_data/ieee/case30.npz +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_data/ieee/case30_truth.npz +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_derived_tables.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_enum_codes.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_flow_sigma_floor.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_from_pandapower.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_generator_shunt_kernels.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_golden.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_gram_numba.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_ieee_cases.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_ipm.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_ipm_hints.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_ipm_injection_roles.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_ipm_integration.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_jacobian_pattern.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_kkt_solver.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_load_characteristic_segments.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_load_only_injection.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_losses_breakdown.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_losses_q_convention.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_losses_trafo_classification.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_materialize_kernel.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_measurement_filter_kernels.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_measurement_ids.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_mirror_voltage_kernel.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_near_critical_ranking.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_neutral_wording.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_noise_model.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_normalized_residuals.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_numpy_only_runtime.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_observability.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_on_line_kernel.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_perf_synthetic_network.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_pipeline_config_validation.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_pipeline_idempotent.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_pipeline_invariant_errors.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_pipeline_step_order.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_pipeline_working_layer.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_post_processing.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_preparation_steps.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_prepare_network.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_pseudo_unknown_load.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_pseudo_v_unobservable.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_quality_summary_paths.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_reconcile_balance.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_refine_merge.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_resolve_pass.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_result_quality_summary.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_rn_selected_inversion.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_serialize_npz.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_shunt_sanity.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_solution_hints.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_solve_status.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_telemetry_apply_kernel.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_telemetry_bounds.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_telemetry_physics_kernels.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_tm_code_quality.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_topology_kernels.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_topology_port.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_transformer_from_shunt.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_units.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_units_array_kernels.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_v_mirror.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_v_refine.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_variance_charging.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_version.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_voltage_calibration_gen_nodes.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_voltage_range_filter_fallback.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_wls.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_wls_robustness.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_working_container.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_ybus.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tests/test_z_vector.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tools/gen_ieee_fixtures.py +0 -0
- {gridstate-2.7.0 → gridstate-2.9.0}/tools/golden.py +0 -0
|
@@ -9,6 +9,62 @@ types of the merged pull requests; breaking and behavior-changing items are
|
|
|
9
9
|
listed separately under "Breaking changes" and "Behavior changes". A release is
|
|
10
10
|
cut by tagging the merged main commit; the version is derived from that tag.
|
|
11
11
|
|
|
12
|
+
## v2.9.0 (2026-10-18)
|
|
13
|
+
|
|
14
|
+
### Feat
|
|
15
|
+
|
|
16
|
+
- `pseudo_voltage_scope` (`add_pseudo_measurements(voltage_scope=...)`): the
|
|
17
|
+
nodes whose pseudo-V prior keeps its σ. `"all"` (default) keeps the current
|
|
18
|
+
behavior. `"generation"` keeps it only at the slack and the nodes with
|
|
19
|
+
generation (`exist_gen` or an active generator); elsewhere the prior variance
|
|
20
|
+
is multiplied by `v_out_of_scope_loose_factor` (default 1e4, σ × 100) and the
|
|
21
|
+
prior stays for observability only. A prior at the nominal V on a load node
|
|
22
|
+
without a V measurement pulls its estimate down, and the solver makes up the
|
|
23
|
+
difference with the load Q. `"none"` loosens every pseudo-V prior, the slack
|
|
24
|
+
and the generation nodes included: V is then held by the network, the
|
|
25
|
+
measurements and the voltage limits (meant for `ipm` / `ipm_pd`). The step
|
|
26
|
+
reports `v_priors_loose` in both modes.
|
|
27
|
+
|
|
28
|
+
### Fix
|
|
29
|
+
|
|
30
|
+
- The UI manifest showed the IPM settings (balance modes, balance weights and
|
|
31
|
+
the BUS-equivalent prior) only for `algorithm="ipm"`, although `ipm_pd` uses
|
|
32
|
+
them too. Their `depends` is now `{"algorithm": ["ipm", "ipm_pd"]}`: a list
|
|
33
|
+
value in `depends` means "one of". `ipm_trust_region_scope` acts in the
|
|
34
|
+
primal solver only and keeps `{"algorithm": "ipm"}`.
|
|
35
|
+
|
|
36
|
+
## v2.8.0 (2026-10-18)
|
|
37
|
+
|
|
38
|
+
### Behavior changes
|
|
39
|
+
|
|
40
|
+
- `init="flat"` starts V consistent with the transformer ratios instead of
|
|
41
|
+
1 p.u. everywhere: `log V` from a least squares over the branches
|
|
42
|
+
(`log V_to − log V_from = −log k`, weight `1/|z|`, `log V_slack = 0`). A node
|
|
43
|
+
whose nominal voltage does not match its transformer ratio (or an off-nominal
|
|
44
|
+
tap on a low-impedance branch) no longer starts with a mismatch of hundreds
|
|
45
|
+
of p.u. Applies to every algorithm; without off-nominal ratios the start is
|
|
46
|
+
unchanged. WLS on an ill-conditioned model may converge to a neighbouring
|
|
47
|
+
solution (on the test models p95 |ΔV| against the reference within 0.05 kV).
|
|
48
|
+
- `ipm_pd` voltage guard on by default (`ipm_voltage_guard` /
|
|
49
|
+
`voltage_guard`, ignored by other algorithms): every node V is kept within a
|
|
50
|
+
box from its `voltage_min` / `voltage_max` (unset or implausible limits fall
|
|
51
|
+
back to 0.55 / 1.8 p.u., 0.7–1.5 p.u. with neither set) and one step changes
|
|
52
|
+
V by at most 20 %, an angle by at most 0.6 rad and a branch angle difference
|
|
53
|
+
by at most 0.35 rad. Where a node limit is active the estimate sits on it.
|
|
54
|
+
|
|
55
|
+
### Fix
|
|
56
|
+
|
|
57
|
+
- `ipm_pd` with the global hard balance no longer collapses a generator node
|
|
58
|
+
into the low-voltage power-flow branch from a flat start (V → 0, the node
|
|
59
|
+
angle undetermined, a formally converged V ≈ 0.2 p.u.).
|
|
60
|
+
|
|
61
|
+
### Feat
|
|
62
|
+
|
|
63
|
+
- `solve_ipm_pd`: `state_box_*` (barrier boxes on state coordinates that stay
|
|
64
|
+
under the trust region and the stopping test, without box damping) and the
|
|
65
|
+
`step_rel_*` / `step_abs_*` / `step_diff_*` step limits.
|
|
66
|
+
- `gridstate.state.tap_consistent_voltages`.
|
|
67
|
+
|
|
12
68
|
## v2.7.0 (2026-10-18)
|
|
13
69
|
|
|
14
70
|
### Behavior changes
|
|
@@ -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.9.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 9, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'gaa9a1ec6e'
|
|
@@ -172,6 +172,37 @@ class _Linearization:
|
|
|
172
172
|
return dx, ds_l, ds_u, dy_l, dy_u, dlam
|
|
173
173
|
|
|
174
174
|
|
|
175
|
+
def _step_cap(
|
|
176
|
+
dx: np.ndarray,
|
|
177
|
+
x: np.ndarray,
|
|
178
|
+
rel_idx: np.ndarray,
|
|
179
|
+
rel_max: float,
|
|
180
|
+
abs_idx: np.ndarray,
|
|
181
|
+
abs_max: float,
|
|
182
|
+
pair_i: np.ndarray,
|
|
183
|
+
pair_j: np.ndarray,
|
|
184
|
+
diff_max: float,
|
|
185
|
+
) -> float:
|
|
186
|
+
"""Largest primal step length allowed by the per-coordinate step limits."""
|
|
187
|
+
cap = math.inf
|
|
188
|
+
if rel_max > 0.0 and rel_idx.size:
|
|
189
|
+
d = np.abs(dx[rel_idx])
|
|
190
|
+
lim = rel_max * np.abs(x[rel_idx])
|
|
191
|
+
k = d > 0.0
|
|
192
|
+
if k.any():
|
|
193
|
+
cap = min(cap, float(np.min(lim[k] / d[k])))
|
|
194
|
+
if abs_max > 0.0 and abs_idx.size:
|
|
195
|
+
d_max = float(np.max(np.abs(dx[abs_idx])))
|
|
196
|
+
if d_max > 0.0:
|
|
197
|
+
cap = min(cap, abs_max / d_max)
|
|
198
|
+
if diff_max > 0.0 and pair_i.size:
|
|
199
|
+
dz = np.append(dx, 0.0)
|
|
200
|
+
d_max = float(np.max(np.abs(dz[pair_i] - dz[pair_j])))
|
|
201
|
+
if d_max > 0.0:
|
|
202
|
+
cap = min(cap, diff_max / d_max)
|
|
203
|
+
return cap
|
|
204
|
+
|
|
205
|
+
|
|
175
206
|
@dataclass
|
|
176
207
|
class _Merit:
|
|
177
208
|
"""The merit ``φ`` of one iteration (see the module docstring); ``mu`` is the target ``σμ``."""
|
|
@@ -249,6 +280,15 @@ def solve_ipm_pd(
|
|
|
249
280
|
eq_prox: float = 1e-9,
|
|
250
281
|
box_prox: float = 1e-6,
|
|
251
282
|
box_prox_max: float = 1e-2,
|
|
283
|
+
state_box_idx: np.ndarray | None = None,
|
|
284
|
+
state_box_lo: np.ndarray | None = None,
|
|
285
|
+
state_box_hi: np.ndarray | None = None,
|
|
286
|
+
step_rel_idx: np.ndarray | None = None,
|
|
287
|
+
step_rel_max: float = 0.0,
|
|
288
|
+
step_abs_idx: np.ndarray | None = None,
|
|
289
|
+
step_abs_max: float = 0.0,
|
|
290
|
+
step_diff_pairs: tuple[np.ndarray, np.ndarray] | None = None,
|
|
291
|
+
step_diff_max: float = 0.0,
|
|
252
292
|
) -> IPMResult:
|
|
253
293
|
"""Mehrotra predictor-corrector IPM (see the module docstring).
|
|
254
294
|
|
|
@@ -297,6 +337,17 @@ def solve_ipm_pd(
|
|
|
297
337
|
conflicts into the residuals and make this markedly worse. ``0``
|
|
298
338
|
reweights to the end.
|
|
299
339
|
kkt_solver: Newton-system solver (``None`` — scipy).
|
|
340
|
+
state_box_idx, state_box_lo, state_box_hi: barrier boxes on state
|
|
341
|
+
coordinates (e.g. V). Unlike ``box_idx`` they stay under the trust
|
|
342
|
+
region and the stopping test and get no box damping: the box only
|
|
343
|
+
keeps the iteration away from an inadmissible region (V → 0, where
|
|
344
|
+
the node angle is undetermined).
|
|
345
|
+
step_rel_idx, step_rel_max: ``|αp·Δx_i| ≤ step_rel_max·|x_i|`` on these
|
|
346
|
+
coordinates (``0`` — off).
|
|
347
|
+
step_abs_idx, step_abs_max: ``|αp·Δx_i| ≤ step_abs_max`` (``0`` — off).
|
|
348
|
+
step_diff_pairs, step_diff_max: ``|αp·(Δx_i − Δx_j)| ≤ step_diff_max`` for
|
|
349
|
+
each pair ``(i, j)``; an index ``-1`` stands for a fixed zero (the
|
|
350
|
+
slack angle). ``0`` — off.
|
|
300
351
|
eq_prox: with equality rows, a proximal term ``eq_prox·max(ρ)`` on the
|
|
301
352
|
diagonal of the box coordinates. A hard balance fixes only the net
|
|
302
353
|
injection of a node, so the common level of its generation and load
|
|
@@ -335,6 +386,28 @@ def solve_ipm_pd(
|
|
|
335
386
|
raise ValueError("box_lo/box_hi contain pairs with lo >= hi")
|
|
336
387
|
else:
|
|
337
388
|
b_lo = b_hi = np.zeros(0)
|
|
389
|
+
n_var_box = box_idx.size
|
|
390
|
+
if state_box_idx is not None and np.size(state_box_idx):
|
|
391
|
+
if state_box_lo is None or state_box_hi is None:
|
|
392
|
+
raise ValueError("state_box_lo and state_box_hi are required with state_box_idx")
|
|
393
|
+
sb_lo = np.asarray(state_box_lo, dtype=np.float64)
|
|
394
|
+
sb_hi = np.asarray(state_box_hi, dtype=np.float64)
|
|
395
|
+
if np.any(sb_lo >= sb_hi):
|
|
396
|
+
raise ValueError("state_box_lo/state_box_hi contain pairs with lo >= hi")
|
|
397
|
+
box_idx = np.concatenate([box_idx, np.asarray(state_box_idx, dtype=np.int64)])
|
|
398
|
+
b_lo = np.concatenate([b_lo, sb_lo])
|
|
399
|
+
b_hi = np.concatenate([b_hi, sb_hi])
|
|
400
|
+
var_box_idx = box_idx[:n_var_box]
|
|
401
|
+
rel_idx = np.asarray(step_rel_idx if step_rel_idx is not None else [], dtype=np.int64)
|
|
402
|
+
abs_idx = np.asarray(step_abs_idx if step_abs_idx is not None else [], dtype=np.int64)
|
|
403
|
+
if step_diff_pairs is not None:
|
|
404
|
+
# -1 -> the appended zero coordinate.
|
|
405
|
+
pair_i = np.asarray(step_diff_pairs[0], dtype=np.int64)
|
|
406
|
+
pair_j = np.asarray(step_diff_pairs[1], dtype=np.int64)
|
|
407
|
+
pair_i = np.where(pair_i < 0, n, pair_i)
|
|
408
|
+
pair_j = np.where(pair_j < 0, n, pair_j)
|
|
409
|
+
else:
|
|
410
|
+
pair_i = pair_j = np.zeros(0, dtype=np.int64)
|
|
338
411
|
mb_rows = np.asarray(meas_box_rows if meas_box_rows is not None else [], dtype=np.int64)
|
|
339
412
|
if mb_rows.size:
|
|
340
413
|
if meas_box_lo is None or meas_box_hi is None:
|
|
@@ -386,7 +459,8 @@ def solve_ipm_pd(
|
|
|
386
459
|
)
|
|
387
460
|
|
|
388
461
|
tr_mask = np.ones(n, dtype=bool)
|
|
389
|
-
tr_mask[
|
|
462
|
+
tr_mask[var_box_idx] = False
|
|
463
|
+
prox_on = np.arange(nb) < n_var_box
|
|
390
464
|
tr_pos = np.flatnonzero(tr_mask)
|
|
391
465
|
limiters: dict[int, int] = {}
|
|
392
466
|
limiting_rows: dict[int, int] = {}
|
|
@@ -456,7 +530,9 @@ def solve_ipm_pd(
|
|
|
456
530
|
w_mat[eq_rows] = rho
|
|
457
531
|
w_mat[mb_rows] += d_k[nb:]
|
|
458
532
|
diag = np.zeros(n)
|
|
459
|
-
diag[box_idx] = d_k[:nb] +
|
|
533
|
+
diag[box_idx] = d_k[:nb] + np.where(
|
|
534
|
+
prox_on, max(eq_prox_abs, box_scale * w_median), eq_prox_abs
|
|
535
|
+
)
|
|
460
536
|
# Stationarity residual of the current point (for reporting).
|
|
461
537
|
y_dual = np.zeros(nb + mb_rows.size)
|
|
462
538
|
y_dual[il] += y_l
|
|
@@ -568,6 +644,14 @@ def solve_ipm_pd(
|
|
|
568
644
|
# The stopping test measures the Newton step, not how far the search cut it.
|
|
569
645
|
step_inf = a_p * free_inf
|
|
570
646
|
fit_inf = a_p * float(np.max(np.sqrt(w_data) * np.abs(hdx))) if m else 0.0
|
|
647
|
+
# The step limits shorten the step like the search below, after
|
|
648
|
+
# step_inf has measured the Newton step.
|
|
649
|
+
a_p = min(
|
|
650
|
+
a_p,
|
|
651
|
+
_step_cap(
|
|
652
|
+
dx, x, rel_idx, step_rel_max, abs_idx, step_abs_max, pair_i, pair_j, step_diff_max
|
|
653
|
+
),
|
|
654
|
+
)
|
|
571
655
|
a_full = a_p
|
|
572
656
|
r_new = None
|
|
573
657
|
# A direction that does not descend (inconsistent slacks of a nonlinear
|
|
@@ -55,6 +55,7 @@ from gridstate.state import (
|
|
|
55
55
|
flat_start,
|
|
56
56
|
flat_start_with_box,
|
|
57
57
|
pack,
|
|
58
|
+
tap_consistent_voltages,
|
|
58
59
|
unpack,
|
|
59
60
|
unpack_full,
|
|
60
61
|
)
|
|
@@ -120,7 +121,9 @@ def estimate(
|
|
|
120
121
|
``"ipm_pd"`` (the same problem by the primal-dual Mehrotra
|
|
121
122
|
predictor-corrector, :mod:`gridstate.algorithms.ipm_pd`).
|
|
122
123
|
init: initial guess strategy:
|
|
123
|
-
- ``"flat"`` —
|
|
124
|
+
- ``"flat"`` — δ=0 for all nodes, V consistent with the
|
|
125
|
+
transformer ratios (:func:`gridstate.state.tap_consistent_voltages`;
|
|
126
|
+
1 p.u. without off-nominal ratios);
|
|
124
127
|
- ``"results"`` — take the current ``voltage_magnitude``/``voltage_angle``
|
|
125
128
|
from ``model``;
|
|
126
129
|
- ``"slack"`` — V=V_slack for all nodes, δ=0.
|
|
@@ -651,7 +654,9 @@ def _initial_state_from_mode(
|
|
|
651
654
|
) -> np.ndarray:
|
|
652
655
|
"""``e_init`` of the ``init`` strategy alone."""
|
|
653
656
|
if init == "flat":
|
|
654
|
-
|
|
657
|
+
e = flat_start(layout)
|
|
658
|
+
e[layout.offset_v : layout.offset_v + layout.n_bus] = tap_consistent_voltages(network_pu)
|
|
659
|
+
return e
|
|
655
660
|
|
|
656
661
|
if init == "results":
|
|
657
662
|
nodes_arr = model.nodes.to_numpy()
|
|
@@ -684,6 +689,66 @@ def _initial_state_from_mode(
|
|
|
684
689
|
raise ValueError(f"Unknown init mode init={init!r}")
|
|
685
690
|
|
|
686
691
|
|
|
692
|
+
# Voltage guard of ipm_pd (``voltage_guard=True``): node V box in p.u. of the
|
|
693
|
+
# nominal voltage and the per-iteration step limits.
|
|
694
|
+
_VG_LO_MIN = 0.5 # voltage_min below this share of nominal is ignored ...
|
|
695
|
+
_VG_LO_FALLBACK = 0.55 # ... and replaced by this
|
|
696
|
+
_VG_HI_MAX = 2.0 # voltage_max above this share of nominal is ignored ...
|
|
697
|
+
_VG_HI_FALLBACK = 1.8 # ... and replaced by this
|
|
698
|
+
_VG_BOTH_UNSET = (0.7, 1.5) # neither limit set
|
|
699
|
+
_VG_V_STEP_REL = 0.2 # |αp·ΔV| ≤ 0.2·V
|
|
700
|
+
_VG_DELTA_STEP = 0.6 # |αp·Δδ| ≤ 0.6 rad
|
|
701
|
+
_VG_BRANCH_DELTA_STEP = 0.35 # |αp·(Δδ_i − Δδ_j)| ≤ 0.35 rad over a branch
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def _voltage_guard_kwargs(
|
|
705
|
+
model: Working, network_pu: NetworkPU, layout: StateLayout
|
|
706
|
+
) -> dict[str, Any]:
|
|
707
|
+
"""``solve_ipm_pd`` kwargs of the voltage guard: V boxes and step limits.
|
|
708
|
+
|
|
709
|
+
The box of a node comes from its ``voltage_min`` / ``voltage_max`` (kV): a
|
|
710
|
+
limit that is unset (0) or implausible (below half / above twice the
|
|
711
|
+
nominal) falls back to 0.55 / 1.8 p.u.; with neither limit set the box is
|
|
712
|
+
0.7…1.5 p.u. It keeps the iteration out of the collapsed region (V → 0,
|
|
713
|
+
where the node angle is undetermined); the step limits keep one Newton
|
|
714
|
+
step from crossing into it.
|
|
715
|
+
"""
|
|
716
|
+
nodes = model.nodes.to_numpy()
|
|
717
|
+
id_to_pos = id_to_pos_map(network_pu.bus_ids)
|
|
718
|
+
n = network_pu.n_bus
|
|
719
|
+
umin = np.zeros(n)
|
|
720
|
+
umax = np.zeros(n)
|
|
721
|
+
for row in nodes[nodes["status"]]:
|
|
722
|
+
pos = id_to_pos.get(int(row["id"]))
|
|
723
|
+
if pos is not None:
|
|
724
|
+
umin[pos] = float(row["voltage_min"])
|
|
725
|
+
umax[pos] = float(row["voltage_max"])
|
|
726
|
+
vn = np.asarray(network_pu.bus_vn_kv, dtype=np.float64)
|
|
727
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
728
|
+
lo_pu = np.where(vn > 0, umin / vn, 0.0)
|
|
729
|
+
hi_pu = np.where(vn > 0, umax / vn, 0.0)
|
|
730
|
+
lo = np.where((umin > 0) & (lo_pu >= _VG_LO_MIN), lo_pu, _VG_LO_FALLBACK)
|
|
731
|
+
hi = np.where((umax > 0) & (hi_pu <= _VG_HI_MAX), hi_pu, _VG_HI_FALLBACK)
|
|
732
|
+
unset = (umin <= 0) & (umax <= 0)
|
|
733
|
+
lo[unset], hi[unset] = _VG_BOTH_UNSET
|
|
734
|
+
bad = lo >= hi
|
|
735
|
+
lo[bad], hi[bad] = _VG_LO_FALLBACK, _VG_HI_FALLBACK
|
|
736
|
+
v_idx = np.arange(layout.offset_v, layout.offset_v + n, dtype=np.int64)
|
|
737
|
+
delta_of = np.full(n, -1, dtype=np.int64)
|
|
738
|
+
delta_of[layout.non_slack_idx] = layout.offset_delta + np.arange(n - 1)
|
|
739
|
+
return {
|
|
740
|
+
"state_box_idx": v_idx,
|
|
741
|
+
"state_box_lo": lo,
|
|
742
|
+
"state_box_hi": hi,
|
|
743
|
+
"step_rel_idx": v_idx,
|
|
744
|
+
"step_rel_max": _VG_V_STEP_REL,
|
|
745
|
+
"step_abs_idx": delta_of[layout.non_slack_idx],
|
|
746
|
+
"step_abs_max": _VG_DELTA_STEP,
|
|
747
|
+
"step_diff_pairs": (delta_of[network_pu.from_idx], delta_of[network_pu.to_idx]),
|
|
748
|
+
"step_diff_max": _VG_BRANCH_DELTA_STEP,
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
|
|
687
752
|
def _run_ipm(
|
|
688
753
|
model: Working,
|
|
689
754
|
network_pu: NetworkPU,
|
|
@@ -712,6 +777,7 @@ def _run_ipm(
|
|
|
712
777
|
role_estimates: dict[int, float] | None = None,
|
|
713
778
|
hint_iterations: int = 100,
|
|
714
779
|
huber_threshold: str = "fixed",
|
|
780
|
+
voltage_guard: bool = True,
|
|
715
781
|
**ipm_kwargs: Any,
|
|
716
782
|
) -> IPMResult:
|
|
717
783
|
"""IPM mode: extends the state vector with box-vars and solves the primal log-barrier WLS.
|
|
@@ -751,6 +817,10 @@ def _run_ipm(
|
|
|
751
817
|
than ``hint_iterations`` iterations (``0`` — no iteration trigger) logs a
|
|
752
818
|
WARNING with convergence hints (:mod:`gridstate.ipm_hints`).
|
|
753
819
|
|
|
820
|
+
``voltage_guard`` (``"ipm_pd"`` only, ignored by ``"ipm"``): node V boxes
|
|
821
|
+
from ``voltage_min`` / ``voltage_max`` and per-step limits on V/δ (see
|
|
822
|
+
:func:`_voltage_guard_kwargs`).
|
|
823
|
+
|
|
754
824
|
``huber_threshold`` (``"ipm_pd"`` only): ``"fixed"`` keeps the Huber
|
|
755
825
|
threshold at ``huber_c``; ``"adaptive"`` raises it to ``k·median|r/σ|``,
|
|
756
826
|
recomputed every iteration, so the estimate follows the solver path. The
|
|
@@ -866,6 +936,7 @@ def _run_ipm(
|
|
|
866
936
|
huber_warmup_iters=2,
|
|
867
937
|
huber_adaptive_k=0.0 if huber_threshold == "fixed" else 2.0,
|
|
868
938
|
kkt_solver=kkt,
|
|
939
|
+
**(_voltage_guard_kwargs(model, network_pu, layout_ipm) if voltage_guard else {}),
|
|
869
940
|
)
|
|
870
941
|
pd_result = result
|
|
871
942
|
if result.solve_status is not SolveStatus.KKT:
|
|
@@ -10,7 +10,9 @@ the UI manifest (:func:`gridstate.pipeline.manifest`):
|
|
|
10
10
|
* ``help``: hint text;
|
|
11
11
|
* ``control``: UI control hint (``"switch"`` / ``"number"`` / ``"select"``);
|
|
12
12
|
* ``min`` / ``max``: bounds (numbers); ``choices``: allowed values (select);
|
|
13
|
-
* ``depends``: show only when
|
|
13
|
+
* ``depends``: show only when every named field matches: a scalar value means
|
|
14
|
+
equality, a list means one of its values (e.g. ``algorithm`` in
|
|
15
|
+
``["ipm", "ipm_pd"]``).
|
|
14
16
|
|
|
15
17
|
``choices`` / ``min`` / ``max`` are enforced: :meth:`PipelineConfig.validate`
|
|
16
18
|
runs on construction and at the start of a run.
|
|
@@ -79,6 +81,8 @@ _G_MODE = "Режим (нагрузка/генерация)"
|
|
|
79
81
|
_G_CASCADE = "Каскад и статусы"
|
|
80
82
|
_G_PSEUDO = "Псевдо-измерения"
|
|
81
83
|
_G_EST = "Оценка состояния"
|
|
84
|
+
# Algorithms that read the IPM settings (``depends`` of the IPM group).
|
|
85
|
+
_IPM_SOLVERS = ["ipm", "ipm_pd"]
|
|
82
86
|
_G_IPM = "IPM"
|
|
83
87
|
_G_POST = "Пост-обработка"
|
|
84
88
|
|
|
@@ -313,6 +317,24 @@ class PipelineConfig:
|
|
|
313
317
|
"measurements."
|
|
314
318
|
),
|
|
315
319
|
)
|
|
320
|
+
pseudo_voltage_scope: Literal["all", "generation", "none"] = _param(
|
|
321
|
+
"all",
|
|
322
|
+
group=_G_PSEUDO,
|
|
323
|
+
label="Узлы с псевдо-U",
|
|
324
|
+
control="select",
|
|
325
|
+
choices=["all", "generation", "none"],
|
|
326
|
+
depends={"add_pseudo": True},
|
|
327
|
+
help="Nodes without a V measurement whose pseudo-V prior keeps its σ. "
|
|
328
|
+
"all — every node (default); generation — only the slack and the nodes "
|
|
329
|
+
"with generation (exist_gen or an active generator), which can hold their "
|
|
330
|
+
"voltage. Elsewhere the prior is loosened (σ × 100) and stays for "
|
|
331
|
+
"observability only: a prior at the nominal V on a load node pulls its "
|
|
332
|
+
"estimate down and the solver makes up the difference with the load Q. "
|
|
333
|
+
"none — every pseudo-V prior is loosened, the slack and the generation "
|
|
334
|
+
"nodes included: V is held by the network, the measurements and the "
|
|
335
|
+
"voltage limits. Meant for ipm/ipm_pd (their barrier keeps V within the "
|
|
336
|
+
"limits); with WLS V of an unmeasured area rests on the loosened prior.",
|
|
337
|
+
)
|
|
316
338
|
unobservable_v_sigma_frac: float = _param(
|
|
317
339
|
0.02,
|
|
318
340
|
group=_G_PSEUDO,
|
|
@@ -502,7 +524,7 @@ class PipelineConfig:
|
|
|
502
524
|
control="number",
|
|
503
525
|
min=1e-4,
|
|
504
526
|
max=1e6,
|
|
505
|
-
depends={"algorithm":
|
|
527
|
+
depends={"algorithm": _IPM_SOLVERS},
|
|
506
528
|
help="Меньше → balance мягче. default 0.1. Soft balance only (ipm_balance_mode=soft).",
|
|
507
529
|
)
|
|
508
530
|
ipm_balance_mode: str = _param(
|
|
@@ -511,7 +533,7 @@ class PipelineConfig:
|
|
|
511
533
|
label="Balance mode",
|
|
512
534
|
control="select",
|
|
513
535
|
choices=["soft", "hard"],
|
|
514
|
-
depends={"algorithm":
|
|
536
|
+
depends={"algorithm": _IPM_SOLVERS},
|
|
515
537
|
help="soft — the nodal P/Q balance is a weighted pseudo-measurement "
|
|
516
538
|
"(ipm_balance_weight_factor); hard — an equality constraint enforced with "
|
|
517
539
|
"Lagrange multipliers (ipm: method of multipliers; ipm_pd: regularised KKT) "
|
|
@@ -552,6 +574,16 @@ class PipelineConfig:
|
|
|
552
574
|
"min_value/max_value set is kept within [min_value, max_value] as an "
|
|
553
575
|
"inequality of the interior-point problem. Default OFF.",
|
|
554
576
|
)
|
|
577
|
+
ipm_voltage_guard: bool = _toggle(
|
|
578
|
+
True,
|
|
579
|
+
group=_G_IPM,
|
|
580
|
+
label="Voltage guard (ipm_pd)",
|
|
581
|
+
help="ipm_pd only (ignored otherwise): every node V is kept within a box "
|
|
582
|
+
"from its voltage_min/voltage_max (unset or implausible limits fall back "
|
|
583
|
+
"to 0.55/1.8 p.u. of nominal, 0.7..1.5 with neither set), and one step "
|
|
584
|
+
"changes V by at most 20 %, an angle by at most 0.6 rad and a branch "
|
|
585
|
+
"angle difference by at most 0.35 rad. Default ON.",
|
|
586
|
+
)
|
|
555
587
|
ipm_injection_roles: bool = _toggle(
|
|
556
588
|
False,
|
|
557
589
|
group=_G_IPM,
|
|
@@ -593,7 +625,7 @@ class PipelineConfig:
|
|
|
593
625
|
control="number",
|
|
594
626
|
min=0.0,
|
|
595
627
|
max=1e6,
|
|
596
|
-
depends={"algorithm":
|
|
628
|
+
depends={"algorithm": _IPM_SOLVERS},
|
|
597
629
|
help="Nodes whose injection range is fully set by their generation/load boxes get a "
|
|
598
630
|
"balance row with variance median(σ²_data) / factor. The box carries the node's "
|
|
599
631
|
"freedom; a loose balance only hides data conflicts as an imbalance. 0 — off.",
|
|
@@ -605,7 +637,7 @@ class PipelineConfig:
|
|
|
605
637
|
control="number",
|
|
606
638
|
min=0.0,
|
|
607
639
|
max=1e6,
|
|
608
|
-
depends={"algorithm":
|
|
640
|
+
depends={"algorithm": _IPM_SOLVERS},
|
|
609
641
|
help="Transit nodes (no exist_gen/exist_load) get a balance row with variance "
|
|
610
642
|
"median(σ²_data) / factor. Their injection is known to be zero; a loose balance "
|
|
611
643
|
"turns data conflicts into a fictitious injection. 0 — off.",
|
|
@@ -616,7 +648,7 @@ class PipelineConfig:
|
|
|
616
648
|
label="Balance mode of transit nodes",
|
|
617
649
|
control="select",
|
|
618
650
|
choices=["soft", "hard"],
|
|
619
|
-
depends={"algorithm":
|
|
651
|
+
depends={"algorithm": _IPM_SOLVERS},
|
|
620
652
|
help="soft — the balance of a transit node (no exist_gen/exist_load) is a "
|
|
621
653
|
"weighted row like the others (ipm_transit_balance_weight_factor); hard — an "
|
|
622
654
|
"equality constraint to ipm_transit_balance_tol_p_mw / _q_mvar, the other "
|
|
@@ -649,7 +681,7 @@ class PipelineConfig:
|
|
|
649
681
|
control="number",
|
|
650
682
|
min=0.0,
|
|
651
683
|
max=10.0,
|
|
652
|
-
depends={"algorithm":
|
|
684
|
+
depends={"algorithm": _IPM_SOLVERS},
|
|
653
685
|
help="default 0.01 p.u.².",
|
|
654
686
|
)
|
|
655
687
|
ipm_trust_region_scope: str = _param(
|
|
@@ -659,7 +691,8 @@ class PipelineConfig:
|
|
|
659
691
|
control="select",
|
|
660
692
|
choices=["all", "state"],
|
|
661
693
|
depends={"algorithm": "ipm"},
|
|
662
|
-
help="
|
|
694
|
+
help="Primal ipm only (with ipm_pd: its fallback solve). "
|
|
695
|
+
"Coordinates whose Newton step is bounded by the trust region (0.5 p.u.). "
|
|
663
696
|
"all — the whole state vector, generation/load box variables included "
|
|
664
697
|
"(the previous default); state (default) — V and δ only: a box variable "
|
|
665
698
|
"that has to move far no longer shrinks the V/δ step (boxes stay protected "
|
|
@@ -26,6 +26,11 @@ def manifest(config: PipelineConfig | type[PipelineConfig] | None = None) -> dic
|
|
|
26
26
|
toggle list), grouped by ``group``. It takes the defaults from here instead
|
|
27
27
|
of hard-coding them.
|
|
28
28
|
|
|
29
|
+
``depends`` maps field names to the values under which the control is shown;
|
|
30
|
+
every entry must match. A scalar value matches by equality, a list matches
|
|
31
|
+
any of its values: ``{"algorithm": ["ipm", "ipm_pd"]}`` shows the control
|
|
32
|
+
for both IPM solvers.
|
|
33
|
+
|
|
29
34
|
``config`` selects the source of defaults and of the iterated fields:
|
|
30
35
|
|
|
31
36
|
* ``None`` (default) -- ``PipelineConfig()``, the library defaults;
|
|
@@ -61,6 +61,7 @@ def _ipm_kwargs(cfg: PipelineConfig) -> dict:
|
|
|
61
61
|
"balance_tol_q_mvar": cfg.ipm_balance_tol_q_mvar,
|
|
62
62
|
"fixed_injection_from_input": cfg.ipm_fixed_injection_from_input,
|
|
63
63
|
"measurement_bounds": cfg.ipm_measurement_bounds,
|
|
64
|
+
"voltage_guard": cfg.ipm_voltage_guard,
|
|
64
65
|
"injection_roles": cfg.ipm_injection_roles,
|
|
65
66
|
"hint_iterations": cfg.ipm_hint_iterations,
|
|
66
67
|
"huber_threshold": cfg.ipm_huber_threshold,
|
|
@@ -236,6 +236,7 @@ def _s_add_pseudo(ctx: _Ctx) -> dict:
|
|
|
236
236
|
unmeasured_gen_p_nodes=ctx.released_injections.get("P", frozenset()),
|
|
237
237
|
unmeasured_gen_q_nodes=ctx.released_injections.get("Q", frozenset()),
|
|
238
238
|
loosen_unknown_load=ctx.cfg.loosen_unknown_load_pseudo,
|
|
239
|
+
voltage_scope=ctx.cfg.pseudo_voltage_scope,
|
|
239
240
|
)
|
|
240
241
|
or {}
|
|
241
242
|
)
|
|
@@ -5,7 +5,10 @@ unobservable, so the gain matrix becomes singular. To avoid this, for every
|
|
|
5
5
|
active node without a `V/P_inj/Q_inj` measurement we add a weak prior:
|
|
6
6
|
|
|
7
7
|
* `V ≈ V_nominal` (either `V_magnitude` from the loaded model, or `vzd`
|
|
8
|
-
for a PV node) with σ=10% of Vnom (slack 0.1%, PV 5%).
|
|
8
|
+
for a PV node) with σ=10% of Vnom (slack 0.1%, PV 5%). With
|
|
9
|
+
``voltage_scope="generation"`` the V prior of a node that cannot hold its
|
|
10
|
+
voltage (no generation, not the slack) is loosened to an observability prior;
|
|
11
|
+
with ``voltage_scope="none"`` every V prior is.
|
|
9
12
|
* `P_inj = pg - pn`, `Q_inj = qg - qn` from the node table (for transit
|
|
10
13
|
nodes this gives 0). σ depends on the node type and empty-model detection.
|
|
11
14
|
"""
|
|
@@ -13,7 +16,7 @@ active node without a `V/P_inj/Q_inj` measurement we add a weak prior:
|
|
|
13
16
|
from __future__ import annotations
|
|
14
17
|
|
|
15
18
|
from dataclasses import dataclass
|
|
16
|
-
from typing import TYPE_CHECKING
|
|
19
|
+
from typing import TYPE_CHECKING, Literal
|
|
17
20
|
|
|
18
21
|
import numpy as np
|
|
19
22
|
|
|
@@ -70,6 +73,11 @@ class PseudoMeasConfig:
|
|
|
70
73
|
# Loosen the P/Q injection prior of a node with load but no load data (no
|
|
71
74
|
# box, nothing materialized) when the injection is fixed by measurements.
|
|
72
75
|
loosen_unknown_load: bool = False
|
|
76
|
+
# Nodes whose V prior keeps its σ (generation and slack nodes, or none);
|
|
77
|
+
# ``None`` = every node. Outside it the V prior variance is multiplied by
|
|
78
|
+
# ``v_out_of_scope_loose_factor``.
|
|
79
|
+
v_scope_nodes: frozenset[int] | None = None
|
|
80
|
+
v_out_of_scope_loose_factor: float = 1e4
|
|
73
81
|
|
|
74
82
|
|
|
75
83
|
def _generation_range_variance(row: np.void, lo_col: str, hi_col: str, fallback: float) -> float:
|
|
@@ -216,6 +224,7 @@ def _build_pseudo_rows(
|
|
|
216
224
|
n_zinj_added = 0
|
|
217
225
|
n_unobs_v = 0
|
|
218
226
|
n_unknown_load = 0
|
|
227
|
+
n_v_loose = 0
|
|
219
228
|
|
|
220
229
|
new_rows: list[dict] = []
|
|
221
230
|
for row in nodes_arr:
|
|
@@ -261,6 +270,11 @@ def _build_pseudo_rows(
|
|
|
261
270
|
# nodes (degree=1, empty-model P_inj σ=100) 10% allowed the SE
|
|
262
271
|
# to drag V down to 0.3-0.5 p.u.: the pseudo-V did not hold the anchor.
|
|
263
272
|
sigma = (0.05 * vn) ** 2
|
|
273
|
+
if config.v_scope_nodes is not None and nid not in config.v_scope_nodes:
|
|
274
|
+
# Outside the scope a prior at the nominal or input V would pull
|
|
275
|
+
# the estimate there; keep it for observability only.
|
|
276
|
+
sigma *= config.v_out_of_scope_loose_factor
|
|
277
|
+
n_v_loose += 1
|
|
264
278
|
new_rows.append(
|
|
265
279
|
pseudo_node_measurement(ids.next(), nid, KIND_VOLTAGE, v_prior_value, float(sigma))
|
|
266
280
|
)
|
|
@@ -335,6 +349,8 @@ def _build_pseudo_rows(
|
|
|
335
349
|
"unobservable_v_tight": n_unobs_v,
|
|
336
350
|
"unknown_load_loose": n_unknown_load,
|
|
337
351
|
}
|
|
352
|
+
if config.v_scope_nodes is not None:
|
|
353
|
+
stats["v_priors_loose"] = n_v_loose
|
|
338
354
|
return new_rows, stats
|
|
339
355
|
|
|
340
356
|
|
|
@@ -411,6 +427,8 @@ def add_pseudo_measurements(
|
|
|
411
427
|
unmeasured_gen_p_nodes: frozenset[int] = frozenset(),
|
|
412
428
|
unmeasured_gen_q_nodes: frozenset[int] = frozenset(),
|
|
413
429
|
loosen_unknown_load: bool = False,
|
|
430
|
+
voltage_scope: Literal["all", "generation", "none"] = "all",
|
|
431
|
+
v_out_of_scope_loose_factor: float = 1e4,
|
|
414
432
|
) -> dict:
|
|
415
433
|
"""Augment the model with pseudo-measurements to remove under-observability.
|
|
416
434
|
|
|
@@ -480,10 +498,26 @@ def add_pseudo_measurements(
|
|
|
480
498
|
at both ends of every incident branch) that prior's variance is
|
|
481
499
|
multiplied by ``boundary_inj_loose_factor``. Typical nodes: cut ends of
|
|
482
500
|
lines leaving the model and substations of neighbouring systems.
|
|
501
|
+
voltage_scope: nodes whose V prior keeps its σ. ``"all"`` (default):
|
|
502
|
+
every node. ``"generation"``: only the nodes that can hold their
|
|
503
|
+
voltage, i.e. the slack and the nodes with generation (``exist_gen``
|
|
504
|
+
or an active generator); elsewhere the V prior variance is multiplied
|
|
505
|
+
by ``v_out_of_scope_loose_factor``. A V prior at the nominal (or
|
|
506
|
+
input) V on a load node without a V measurement pulls the estimate
|
|
507
|
+
there, and the solver makes up the difference with the load Q.
|
|
508
|
+
``"none"``: every V prior is loosened, the slack and the generation
|
|
509
|
+
nodes included; V is then held by the network, the measurements and
|
|
510
|
+
the voltage limits. Meant for ``ipm`` / ``ipm_pd``, whose barrier
|
|
511
|
+
keeps V within the limits; WLS has no such bound, and V of a node seen
|
|
512
|
+
by no measurement rests on the loosened prior alone.
|
|
513
|
+
v_out_of_scope_loose_factor: variance multiplier of the V prior outside
|
|
514
|
+
``voltage_scope``. Default 1e4 (σ × 100) keeps the prior in the
|
|
515
|
+
system only for observability.
|
|
483
516
|
|
|
484
517
|
Returns:
|
|
485
518
|
``{"v_priors_added": N, "zero_inj_added": N, "unobservable_v_tight": N,
|
|
486
|
-
"unknown_load_loose": N}`` — counters
|
|
519
|
+
"unknown_load_loose": N}`` — counters, plus ``"v_priors_loose": N`` with
|
|
520
|
+
``voltage_scope`` other than ``"all"``.
|
|
487
521
|
"""
|
|
488
522
|
nodes_arr = model.nodes.to_numpy()
|
|
489
523
|
meas_arr = model.measurements.to_numpy()
|
|
@@ -496,6 +530,21 @@ def add_pseudo_measurements(
|
|
|
496
530
|
):
|
|
497
531
|
node_load_props = extract_node_load_props_from_model(model)
|
|
498
532
|
|
|
533
|
+
if voltage_scope not in ("all", "generation", "none"):
|
|
534
|
+
raise ValueError(
|
|
535
|
+
f"voltage_scope={voltage_scope!r} is not one of ['all', 'generation', 'none']"
|
|
536
|
+
)
|
|
537
|
+
v_scope_nodes: frozenset[int] | None = None
|
|
538
|
+
if voltage_scope == "generation":
|
|
539
|
+
gen = model.generators.to_numpy()
|
|
540
|
+
holds_v = nodes_arr["exist_gen"].astype(bool)
|
|
541
|
+
holds_v |= nodes_arr["node_type"] == int(NodeType.SLACK)
|
|
542
|
+
v_scope_nodes = frozenset(
|
|
543
|
+
nodes_arr["id"][holds_v].tolist() + gen["node_id"][gen["status"].astype(bool)].tolist()
|
|
544
|
+
)
|
|
545
|
+
elif voltage_scope == "none":
|
|
546
|
+
v_scope_nodes = frozenset()
|
|
547
|
+
|
|
499
548
|
config = PseudoMeasConfig(
|
|
500
549
|
add_zero_injections=add_zero_injections,
|
|
501
550
|
slack_voltage_sigma_frac=slack_voltage_sigma_frac,
|
|
@@ -510,6 +559,8 @@ def add_pseudo_measurements(
|
|
|
510
559
|
unmeasured_gen_p_nodes=frozenset(unmeasured_gen_p_nodes),
|
|
511
560
|
unmeasured_gen_q_nodes=frozenset(unmeasured_gen_q_nodes),
|
|
512
561
|
loosen_unknown_load=loosen_unknown_load,
|
|
562
|
+
v_scope_nodes=v_scope_nodes,
|
|
563
|
+
v_out_of_scope_loose_factor=v_out_of_scope_loose_factor,
|
|
513
564
|
)
|
|
514
565
|
|
|
515
566
|
new_rows, stats = _add_pseudo_measurements_on_arrays(
|