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