gridstate 2.4.1__tar.gz → 2.6.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.4.1 → gridstate-2.6.0}/CHANGELOG.md +35 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/PKG-INFO +1 -1
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/_version.py +3 -3
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/api.py +5 -6
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/ipm_hints.py +1 -1
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/pipeline/config.py +55 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/pipeline/passes.py +4 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/pipeline/runner.py +9 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/preprocessing/ipm_setup.py +81 -10
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/solution_hints.py +40 -1
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate.egg-info/PKG-INFO +1 -1
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate.egg-info/scm_version.json +2 -2
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_boxed_balance.py +27 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_ipm_balance_modes.py +67 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_numpy_only_runtime.py +2 -1
- gridstate-2.6.0/tests/test_solution_hints.py +98 -0
- gridstate-2.4.1/tests/test_solution_hints.py +0 -40
- {gridstate-2.4.1 → gridstate-2.6.0}/.github/workflows/ci.yml +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/.github/workflows/release.yml +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/.gitignore +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/.pre-commit-config.yaml +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/LICENSE +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/Makefile +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/README.md +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/build_cython.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/__init__.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/__pyinstaller/__init__.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/__pyinstaller/hook-gridstate.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/_arrays.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/adapters/__init__.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/adapters/from_pandapower.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algebra/__init__.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algebra/_selinv_kernels.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algebra/base.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algebra/estimators.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algebra/jacobian_pattern.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algebra/normalized_residuals.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algorithms/__init__.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algorithms/_gram_numba.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algorithms/ipm.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algorithms/ipm_pd.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algorithms/kkt_solver.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algorithms/status.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/algorithms/wls.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/bad_data_repass.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/bounds.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/constants.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/contract/__init__.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/contract/_legacy.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/contract/containers.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/contract/derived.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/contract/derived_tables.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/contract/serialize.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/contract/tables.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/contract/validate.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/contract/version.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/load_characteristics.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/losses.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/measurement_ids.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/pipeline/__init__.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/pipeline/context.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/pipeline/manifest.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/pipeline/orchestration.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/pipeline/preparation.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/pipeline/steps.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/post_processing.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/preprocessing/__init__.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/preprocessing/_scan.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/preprocessing/meas_rows.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/preprocessing/mirror_voltage.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/preprocessing/node_props.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/preprocessing/pseudo_measurements.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/py.typed +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/quality_summary.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/resolve_pass.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/result.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/runtime.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/shunt_sanity.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/state.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/__init__.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/_filters.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/apply_resolved.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/generators.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/kinds.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/load_only_injection.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/loss_filter.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/measurements.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/noise.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/on_line.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/quality.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/rpn.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/shunts.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/sigma_floor.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/units.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/voltage_filter.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/telemetry/voltage_nominal.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/topology.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/units.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/utils.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/v_mirror.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/v_refine.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/validation/__init__.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/validation/_diagnostics.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/validation/bad_data.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/validation/chi2_test.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/validation/observability.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/working.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/ybus.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate/z_vector.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate.egg-info/SOURCES.txt +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate.egg-info/dependency_links.txt +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate.egg-info/entry_points.txt +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate.egg-info/requires.txt +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate.egg-info/scm_file_list.json +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/gridstate.egg-info/top_level.txt +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/pyproject.toml +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/setup.cfg +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/__init__.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/_golden.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/conftest.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_adapters_lazy_import.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_aggregate_generators.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_algebra_base.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_anti_overshoot.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_arrays_helpers.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_bad_data.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_bad_data_repass.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_bounds.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_breaker_reactance.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_chain_mirror.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_contract.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_contract_edge_cases.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_contract_runtime.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case118.json +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case118.npz +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case118_degraded.json +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case118_degraded.npz +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case14.json +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case14.npz +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case14_degraded.json +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case14_degraded.npz +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case30.json +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case30.npz +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case30_degraded.json +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/case30_degraded.npz +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/golden/manifest.json +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/ieee/case118.npz +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/ieee/case118_truth.npz +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/ieee/case14.npz +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/ieee/case14_truth.npz +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/ieee/case30.npz +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_data/ieee/case30_truth.npz +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_derived_tables.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_enum_codes.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_flow_sigma_floor.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_from_pandapower.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_generator_shunt_kernels.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_golden.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_gram_numba.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_ieee_cases.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_ipm.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_ipm_hints.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_ipm_injection_roles.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_ipm_integration.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_ipm_pd.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_jacobian_pattern.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_kkt_solver.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_load_characteristic_segments.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_load_only_injection.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_losses_breakdown.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_losses_q_convention.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_losses_trafo_classification.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_materialize_kernel.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_measurement_filter_kernels.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_measurement_ids.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_mirror_voltage_kernel.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_near_critical_ranking.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_neutral_wording.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_noise_model.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_normalized_residuals.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_observability.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_on_line_kernel.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_perf_synthetic_network.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_pipeline.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_pipeline_config_validation.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_pipeline_idempotent.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_pipeline_invariant_errors.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_pipeline_step_order.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_pipeline_warm_start.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_pipeline_working_layer.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_post_processing.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_preparation_steps.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_prepare_network.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_pseudo_unknown_load.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_pseudo_v_unobservable.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_quality_summary_paths.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_reconcile_balance.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_refine_merge.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_resolve_pass.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_result_quality_summary.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_rn_selected_inversion.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_serialize_npz.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_shunt_sanity.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_solve_status.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_state.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_state_range.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_telemetry_apply_kernel.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_telemetry_bounds.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_telemetry_physics_kernels.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_tm_code_quality.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_topology_kernels.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_topology_port.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_transformer_from_shunt.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_units.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_units_array_kernels.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_v_mirror.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_v_refine.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_variance_charging.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_version.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_voltage_calibration_gen_nodes.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_voltage_range_filter_fallback.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_wls.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_wls_robustness.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_working_container.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_ybus.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tests/test_z_vector.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tools/gen_ieee_fixtures.py +0 -0
- {gridstate-2.4.1 → gridstate-2.6.0}/tools/golden.py +0 -0
|
@@ -9,6 +9,41 @@ 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.6.0 (2026-10-18)
|
|
13
|
+
|
|
14
|
+
### Behavior changes
|
|
15
|
+
|
|
16
|
+
- `ipm_pd` gets the 300-iteration budget with equality rows too (it was 100
|
|
17
|
+
before the primal fallback), including the global `ipm_balance_mode="hard"`:
|
|
18
|
+
with the adaptive box damping and the measurement-space stopping test of
|
|
19
|
+
v2.4.1 the long budget converges where the short one fell back.
|
|
20
|
+
|
|
21
|
+
### Feat
|
|
22
|
+
|
|
23
|
+
- `ipm_transit_balance_mode` (new, default `soft`): `hard` makes the P and Q
|
|
24
|
+
balance rows of transit nodes (no `exist_gen` / `exist_load`) equality
|
|
25
|
+
constraints of the solver, to `ipm_transit_balance_tol_p_mw` (new, default
|
|
26
|
+
0.01) / `ipm_transit_balance_tol_q_mvar` (new, default 0.1); the other
|
|
27
|
+
balance rows stay soft. The global hard balance overrides it. `IPMSetup`
|
|
28
|
+
gains `eq_rows` / `eq_tol`.
|
|
29
|
+
|
|
30
|
+
## v2.5.0 (2026-10-18)
|
|
31
|
+
|
|
32
|
+
### Behavior changes
|
|
33
|
+
|
|
34
|
+
- After the run, transit nodes (no `exist_gen` / `exist_load`) whose solved
|
|
35
|
+
injection exceeds `transit_injection_warn_mva` (new, default 5 MVA; 0 turns
|
|
36
|
+
it off) are listed in a WARNING: a data conflict, or a generation, load or
|
|
37
|
+
shunt the model does not describe. Estimates are not affected.
|
|
38
|
+
|
|
39
|
+
### Feat
|
|
40
|
+
|
|
41
|
+
- `ipm_transit_balance_weight_factor` (new, default 0 — off): the balance row
|
|
42
|
+
of a transit node gets the variance median(σ²_data) / factor (never looser
|
|
43
|
+
than the soft balance). Their injection is known to be zero; with the
|
|
44
|
+
default soft weight a data conflict spreads over them as a fictitious
|
|
45
|
+
injection.
|
|
46
|
+
|
|
12
47
|
## v2.4.1 (2026-10-18)
|
|
13
48
|
|
|
14
49
|
### 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.6.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 6, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'ge81a73f8f'
|
|
@@ -826,7 +826,7 @@ def _run_ipm(
|
|
|
826
826
|
delta, v, _pg, _qg, _pn, _qn = unpack_full(x, layout_ipm)
|
|
827
827
|
return algebra.evaluate_jacobian(v, delta)
|
|
828
828
|
|
|
829
|
-
eq_idx = setup.
|
|
829
|
+
eq_idx = setup.eq_rows
|
|
830
830
|
pd_result: IPMResult | None = None
|
|
831
831
|
if solver == "ipm_pd":
|
|
832
832
|
mb_rows = mb_lo = mb_hi = None
|
|
@@ -852,13 +852,12 @@ def _run_ipm(
|
|
|
852
852
|
meas_box_lo=mb_lo,
|
|
853
853
|
meas_box_hi=mb_hi,
|
|
854
854
|
eq_idx=eq_idx,
|
|
855
|
-
eq_tol=setup.
|
|
855
|
+
eq_tol=setup.eq_tol,
|
|
856
856
|
mu_init=1.0,
|
|
857
857
|
step_tol=max(tolerance, 1e-4),
|
|
858
858
|
# Weakly determined units need up to ~260 iterations with separate
|
|
859
|
-
# injection parts
|
|
860
|
-
|
|
861
|
-
max_iter=max(max_iterations, 100 if eq_idx is not None else 300),
|
|
859
|
+
# injection parts, with or without equality rows.
|
|
860
|
+
max_iter=max(max_iterations, 300),
|
|
862
861
|
tr_radius=0.5,
|
|
863
862
|
huber_c=huber_c,
|
|
864
863
|
huber_mask=huber_mask,
|
|
@@ -910,7 +909,7 @@ def _run_ipm(
|
|
|
910
909
|
huber_adaptive_k=2.0,
|
|
911
910
|
kkt_solver=kkt,
|
|
912
911
|
eq_idx=eq_idx,
|
|
913
|
-
eq_tol=setup.
|
|
912
|
+
eq_tol=setup.eq_tol,
|
|
914
913
|
)
|
|
915
914
|
if solver == "ipm_pd":
|
|
916
915
|
result.message = f"ipm_pd fallback to ipm ({pd_message}): {result.message}"
|
|
@@ -219,7 +219,7 @@ def ipm_convergence_hints(
|
|
|
219
219
|
meas_index=setup.meas_index,
|
|
220
220
|
network_pu=network_pu,
|
|
221
221
|
# Equality rows: their σ is a penalty, not a measurement error.
|
|
222
|
-
skip=setup.
|
|
222
|
+
skip=setup.eq_rows,
|
|
223
223
|
part_rows=setup.role_rows,
|
|
224
224
|
top=top,
|
|
225
225
|
)
|
|
@@ -396,6 +396,17 @@ class PipelineConfig:
|
|
|
396
396
|
"and logged with hints (e.g. the low-voltage root at a node whose V is barely "
|
|
397
397
|
"observable). 0 disables the check.",
|
|
398
398
|
)
|
|
399
|
+
transit_injection_warn_mva: float = _param(
|
|
400
|
+
5.0,
|
|
401
|
+
group=_G_EST,
|
|
402
|
+
label="Transit injection warning, MVA",
|
|
403
|
+
control="number",
|
|
404
|
+
min=0.0,
|
|
405
|
+
max=1e4,
|
|
406
|
+
help="After the run, transit nodes (no exist_gen/exist_load) whose solved "
|
|
407
|
+
"injection exceeds this are listed in a WARNING: a data conflict or an "
|
|
408
|
+
"undescribed generation, load or shunt. 0 disables the warning.",
|
|
409
|
+
)
|
|
399
410
|
kkt_solver: str = _param(
|
|
400
411
|
"auto",
|
|
401
412
|
group=_G_EST,
|
|
@@ -587,6 +598,50 @@ class PipelineConfig:
|
|
|
587
598
|
"balance row with variance median(σ²_data) / factor. The box carries the node's "
|
|
588
599
|
"freedom; a loose balance only hides data conflicts as an imbalance. 0 — off.",
|
|
589
600
|
)
|
|
601
|
+
ipm_transit_balance_weight_factor: float = _param(
|
|
602
|
+
0.0,
|
|
603
|
+
group=_G_IPM,
|
|
604
|
+
label="Balance weight of transit nodes",
|
|
605
|
+
control="number",
|
|
606
|
+
min=0.0,
|
|
607
|
+
max=1e6,
|
|
608
|
+
depends={"algorithm": "ipm"},
|
|
609
|
+
help="Transit nodes (no exist_gen/exist_load) get a balance row with variance "
|
|
610
|
+
"median(σ²_data) / factor. Their injection is known to be zero; a loose balance "
|
|
611
|
+
"turns data conflicts into a fictitious injection. 0 — off.",
|
|
612
|
+
)
|
|
613
|
+
ipm_transit_balance_mode: str = _param(
|
|
614
|
+
"soft",
|
|
615
|
+
group=_G_IPM,
|
|
616
|
+
label="Balance mode of transit nodes",
|
|
617
|
+
control="select",
|
|
618
|
+
choices=["soft", "hard"],
|
|
619
|
+
depends={"algorithm": "ipm"},
|
|
620
|
+
help="soft — the balance of a transit node (no exist_gen/exist_load) is a "
|
|
621
|
+
"weighted row like the others (ipm_transit_balance_weight_factor); hard — an "
|
|
622
|
+
"equality constraint to ipm_transit_balance_tol_p_mw / _q_mvar, the other "
|
|
623
|
+
"balance rows stay soft. No effect when ipm_balance_mode=hard.",
|
|
624
|
+
)
|
|
625
|
+
ipm_transit_balance_tol_p_mw: float = _param(
|
|
626
|
+
0.01,
|
|
627
|
+
group=_G_IPM,
|
|
628
|
+
label="Transit balance tolerance P, MW",
|
|
629
|
+
control="number",
|
|
630
|
+
min=1e-6,
|
|
631
|
+
max=1e3,
|
|
632
|
+
depends={"ipm_transit_balance_mode": "hard"},
|
|
633
|
+
help="Admissible |P injection| of a transit node under the hard transit balance.",
|
|
634
|
+
)
|
|
635
|
+
ipm_transit_balance_tol_q_mvar: float = _param(
|
|
636
|
+
0.1,
|
|
637
|
+
group=_G_IPM,
|
|
638
|
+
label="Transit balance tolerance Q, MVAr",
|
|
639
|
+
control="number",
|
|
640
|
+
min=1e-6,
|
|
641
|
+
max=1e3,
|
|
642
|
+
depends={"ipm_transit_balance_mode": "hard"},
|
|
643
|
+
help="Admissible |Q injection| of a transit node under the hard transit balance.",
|
|
644
|
+
)
|
|
590
645
|
ipm_prior_sigma2_bus_equiv_pu: float = _param(
|
|
591
646
|
0.01,
|
|
592
647
|
group=_G_IPM,
|
|
@@ -51,6 +51,10 @@ def _ipm_kwargs(cfg: PipelineConfig) -> dict:
|
|
|
51
51
|
"balance_weight_factor": cfg.ipm_balance_weight_factor,
|
|
52
52
|
"prior_sigma2_bus_equiv_pu": cfg.ipm_prior_sigma2_bus_equiv_pu,
|
|
53
53
|
"boxed_balance_weight_factor": cfg.ipm_boxed_balance_weight_factor,
|
|
54
|
+
"transit_balance_weight_factor": cfg.ipm_transit_balance_weight_factor,
|
|
55
|
+
"transit_balance_mode": cfg.ipm_transit_balance_mode,
|
|
56
|
+
"transit_balance_tol_p_mw": cfg.ipm_transit_balance_tol_p_mw,
|
|
57
|
+
"transit_balance_tol_q_mvar": cfg.ipm_transit_balance_tol_q_mvar,
|
|
54
58
|
"trust_region_scope": cfg.ipm_trust_region_scope,
|
|
55
59
|
"balance_mode": cfg.ipm_balance_mode,
|
|
56
60
|
"balance_tol_p_mw": cfg.ipm_balance_tol_p_mw,
|
|
@@ -64,6 +64,7 @@ neither :data:`STEPS` nor the manifest and have no ``step_start``):
|
|
|
64
64
|
|
|
65
65
|
from __future__ import annotations
|
|
66
66
|
|
|
67
|
+
import logging
|
|
67
68
|
import time
|
|
68
69
|
from collections.abc import Callable
|
|
69
70
|
from dataclasses import replace
|
|
@@ -76,10 +77,14 @@ from gridstate.pipeline.context import Step, _Ctx, _no_solution
|
|
|
76
77
|
from gridstate.pipeline.steps import STEPS
|
|
77
78
|
from gridstate.resolve_pass import fail_out_of_range, state_range_violation
|
|
78
79
|
from gridstate.result import SEResult
|
|
80
|
+
from gridstate.solution_hints import transit_injection_hints
|
|
79
81
|
from gridstate.state import InitialPoint
|
|
80
82
|
from gridstate.working import Working
|
|
81
83
|
|
|
82
84
|
|
|
85
|
+
logger = logging.getLogger(__name__)
|
|
86
|
+
|
|
87
|
+
|
|
83
88
|
# Consistency guard between config and derived (the config is used twice): the
|
|
84
89
|
# producer of the plans and the step executor must be gated by ONE config. An
|
|
85
90
|
# enabled step that did not get its plan means the configs diverged (derive
|
|
@@ -369,6 +374,10 @@ def run(
|
|
|
369
374
|
},
|
|
370
375
|
)
|
|
371
376
|
|
|
377
|
+
hints = transit_injection_hints(ctx.model.nodes.to_numpy(), cfg.transit_injection_warn_mva)
|
|
378
|
+
if hints:
|
|
379
|
+
logger.warning("%s", "\n".join(hints))
|
|
380
|
+
|
|
372
381
|
_emit(
|
|
373
382
|
on_event,
|
|
374
383
|
{
|
|
@@ -282,6 +282,11 @@ class IPMSetup:
|
|
|
282
282
|
# ``balance_tol`` holds the admissible |imbalance| per balance row in p.u.
|
|
283
283
|
# ``None`` in the soft mode.
|
|
284
284
|
balance_tol: np.ndarray | None = None
|
|
285
|
+
# Rows the solver enforces as equalities and their admissible |c| (p.u.):
|
|
286
|
+
# every balance row in the hard mode, the transit balance rows with
|
|
287
|
+
# ``transit_balance_mode="hard"``; ``None`` — no equality rows.
|
|
288
|
+
eq_rows: np.ndarray | None = None
|
|
289
|
+
eq_tol: np.ndarray | None = None
|
|
285
290
|
# ``injection_roles``: rows of ``z`` that measure a box variable directly
|
|
286
291
|
# (a generation / load part of a node injection) and their measurement ids;
|
|
287
292
|
# ``role_dropped_ids`` — part rows folded back into a net injection row.
|
|
@@ -511,6 +516,15 @@ def _boxed_balance_masks(
|
|
|
511
516
|
return boxed[0], boxed[1]
|
|
512
517
|
|
|
513
518
|
|
|
519
|
+
def _transit_mask(nodes_arr: np.ndarray, node_ids: list[int]) -> np.ndarray:
|
|
520
|
+
"""Mask over ``node_ids``: transit nodes (neither ``exist_gen`` nor ``exist_load``)."""
|
|
521
|
+
rows = {int(r["id"]): r for r in nodes_arr}
|
|
522
|
+
return np.array(
|
|
523
|
+
[not (bool(rows[nid]["exist_gen"]) or bool(rows[nid]["exist_load"])) for nid in node_ids],
|
|
524
|
+
dtype=bool,
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
|
|
514
528
|
def _median_measurement_sigma2(sigma2: np.ndarray) -> float | None:
|
|
515
529
|
"""Median variance (p.u.²) of the data rows; ``None`` when there are none."""
|
|
516
530
|
positive = sigma2[sigma2 > 1e-15]
|
|
@@ -538,6 +552,7 @@ def build_ipm_setup(
|
|
|
538
552
|
balance_sigma2: float | None = None,
|
|
539
553
|
balance_weight_factor: float = 0.1,
|
|
540
554
|
boxed_balance_weight_factor: float = 0.0,
|
|
555
|
+
transit_balance_weight_factor: float = 0.0,
|
|
541
556
|
default_box_halfwidth_pu: float = 50.0,
|
|
542
557
|
prior_sigma2_normal_pu: float = 0.0,
|
|
543
558
|
prior_sigma2_bus_equiv_pu: float = 0.01,
|
|
@@ -546,6 +561,9 @@ def build_ipm_setup(
|
|
|
546
561
|
hard_balance_rho_factor: float = 1e3,
|
|
547
562
|
balance_tol_p_mw: float = 1.0,
|
|
548
563
|
balance_tol_q_mvar: float = 10.0,
|
|
564
|
+
transit_balance_mode: str = "soft",
|
|
565
|
+
transit_balance_tol_p_mw: float = 0.01,
|
|
566
|
+
transit_balance_tol_q_mvar: float = 0.1,
|
|
549
567
|
fixed_injection_from_input: bool = False,
|
|
550
568
|
injection_roles: bool = False,
|
|
551
569
|
measurements: _ArrayCollection | None = None,
|
|
@@ -576,6 +594,12 @@ def build_ipm_setup(
|
|
|
576
594
|
``median(σ²_data) / boxed_balance_weight_factor`` (never looser than
|
|
577
595
|
the soft one): the box already carries the node's freedom, so a
|
|
578
596
|
loose balance only hides data conflicts as an imbalance. ``0`` — off.
|
|
597
|
+
transit_balance_weight_factor: balance weight for transit nodes (no
|
|
598
|
+
``exist_gen`` / ``exist_load``): their injection is known to be
|
|
599
|
+
zero, so a loose balance turns data conflicts into a fictitious
|
|
600
|
+
injection. Their variance is ``median(σ²_data) /
|
|
601
|
+
transit_balance_weight_factor`` (never looser than the soft one).
|
|
602
|
+
``0`` — off.
|
|
579
603
|
default_box_halfwidth_pu: half-width (p.u.) of the default box
|
|
580
604
|
``[-hw, +hw]`` for an exist_* node with unset bounds.
|
|
581
605
|
Default 50 p.u. (±5 GW/GVAr), deliberately wider than any
|
|
@@ -612,6 +636,14 @@ def build_ipm_setup(
|
|
|
612
636
|
hard_balance_rho_factor: see ``balance_mode``.
|
|
613
637
|
balance_tol_p_mw, balance_tol_q_mvar: admissible |imbalance| of a hard
|
|
614
638
|
P / Q balance row (MW / MVAr).
|
|
639
|
+
transit_balance_mode: ``"soft"`` (default) — the balance rows of transit
|
|
640
|
+
nodes (no ``exist_gen`` / ``exist_load``) are weighted like the
|
|
641
|
+
others (see ``transit_balance_weight_factor``); ``"hard"`` — they are
|
|
642
|
+
equality constraints with the penalty of ``balance_mode="hard"``,
|
|
643
|
+
the other balance rows stay soft. Ignored when ``balance_mode`` is
|
|
644
|
+
already ``"hard"``.
|
|
645
|
+
transit_balance_tol_p_mw, transit_balance_tol_q_mvar: admissible
|
|
646
|
+
|injection| of a transit node in the hard transit mode (MW / MVAr).
|
|
615
647
|
fixed_injection_from_input: the balance row of a node carries the
|
|
616
648
|
input injection of its parts without a box variable (no
|
|
617
649
|
``exist_gen`` / ``exist_load``) as a fixed term instead of zero.
|
|
@@ -716,6 +748,11 @@ def build_ipm_setup(
|
|
|
716
748
|
if balance_mode not in ("soft", "hard"):
|
|
717
749
|
raise ValueError(f"balance_mode={balance_mode!r} is not one of ['soft', 'hard']")
|
|
718
750
|
hard = balance_mode == "hard"
|
|
751
|
+
if transit_balance_mode not in ("soft", "hard"):
|
|
752
|
+
raise ValueError(
|
|
753
|
+
f"transit_balance_mode={transit_balance_mode!r} is not one of ['soft', 'hard']"
|
|
754
|
+
)
|
|
755
|
+
transit_hard = transit_balance_mode == "hard" and not hard and n_balance > 0
|
|
719
756
|
|
|
720
757
|
# ---- Prior measurements: z=0, individual σ². Only for box-vars with σ²>0 ----
|
|
721
758
|
pgen_prior_arr = np.asarray(pgen_sec.prior_s2, dtype=np.float64)
|
|
@@ -790,6 +827,22 @@ def build_ipm_setup(
|
|
|
790
827
|
q_rows = sigma2_new[n_old + n_balance : n_old + 2 * n_balance]
|
|
791
828
|
p_rows[boxed_p] = np.minimum(p_rows[boxed_p], boxed_s2)
|
|
792
829
|
q_rows[boxed_q] = np.minimum(q_rows[boxed_q], boxed_s2)
|
|
830
|
+
if transit_balance_weight_factor > 0.0 and n_balance > 0 and not hard:
|
|
831
|
+
median_sigma2 = _median_measurement_sigma2(sigma2_old)
|
|
832
|
+
if median_sigma2 is not None:
|
|
833
|
+
transit_s2 = min(
|
|
834
|
+
median_sigma2 / float(transit_balance_weight_factor), balance_sigma2_eff
|
|
835
|
+
)
|
|
836
|
+
transit = _transit_mask(nodes_arr, active_ids)
|
|
837
|
+
for start in (n_old, n_old + n_balance):
|
|
838
|
+
rows = sigma2_new[start : start + n_balance]
|
|
839
|
+
rows[transit] = np.minimum(rows[transit], transit_s2)
|
|
840
|
+
transit_eq = np.zeros(0, dtype=np.int64)
|
|
841
|
+
if transit_hard:
|
|
842
|
+
transit = _transit_mask(nodes_arr, active_ids)
|
|
843
|
+
pos = np.flatnonzero(transit)
|
|
844
|
+
transit_eq = np.concatenate([n_old + pos, n_old + n_balance + pos]).astype(np.int64)
|
|
845
|
+
sigma2_new[transit_eq] = _adaptive_balance_sigma2(sigma2_old, hard_balance_rho_factor)
|
|
793
846
|
if n_prior > 0:
|
|
794
847
|
prior_sigmas = np.concatenate(
|
|
795
848
|
[
|
|
@@ -867,6 +920,31 @@ def build_ipm_setup(
|
|
|
867
920
|
meas_id=new_meas_id,
|
|
868
921
|
)
|
|
869
922
|
|
|
923
|
+
balance_tol = (
|
|
924
|
+
np.concatenate(
|
|
925
|
+
[
|
|
926
|
+
np.full(n_balance, balance_tol_p_mw / BASE_MVA),
|
|
927
|
+
np.full(n_balance, balance_tol_q_mvar / BASE_MVA),
|
|
928
|
+
]
|
|
929
|
+
)
|
|
930
|
+
if hard
|
|
931
|
+
else None
|
|
932
|
+
)
|
|
933
|
+
eq_rows: np.ndarray | None = None
|
|
934
|
+
eq_tol: np.ndarray | None = None
|
|
935
|
+
if hard:
|
|
936
|
+
eq_rows = np.arange(n_old, n_old + 2 * n_balance, dtype=np.int64)
|
|
937
|
+
eq_tol = balance_tol
|
|
938
|
+
elif transit_eq.size:
|
|
939
|
+
n_t = transit_eq.size // 2
|
|
940
|
+
eq_rows = transit_eq
|
|
941
|
+
eq_tol = np.concatenate(
|
|
942
|
+
[
|
|
943
|
+
np.full(n_t, transit_balance_tol_p_mw / BASE_MVA),
|
|
944
|
+
np.full(n_t, transit_balance_tol_q_mvar / BASE_MVA),
|
|
945
|
+
]
|
|
946
|
+
)
|
|
947
|
+
|
|
870
948
|
return IPMSetup(
|
|
871
949
|
layout=layout,
|
|
872
950
|
box_idx_in_state=box_idx_arr,
|
|
@@ -881,16 +959,9 @@ def build_ipm_setup(
|
|
|
881
959
|
meas_index=meas_index_aug,
|
|
882
960
|
balance_start=n_old,
|
|
883
961
|
n_balance=n_balance,
|
|
884
|
-
balance_tol=
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
np.full(n_balance, balance_tol_p_mw / BASE_MVA),
|
|
888
|
-
np.full(n_balance, balance_tol_q_mvar / BASE_MVA),
|
|
889
|
-
]
|
|
890
|
-
)
|
|
891
|
-
if hard
|
|
892
|
-
else None
|
|
893
|
-
),
|
|
962
|
+
balance_tol=balance_tol,
|
|
963
|
+
eq_rows=eq_rows,
|
|
964
|
+
eq_tol=eq_tol,
|
|
894
965
|
role_rows=np.arange(role_start, n_new, dtype=np.int64),
|
|
895
966
|
role_meas_ids=role_ids.astype(np.int64),
|
|
896
967
|
role_dropped_ids=np.asarray(
|
|
@@ -6,7 +6,9 @@ wrong. A typical case is a node whose voltage magnitude is barely observable
|
|
|
6
6
|
problem then has a second, low-voltage root, and a change of the solver path
|
|
7
7
|
can land on it while the solver reports success. :func:`low_voltage_nodes`
|
|
8
8
|
finds such nodes; :func:`low_voltage_hints` and :func:`residual_hints` turn
|
|
9
|
-
the solved state into readable lines for the log.
|
|
9
|
+
the solved state into readable lines for the log. :func:`transit_injection_hints`
|
|
10
|
+
lists transit nodes left with an injection, which points at a data conflict or
|
|
11
|
+
at a node whose generation, load or shunt is not described in the model.
|
|
10
12
|
"""
|
|
11
13
|
|
|
12
14
|
from __future__ import annotations
|
|
@@ -162,10 +164,47 @@ def residual_hints(
|
|
|
162
164
|
return lines
|
|
163
165
|
|
|
164
166
|
|
|
167
|
+
def transit_injection_hints(nodes: np.ndarray, threshold_mva: float, *, top: int = 10) -> list[str]:
|
|
168
|
+
"""Transit nodes with a solved injection above ``threshold_mva``, largest first.
|
|
169
|
+
|
|
170
|
+
A transit node (active, neither ``exist_gen`` nor ``exist_load``) injects
|
|
171
|
+
nothing, so ``|p_inj_calc + j·q_inj_calc|`` there is what the estimate
|
|
172
|
+
could not fit elsewhere. ``nodes`` is a solved node table; empty when no
|
|
173
|
+
node exceeds the threshold or ``threshold_mva <= 0``.
|
|
174
|
+
"""
|
|
175
|
+
if threshold_mva <= 0.0 or not len(nodes):
|
|
176
|
+
return []
|
|
177
|
+
transit = (
|
|
178
|
+
nodes["status"].astype(bool)
|
|
179
|
+
& ~nodes["exist_gen"].astype(bool)
|
|
180
|
+
& ~nodes["exist_load"].astype(bool)
|
|
181
|
+
)
|
|
182
|
+
s_mva = np.hypot(nodes["p_inj_calc"], nodes["q_inj_calc"])
|
|
183
|
+
hit = np.flatnonzero(transit & np.isfinite(s_mva) & (s_mva > threshold_mva))
|
|
184
|
+
if not hit.size:
|
|
185
|
+
return []
|
|
186
|
+
order = hit[np.argsort(-s_mva[hit])]
|
|
187
|
+
lines = [
|
|
188
|
+
f"{hit.size} transit node(s) (no exist_gen/exist_load) inject more than "
|
|
189
|
+
f"{threshold_mva:g} MVA; a data conflict or an undescribed generation, load or "
|
|
190
|
+
"shunt at the node:"
|
|
191
|
+
]
|
|
192
|
+
for k in order[:top].tolist():
|
|
193
|
+
lines.append(
|
|
194
|
+
f" node {int(nodes['id'][k])} ({float(nodes['voltage_nominal'][k]):g} kV): "
|
|
195
|
+
f"P = {float(nodes['p_inj_calc'][k]):.1f} MW, "
|
|
196
|
+
f"Q = {float(nodes['q_inj_calc'][k]):.1f} MVAr"
|
|
197
|
+
)
|
|
198
|
+
if hit.size > top:
|
|
199
|
+
lines.append(f" ... and {hit.size - top} more")
|
|
200
|
+
return lines
|
|
201
|
+
|
|
202
|
+
|
|
165
203
|
__all__ = [
|
|
166
204
|
"KIND_NAMES",
|
|
167
205
|
"describe_row",
|
|
168
206
|
"low_voltage_hints",
|
|
169
207
|
"low_voltage_nodes",
|
|
170
208
|
"residual_hints",
|
|
209
|
+
"transit_injection_hints",
|
|
171
210
|
]
|
|
@@ -209,3 +209,30 @@ def test_compensator_active_power_is_not_a_box_part():
|
|
|
209
209
|
p_rows, q_rows = _setup(m, boxed_balance_weight_factor=10.0)
|
|
210
210
|
assert p_rows[2] < SOFT
|
|
211
211
|
assert q_rows[2] < SOFT
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_transit_weight_off_by_default():
|
|
215
|
+
assert PipelineConfig().ipm_transit_balance_weight_factor == 0.0
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_transit_nodes_get_the_weighted_row():
|
|
219
|
+
# Node order: 1 slack (generation box), 2 transit, 3 and 4 loads with a box.
|
|
220
|
+
p_rows, q_rows = _setup(_build_net_with_transit(), transit_balance_weight_factor=100.0)
|
|
221
|
+
tight = 2.5e-4 / 100.0
|
|
222
|
+
assert np.allclose(p_rows, [SOFT, tight, SOFT, SOFT])
|
|
223
|
+
assert np.allclose(q_rows, [SOFT, tight, SOFT, SOFT])
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_transit_row_never_looser_than_the_soft_row():
|
|
227
|
+
p_rows, q_rows = _setup(_build_net_with_transit(), transit_balance_weight_factor=0.01)
|
|
228
|
+
assert np.allclose(p_rows, SOFT)
|
|
229
|
+
assert np.allclose(q_rows, SOFT)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_transit_and_boxed_weights_combine():
|
|
233
|
+
p_rows, _ = _setup(
|
|
234
|
+
_build_net_with_transit(),
|
|
235
|
+
boxed_balance_weight_factor=10.0,
|
|
236
|
+
transit_balance_weight_factor=100.0,
|
|
237
|
+
)
|
|
238
|
+
assert np.allclose(p_rows, [2.5e-5, 2.5e-6, 2.5e-5, 2.5e-5])
|
|
@@ -75,6 +75,52 @@ def test_unknown_balance_mode_is_rejected():
|
|
|
75
75
|
_setup(_build_net_with_transit(), balance_mode="strict")
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
def test_transit_hard_mode_makes_only_transit_rows_equalities():
|
|
79
|
+
# Node order 1..4: node 2 is the transit node.
|
|
80
|
+
setup = _setup(
|
|
81
|
+
_build_net_with_transit(),
|
|
82
|
+
transit_balance_mode="hard",
|
|
83
|
+
transit_balance_tol_p_mw=0.02,
|
|
84
|
+
transit_balance_tol_q_mvar=0.2,
|
|
85
|
+
hard_balance_rho_factor=1e3,
|
|
86
|
+
)
|
|
87
|
+
assert setup.balance_tol is None
|
|
88
|
+
assert setup.eq_rows is not None and setup.eq_tol is not None
|
|
89
|
+
np.testing.assert_array_equal(setup.eq_rows, [2 + 1, 2 + N_BALANCE + 1])
|
|
90
|
+
np.testing.assert_allclose(setup.eq_tol, [0.02 / BASE_MVA, 0.2 / BASE_MVA])
|
|
91
|
+
sigma2 = setup.r_matrix.diagonal()[2 : 2 + 2 * N_BALANCE]
|
|
92
|
+
soft = _setup(_build_net_with_transit()).r_matrix.diagonal()[2 : 2 + 2 * N_BALANCE]
|
|
93
|
+
is_eq = np.zeros(2 * N_BALANCE, dtype=bool)
|
|
94
|
+
is_eq[[1, N_BALANCE + 1]] = True
|
|
95
|
+
assert np.allclose(sigma2[is_eq], 2.5e-4 / 1e3)
|
|
96
|
+
np.testing.assert_array_equal(sigma2[~is_eq], soft[~is_eq])
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_soft_modes_have_no_equality_rows():
|
|
100
|
+
setup = _setup(_build_net_with_transit(), transit_balance_weight_factor=10.0)
|
|
101
|
+
assert setup.eq_rows is None and setup.eq_tol is None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_global_hard_mode_overrides_the_transit_mode():
|
|
105
|
+
setup = _setup(
|
|
106
|
+
_build_net_with_transit(),
|
|
107
|
+
balance_mode="hard",
|
|
108
|
+
balance_tol_p_mw=2.0,
|
|
109
|
+
transit_balance_mode="hard",
|
|
110
|
+
transit_balance_tol_p_mw=0.02,
|
|
111
|
+
)
|
|
112
|
+
assert setup.eq_rows is not None and setup.eq_tol is not None
|
|
113
|
+
np.testing.assert_array_equal(setup.eq_rows, setup.balance_rows)
|
|
114
|
+
np.testing.assert_array_equal(setup.eq_tol, setup.balance_tol)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_unknown_transit_balance_mode_is_rejected():
|
|
118
|
+
with pytest.raises(ValueError, match="transit_balance_mode"):
|
|
119
|
+
_setup(_build_net_with_transit(), transit_balance_mode="strict")
|
|
120
|
+
with pytest.raises(ValueError, match="ipm_transit_balance_mode"):
|
|
121
|
+
PipelineConfig(algorithm="ipm_pd", ipm_transit_balance_mode="strict")
|
|
122
|
+
|
|
123
|
+
|
|
78
124
|
def test_fixed_injection_from_input():
|
|
79
125
|
m = _build_net_with_transit()
|
|
80
126
|
# Transit node 2 gets a non-estimated load and generation.
|
|
@@ -131,6 +177,26 @@ def test_hard_balance_closes_the_estimated_imbalance(algorithm: str):
|
|
|
131
177
|
assert imb_p <= 0.1 and imb_q <= 0.1
|
|
132
178
|
|
|
133
179
|
|
|
180
|
+
@pytest.mark.parametrize("algorithm", ["ipm", "ipm_pd"])
|
|
181
|
+
def test_transit_hard_balance_zeroes_the_transit_injection(algorithm: str):
|
|
182
|
+
# Soft, the transit node 2 absorbs the data conflict (≈ 4 MW / 14 MVAr).
|
|
183
|
+
soft = _build_net_with_transit()
|
|
184
|
+
estimate(soft, algorithm=algorithm, reconcile_balance=False, include_quality_summary=False)
|
|
185
|
+
n_soft = soft.nodes.to_numpy()
|
|
186
|
+
assert abs(n_soft["p_inj_calc"][1]) > 1.0 and abs(n_soft["q_inj_calc"][1]) > 1.0
|
|
187
|
+
m = _build_net_with_transit()
|
|
188
|
+
res = estimate(
|
|
189
|
+
m,
|
|
190
|
+
algorithm=algorithm,
|
|
191
|
+
reconcile_balance=False,
|
|
192
|
+
include_quality_summary=False,
|
|
193
|
+
transit_balance_mode="hard",
|
|
194
|
+
)
|
|
195
|
+
assert res.success
|
|
196
|
+
n = m.nodes.to_numpy()
|
|
197
|
+
assert abs(n["p_inj_calc"][1]) <= 0.01 and abs(n["q_inj_calc"][1]) <= 0.1
|
|
198
|
+
|
|
199
|
+
|
|
134
200
|
def test_ipm_pd_close_to_ipm():
|
|
135
201
|
results = {}
|
|
136
202
|
for algorithm in ("ipm", "ipm_pd"):
|
|
@@ -170,6 +236,7 @@ def test_measurement_bounds_require_ipm_pd():
|
|
|
170
236
|
def test_config_defaults_and_ranges():
|
|
171
237
|
cfg = PipelineConfig(algorithm="ipm_pd")
|
|
172
238
|
assert cfg.ipm_balance_mode == "soft"
|
|
239
|
+
assert cfg.ipm_transit_balance_mode == "soft"
|
|
173
240
|
assert not cfg.ipm_measurement_bounds and not cfg.ipm_fixed_injection_from_input
|
|
174
241
|
# The soft balance weight may now exceed the median measurement weight tenfold.
|
|
175
242
|
dataclasses.replace(cfg, ipm_balance_weight_factor=1e3)
|
|
@@ -152,7 +152,8 @@ _RUN_SRC = textwrap.dedent(
|
|
|
152
152
|
from gridstate.pipeline import run, PipelineConfig
|
|
153
153
|
|
|
154
154
|
working = build_working()
|
|
155
|
-
|
|
155
|
+
# The toy network leaves a transit injection; its WARNING would reach stderr.
|
|
156
|
+
cfg = PipelineConfig(materialize=False, transit_injection_warn_mva=0.0)
|
|
156
157
|
res = run(working, config=cfg)
|
|
157
158
|
vmax = float(np.max(res.v_pu)) if getattr(res, "v_pu", None) is not None and res.v_pu.size else float("nan")
|
|
158
159
|
print(f"RUN_OK success={bool(res.success)} iters={int(res.iterations)} vmax={vmax:.6f}")
|