gridstate 2.3.0__tar.gz → 2.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gridstate-2.3.0 → gridstate-2.4.0}/CHANGELOG.md +21 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/PKG-INFO +1 -1
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/_version.py +3 -3
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/api.py +33 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/contract/__init__.py +2 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/contract/tables.py +39 -1
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/contract/validate.py +25 -1
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/contract/version.py +3 -1
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/ipm_hints.py +16 -43
- gridstate-2.4.0/gridstate/load_characteristics.py +99 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/pipeline/config.py +24 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/pipeline/passes.py +1 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/pipeline/preparation.py +1 -1
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/post_processing.py +34 -0
- gridstate-2.4.0/gridstate/solution_hints.py +171 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/telemetry/shunts.py +25 -12
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/working.py +19 -5
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate.egg-info/PKG-INFO +1 -1
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate.egg-info/SOURCES.txt +4 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate.egg-info/scm_file_list.json +4 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate.egg-info/scm_version.json +2 -2
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_breaker_reactance.py +13 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_data/golden/case118.json +6 -6
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_data/golden/case118.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_data/golden/case118_degraded.json +6 -6
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_data/golden/case118_degraded.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_data/golden/case14.json +6 -6
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_data/golden/case14.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_data/golden/case14_degraded.json +6 -6
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_data/golden/case14_degraded.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_data/golden/case30.json +6 -6
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_data/golden/case30.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_data/golden/case30_degraded.json +6 -6
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_data/golden/case30_degraded.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_data/golden/manifest.json +75 -12
- gridstate-2.4.0/tests/test_load_characteristic_segments.py +159 -0
- gridstate-2.4.0/tests/test_solution_hints.py +40 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/.github/workflows/ci.yml +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/.github/workflows/release.yml +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/.gitignore +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/.pre-commit-config.yaml +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/LICENSE +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/Makefile +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/README.md +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/build_cython.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/__init__.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/__pyinstaller/__init__.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/__pyinstaller/hook-gridstate.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/_arrays.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/adapters/__init__.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/adapters/from_pandapower.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/algebra/__init__.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/algebra/_selinv_kernels.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/algebra/base.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/algebra/estimators.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/algebra/jacobian_pattern.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/algebra/normalized_residuals.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/algorithms/__init__.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/algorithms/_gram_numba.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/algorithms/ipm.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/algorithms/ipm_pd.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/algorithms/kkt_solver.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/algorithms/status.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/algorithms/wls.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/bad_data_repass.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/bounds.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/constants.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/contract/_legacy.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/contract/containers.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/contract/derived.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/contract/derived_tables.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/contract/serialize.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/losses.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/measurement_ids.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/pipeline/__init__.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/pipeline/context.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/pipeline/manifest.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/pipeline/orchestration.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/pipeline/runner.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/pipeline/steps.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/preprocessing/__init__.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/preprocessing/_scan.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/preprocessing/ipm_setup.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/preprocessing/meas_rows.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/preprocessing/mirror_voltage.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/preprocessing/node_props.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/preprocessing/pseudo_measurements.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/py.typed +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/quality_summary.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/resolve_pass.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/result.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/runtime.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/shunt_sanity.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/state.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/telemetry/__init__.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/telemetry/_filters.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/telemetry/apply_resolved.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/telemetry/generators.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/telemetry/kinds.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/telemetry/load_only_injection.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/telemetry/loss_filter.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/telemetry/measurements.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/telemetry/noise.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/telemetry/on_line.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/telemetry/quality.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/telemetry/rpn.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/telemetry/sigma_floor.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/telemetry/units.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/telemetry/voltage_filter.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/telemetry/voltage_nominal.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/topology.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/units.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/utils.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/v_mirror.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/v_refine.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/validation/__init__.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/validation/_diagnostics.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/validation/bad_data.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/validation/chi2_test.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/validation/observability.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/ybus.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate/z_vector.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate.egg-info/dependency_links.txt +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate.egg-info/entry_points.txt +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate.egg-info/requires.txt +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/gridstate.egg-info/top_level.txt +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/pyproject.toml +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/setup.cfg +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/__init__.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/_golden.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/conftest.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_adapters_lazy_import.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_aggregate_generators.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_algebra_base.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_anti_overshoot.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_arrays_helpers.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_bad_data.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_bad_data_repass.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_bounds.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_boxed_balance.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_chain_mirror.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_contract.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_contract_edge_cases.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_contract_runtime.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_data/ieee/case118.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_data/ieee/case118_truth.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_data/ieee/case14.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_data/ieee/case14_truth.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_data/ieee/case30.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_data/ieee/case30_truth.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_derived_tables.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_enum_codes.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_flow_sigma_floor.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_from_pandapower.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_generator_shunt_kernels.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_golden.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_gram_numba.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_ieee_cases.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_ipm.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_ipm_balance_modes.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_ipm_hints.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_ipm_injection_roles.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_ipm_integration.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_ipm_pd.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_jacobian_pattern.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_kkt_solver.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_load_only_injection.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_losses_breakdown.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_losses_q_convention.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_losses_trafo_classification.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_materialize_kernel.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_measurement_filter_kernels.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_measurement_ids.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_mirror_voltage_kernel.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_near_critical_ranking.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_neutral_wording.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_noise_model.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_normalized_residuals.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_numpy_only_runtime.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_observability.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_on_line_kernel.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_perf_synthetic_network.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_pipeline.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_pipeline_config_validation.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_pipeline_idempotent.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_pipeline_invariant_errors.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_pipeline_step_order.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_pipeline_warm_start.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_pipeline_working_layer.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_post_processing.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_preparation_steps.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_prepare_network.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_pseudo_unknown_load.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_pseudo_v_unobservable.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_quality_summary_paths.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_reconcile_balance.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_refine_merge.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_resolve_pass.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_result_quality_summary.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_rn_selected_inversion.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_serialize_npz.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_shunt_sanity.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_solve_status.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_state.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_state_range.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_telemetry_apply_kernel.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_telemetry_bounds.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_telemetry_physics_kernels.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_tm_code_quality.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_topology_kernels.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_topology_port.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_transformer_from_shunt.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_units.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_units_array_kernels.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_v_mirror.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_v_refine.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_variance_charging.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_version.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_voltage_calibration_gen_nodes.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_voltage_range_filter_fallback.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_wls.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_wls_robustness.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_working_container.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_ybus.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tests/test_z_vector.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tools/gen_ieee_fixtures.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.0}/tools/golden.py +0 -0
|
@@ -9,6 +9,27 @@ types of the merged pull requests; breaking and behavior-changing items are
|
|
|
9
9
|
listed separately under "Breaking changes" and "Behavior changes". A release is
|
|
10
10
|
cut by tagging the merged main commit; the version is derived from that tag.
|
|
11
11
|
|
|
12
|
+
## v2.4.0 (2026-10-17)
|
|
13
|
+
|
|
14
|
+
### Behavior changes
|
|
15
|
+
|
|
16
|
+
- Breaker branches marked by the input format (R = 0, X = 1 Ohm) get
|
|
17
|
+
X = 110·min_x·Vn Ohm with min_x = 4e-6 (≈ 0.044/Vn p.u.) instead of a fixed
|
|
18
|
+
1e-3 p.u.; `breaker_x_pu` (new) sets a fixed p.u. value instead. Estimates
|
|
19
|
+
move on models carrying the marker (≤ 4.5e-3 p.u. V with `ipm_pd` on the
|
|
20
|
+
test models) (#115)
|
|
21
|
+
- A solved state with V/Vnom below `implausible_v_ratio` (new, default 0.3)
|
|
22
|
+
at any node is marked not converged whatever the solver reports, with a
|
|
23
|
+
WARNING listing the nodes and hints; a non-converged WLS solve logs its
|
|
24
|
+
largest normalized residuals (#114, #115)
|
|
25
|
+
|
|
26
|
+
### Feat
|
|
27
|
+
|
|
28
|
+
- Piecewise static load characteristics: optional input table
|
|
29
|
+
`load_characteristic_segments` (contract 3.2.0) with per-segment
|
|
30
|
+
coefficients up to V⁴ for P and Q; `apply_load_characteristic` uses it in
|
|
31
|
+
place of the quadratic row; without the table nothing changes (#113, #115)
|
|
32
|
+
|
|
12
33
|
## v2.3.0 (2026-10-17)
|
|
13
34
|
|
|
14
35
|
### 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.4.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 4, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g81767e831'
|
|
@@ -48,6 +48,7 @@ from gridstate.quality_summary import (
|
|
|
48
48
|
top_worst_residuals,
|
|
49
49
|
)
|
|
50
50
|
from gridstate.result import MeasurementNormalizedResiduals, SEResult, extract_output_tables
|
|
51
|
+
from gridstate.solution_hints import low_voltage_hints, low_voltage_nodes, residual_hints
|
|
51
52
|
from gridstate.state import (
|
|
52
53
|
InitialPoint,
|
|
53
54
|
StateLayout,
|
|
@@ -100,6 +101,7 @@ def estimate(
|
|
|
100
101
|
quality_summary_rn_refine_below: float = SELINV_REFINE_BELOW,
|
|
101
102
|
quality_summary_near_critical: float = NEAR_CRITICAL_THRESHOLD,
|
|
102
103
|
success_policy: SuccessPolicy = "legacy",
|
|
104
|
+
implausible_v_ratio: float = 0.3,
|
|
103
105
|
**ipm_kwargs: Any,
|
|
104
106
|
) -> SEResult:
|
|
105
107
|
"""Run state estimation for a model and telemetry.
|
|
@@ -181,6 +183,13 @@ def estimate(
|
|
|
181
183
|
:func:`gridstate.algorithms.status.success_from_status`
|
|
182
184
|
(``kkt`` or ``mu_schedule_done`` only). ``solve_status`` and
|
|
183
185
|
``objective_wls`` are reported under both policies.
|
|
186
|
+
implausible_v_ratio: a solved state with ``V/Vnom`` below it at any
|
|
187
|
+
node is not a usable estimate whatever the solver reports (e.g. the
|
|
188
|
+
low-voltage root at a node whose V is barely observable):
|
|
189
|
+
``success`` becomes ``False``, the message names the nodes and a
|
|
190
|
+
WARNING lists hints (:mod:`gridstate.solution_hints`). ``0``
|
|
191
|
+
disables the check. A WLS solve that does not converge logs its
|
|
192
|
+
largest normalized residuals.
|
|
184
193
|
**ipm_kwargs: IPM-mode knobs, ignored for WLS. ``trust_region_scope``
|
|
185
194
|
(``"state"`` default | ``"all"``) selects the coordinates the IPM
|
|
186
195
|
trust region is measured on (see ``_run_ipm``); everything else is
|
|
@@ -310,6 +319,30 @@ def estimate(
|
|
|
310
319
|
)
|
|
311
320
|
if success_policy == "status" and not success:
|
|
312
321
|
message = f"solve status {solve_status.value}: {message}"
|
|
322
|
+
low_v = low_voltage_nodes(v_pu, implausible_v_ratio)
|
|
323
|
+
if low_v.size:
|
|
324
|
+
ids = network_pu.bus_ids[low_v][:5].tolist()
|
|
325
|
+
more = ", ..." if low_v.size > 5 else ""
|
|
326
|
+
note = (
|
|
327
|
+
f"implausible solution: V/Vnom < {implausible_v_ratio:g} at "
|
|
328
|
+
f"{low_v.size} node(s) {ids}{more}"
|
|
329
|
+
)
|
|
330
|
+
success = False
|
|
331
|
+
message = f"{note}; {message}" if message else note
|
|
332
|
+
hints = low_voltage_hints(
|
|
333
|
+
low_v, v_pu=v_pu, network_pu=network_pu, meas_index=meas_index, z=z
|
|
334
|
+
)
|
|
335
|
+
logger.warning("%s %s:\n%s", algorithm, note, "\n".join(hints))
|
|
336
|
+
elif not success and algorithm == "wls" and z.shape[0]:
|
|
337
|
+
algebra = BaseAlgebra(ybus, yf, yt, meas_index, layout, network_pu)
|
|
338
|
+
hints = residual_hints(
|
|
339
|
+
z - algebra.evaluate_h(v_pu, delta),
|
|
340
|
+
np.sqrt(floored_sigma2(r_matrix.diagonal())),
|
|
341
|
+
meas_index=meas_index,
|
|
342
|
+
network_pu=network_pu,
|
|
343
|
+
)
|
|
344
|
+
if hints:
|
|
345
|
+
logger.warning("wls did not converge (%s):\n%s", message, "\n".join(hints))
|
|
313
346
|
write_results_to_model(model, v_pu, delta, network_pu, yf=yf, yt=yt, ybus=ybus)
|
|
314
347
|
# 7. Post-processing of measurements: estimated_si/value/residual.
|
|
315
348
|
write_measurement_estimates(
|
|
@@ -26,6 +26,7 @@ from gridstate.contract.tables import (
|
|
|
26
26
|
BRANCHES,
|
|
27
27
|
BRANCHES_OUTPUT,
|
|
28
28
|
GENERATORS,
|
|
29
|
+
LOAD_CHARACTERISTIC_SEGMENTS,
|
|
29
30
|
LOAD_CHARACTERISTICS,
|
|
30
31
|
MEASUREMENTS,
|
|
31
32
|
MEASUREMENTS_OUTPUT,
|
|
@@ -70,6 +71,7 @@ __all__ = [
|
|
|
70
71
|
"CONTRACT_VERSION_KEY",
|
|
71
72
|
"GENERATORS",
|
|
72
73
|
"LOAD_CHARACTERISTICS",
|
|
74
|
+
"LOAD_CHARACTERISTIC_SEGMENTS",
|
|
73
75
|
"MEASUREMENTS",
|
|
74
76
|
"MEASUREMENTS_OUTPUT",
|
|
75
77
|
"NODES",
|
|
@@ -670,6 +670,41 @@ LOAD_CHARACTERISTICS = TableSchema(
|
|
|
670
670
|
),
|
|
671
671
|
)
|
|
672
672
|
|
|
673
|
+
# Piecewise segments of the static load characteristics, one row per segment.
|
|
674
|
+
# Optional: a (characteristic, quantity) pair with segments replaces its quadratic
|
|
675
|
+
# coefficients of LOAD_CHARACTERISTICS; a pair without segments keeps them.
|
|
676
|
+
LOAD_CHARACTERISTIC_SEGMENTS = TableSchema(
|
|
677
|
+
name="load_characteristic_segments",
|
|
678
|
+
key=("load_model_id", "quantity", "v_lo"),
|
|
679
|
+
doc=(
|
|
680
|
+
"Piecewise P(V)/Q(V) load characteristics: segment k of a (load_model_id, quantity) "
|
|
681
|
+
"pair applies for v_lo[k] < V ≤ v_lo[k+1] (V = |V|/Vnom; a value on a boundary "
|
|
682
|
+
"belongs to the lower segment); the first segment extends to V = 0 whatever its "
|
|
683
|
+
"v_lo. Factor = c0 + c1·V + c2·V² + c3·V³ + c4·V⁴ (1.0 = nominal)."
|
|
684
|
+
),
|
|
685
|
+
columns=(
|
|
686
|
+
ColumnSpec(
|
|
687
|
+
"load_model_id",
|
|
688
|
+
"i4",
|
|
689
|
+
Role.KEY,
|
|
690
|
+
doc="Characteristic (load_characteristics.id, 0-based); must exist there.",
|
|
691
|
+
),
|
|
692
|
+
ColumnSpec("quantity", "i1", Role.KEY, doc="0 = P, 1 = Q."),
|
|
693
|
+
ColumnSpec(
|
|
694
|
+
"v_lo",
|
|
695
|
+
"f8",
|
|
696
|
+
Role.KEY,
|
|
697
|
+
doc="Lower bound of |V|/Vnom; strictly increasing within a pair; "
|
|
698
|
+
"treated as -inf for the first segment.",
|
|
699
|
+
),
|
|
700
|
+
ColumnSpec("c0", "f8", Role.INPUT, doc="Coefficient of V⁰."),
|
|
701
|
+
ColumnSpec("c1", "f8", Role.INPUT, doc="Coefficient of V¹."),
|
|
702
|
+
ColumnSpec("c2", "f8", Role.INPUT, doc="Coefficient of V²."),
|
|
703
|
+
ColumnSpec("c3", "f8", Role.INPUT, doc="Coefficient of V³."),
|
|
704
|
+
ColumnSpec("c4", "f8", Role.INPUT, doc="Coefficient of V⁴."),
|
|
705
|
+
),
|
|
706
|
+
)
|
|
707
|
+
|
|
673
708
|
# Shunt devices (reactors/capacitor banks), per device. The core aggregates the active ones
|
|
674
709
|
# into node.shunt_g/b for the Y-bus (a numeric step, replacing apply_reactors_to_node_shunt) and
|
|
675
710
|
# in the future switches steps (group_id/step — reserved).
|
|
@@ -708,7 +743,8 @@ SHUNTS = TableSchema(
|
|
|
708
743
|
class SEInputSchema:
|
|
709
744
|
"""Input contract schema: a set of tables (roles KEY/INPUT/WORKING).
|
|
710
745
|
|
|
711
|
-
The domain tables ``tap_steps``/``load_characteristics``/``shunts
|
|
746
|
+
The domain tables ``tap_steps``/``load_characteristics``/``shunts``/
|
|
747
|
+
``load_characteristic_segments`` are
|
|
712
748
|
format-agnostic numeric inputs (tap changer position selection, unit conversion,
|
|
713
749
|
aggregation of shunts are done in an external data source). Input-only
|
|
714
750
|
(roles KEY/INPUT).
|
|
@@ -721,6 +757,7 @@ class SEInputSchema:
|
|
|
721
757
|
tap_steps: TableSchema
|
|
722
758
|
load_characteristics: TableSchema
|
|
723
759
|
shunts: TableSchema
|
|
760
|
+
load_characteristic_segments: TableSchema
|
|
724
761
|
|
|
725
762
|
def tables(self) -> tuple[TableSchema, ...]:
|
|
726
763
|
"""All tables in the order of field declaration (single source — the dataclass)."""
|
|
@@ -748,6 +785,7 @@ SE_INPUT = SEInputSchema(
|
|
|
748
785
|
tap_steps=TAP_STEPS,
|
|
749
786
|
load_characteristics=LOAD_CHARACTERISTICS,
|
|
750
787
|
shunts=SHUNTS,
|
|
788
|
+
load_characteristic_segments=LOAD_CHARACTERISTIC_SEGMENTS,
|
|
751
789
|
)
|
|
752
790
|
|
|
753
791
|
SE_OUTPUT = SEOutputSchema(
|
|
@@ -18,6 +18,7 @@ from typing import Any
|
|
|
18
18
|
|
|
19
19
|
from gridstate.contract.tables import SE_INPUT, Role, SEInputSchema
|
|
20
20
|
from gridstate.contract.version import CONTRACT_VERSION_KEY, check_compatibility
|
|
21
|
+
from gridstate.load_characteristics import segment_problems
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
@dataclass(frozen=True)
|
|
@@ -25,7 +26,7 @@ class ValidationIssue:
|
|
|
25
26
|
"""A single input validation problem."""
|
|
26
27
|
|
|
27
28
|
table: str
|
|
28
|
-
kind: str # "missing_column" | "missing_table" | "version_incompatible"
|
|
29
|
+
kind: str # "missing_column" | "missing_table" | "version_incompatible" | "invalid_rows"
|
|
29
30
|
detail: str
|
|
30
31
|
column: str | None = None
|
|
31
32
|
|
|
@@ -75,6 +76,20 @@ def _available_columns(source: Any, table_name: str) -> set[str] | None:
|
|
|
75
76
|
return set(names or ())
|
|
76
77
|
|
|
77
78
|
|
|
79
|
+
#: Input tables a source may omit (added in a minor contract version).
|
|
80
|
+
_OPTIONAL_TABLES = frozenset({"load_characteristic_segments"})
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _table_array(source: Any, table_name: str) -> Any:
|
|
84
|
+
"""The structured array of table ``table_name`` (``None`` if absent)."""
|
|
85
|
+
if isinstance(source, Mapping):
|
|
86
|
+
return source.get(table_name)
|
|
87
|
+
collection = getattr(source, table_name, None)
|
|
88
|
+
if collection is None or not hasattr(collection, "to_numpy"):
|
|
89
|
+
return None
|
|
90
|
+
return collection.to_numpy()
|
|
91
|
+
|
|
92
|
+
|
|
78
93
|
def _data_version(source: Any, explicit: str | None) -> str | None:
|
|
79
94
|
"""Data contract version: explicit > ``metadata[contract_version]`` > None."""
|
|
80
95
|
if explicit is not None:
|
|
@@ -128,6 +143,8 @@ def validate_input(
|
|
|
128
143
|
for table in schema.tables():
|
|
129
144
|
available = _available_columns(source, table.name)
|
|
130
145
|
if available is None:
|
|
146
|
+
if table.name in _OPTIONAL_TABLES:
|
|
147
|
+
continue
|
|
131
148
|
report.add(
|
|
132
149
|
ValidationIssue(table.name, "missing_table", "table is missing from the input")
|
|
133
150
|
)
|
|
@@ -143,6 +160,13 @@ def validate_input(
|
|
|
143
160
|
)
|
|
144
161
|
)
|
|
145
162
|
|
|
163
|
+
# --- row-level rules of the piecewise load characteristics ---
|
|
164
|
+
segments = _table_array(source, "load_characteristic_segments")
|
|
165
|
+
if segments is not None and len(segments) > 0:
|
|
166
|
+
lc = _table_array(source, "load_characteristics")
|
|
167
|
+
for problem in segment_problems(segments, 0 if lc is None else len(lc)):
|
|
168
|
+
report.add(ValidationIssue("load_characteristic_segments", "invalid_rows", problem))
|
|
169
|
+
|
|
146
170
|
if strict:
|
|
147
171
|
report.raise_if_invalid()
|
|
148
172
|
return report
|
|
@@ -48,7 +48,9 @@ from dataclasses import dataclass
|
|
|
48
48
|
# explicit legacy path ``load_se_input_npz(path, trusted=True)``.
|
|
49
49
|
# 3.1.0 (MINOR): the optional derived table ``telemetry_bounds``
|
|
50
50
|
# (``DerivedInputs.telemetry_bounds``).
|
|
51
|
-
|
|
51
|
+
# 3.2.0 (MINOR): the optional input table ``load_characteristic_segments``
|
|
52
|
+
# (piecewise load characteristics).
|
|
53
|
+
CONTRACT_VERSION = "3.2.0"
|
|
52
54
|
|
|
53
55
|
# Major of the data the explicit legacy path of the ``.npz`` loader still reads
|
|
54
56
|
# (``load_se_input_npz(path, trusted=True)``).
|
|
@@ -25,34 +25,16 @@ import numpy as np
|
|
|
25
25
|
|
|
26
26
|
from gridstate.algorithms.ipm import IPMResult
|
|
27
27
|
from gridstate.preprocessing.ipm_setup import IPMSetup
|
|
28
|
+
from gridstate.solution_hints import KIND_NAMES, describe_row, residual_hints
|
|
28
29
|
from gridstate.units import BASE_MVA, NetworkPU
|
|
29
30
|
from gridstate.z_vector import (
|
|
30
31
|
KIND_BOX_PRIOR_PGEN,
|
|
31
32
|
KIND_BOX_PRIOR_PNAG,
|
|
32
33
|
KIND_BOX_PRIOR_QGEN,
|
|
33
34
|
KIND_BOX_PRIOR_QNAG,
|
|
34
|
-
KIND_NODE_BALANCE_P,
|
|
35
|
-
KIND_NODE_BALANCE_Q,
|
|
36
35
|
)
|
|
37
36
|
|
|
38
37
|
|
|
39
|
-
#: Row kinds by name (``MeasurementType`` plus the rows the IPM setup appends).
|
|
40
|
-
_KIND_NAMES = {
|
|
41
|
-
0: "P flow",
|
|
42
|
-
1: "Q flow",
|
|
43
|
-
2: "V",
|
|
44
|
-
3: "I",
|
|
45
|
-
4: "P injection",
|
|
46
|
-
5: "Q injection",
|
|
47
|
-
KIND_NODE_BALANCE_P: "P balance",
|
|
48
|
-
KIND_NODE_BALANCE_Q: "Q balance",
|
|
49
|
-
KIND_BOX_PRIOR_PGEN: "Pgen",
|
|
50
|
-
KIND_BOX_PRIOR_QGEN: "Qgen",
|
|
51
|
-
KIND_BOX_PRIOR_PNAG: "Pload",
|
|
52
|
-
KIND_BOX_PRIOR_QNAG: "Qload",
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
38
|
def _box_sections(setup: IPMSetup) -> list[tuple[int, int, np.ndarray]]:
|
|
57
39
|
lay = setup.layout
|
|
58
40
|
return [
|
|
@@ -120,7 +102,7 @@ class _Context:
|
|
|
120
102
|
for bkind, _off, _pos in _box_sections(self.setup):
|
|
121
103
|
rows = self.rows(pos, bkind)
|
|
122
104
|
if rows.size and not self.is_role[rows].any():
|
|
123
|
-
free_parts.append(
|
|
105
|
+
free_parts.append(KIND_NAMES[bkind])
|
|
124
106
|
label = f"node {node} {var} ({n_nbr} neighbour node{'s' if n_nbr != 1 else ''})"
|
|
125
107
|
if n_nbr <= 1 and free_parts:
|
|
126
108
|
hint = (
|
|
@@ -135,7 +117,7 @@ class _Context:
|
|
|
135
117
|
lo = float(self.setup.box_lo[k]) * BASE_MVA if k is not None else float("nan")
|
|
136
118
|
hi = float(self.setup.box_hi[k]) * BASE_MVA if k is not None else float("nan")
|
|
137
119
|
measured, sigma = self.box_variable_state(pos, kind)
|
|
138
|
-
name =
|
|
120
|
+
name = KIND_NAMES[kind]
|
|
139
121
|
label = f"node {node} {name} range [{lo:.1f}, {hi:.1f}]"
|
|
140
122
|
if measured:
|
|
141
123
|
label += ", measured"
|
|
@@ -151,17 +133,7 @@ class _Context:
|
|
|
151
133
|
|
|
152
134
|
def describe_row(self, row: int) -> str:
|
|
153
135
|
mi = self.setup.meas_index
|
|
154
|
-
|
|
155
|
-
name = _KIND_NAMES.get(kind, f"kind {kind}")
|
|
156
|
-
okind, opos = int(mi.object_kind[row]), int(mi.object_pos[row])
|
|
157
|
-
if okind == 1:
|
|
158
|
-
side = "from" if int(mi.branch_side[row]) == 0 else "to"
|
|
159
|
-
obj = f"branch {int(self.net.branch_ids[opos])} ({side})"
|
|
160
|
-
else:
|
|
161
|
-
obj = f"node {self.node_id(opos)}"
|
|
162
|
-
mid = int(mi.meas_id[row])
|
|
163
|
-
what = "part measurement" if self.is_role[row] else ("measurement" if mid > 0 else "row")
|
|
164
|
-
return f"{name} {what} {mid} at {obj}" if mid > 0 else f"{name} {what} at {obj}"
|
|
136
|
+
return describe_row(mi, self.net, row, part=bool(self.is_role[row]))
|
|
165
137
|
|
|
166
138
|
|
|
167
139
|
def _top(counts: Mapping[int, int], n: int, floor: float) -> list[tuple[int, int]]:
|
|
@@ -240,17 +212,18 @@ def ipm_convergence_hints(
|
|
|
240
212
|
lines.extend(f" {label}" for _, label in weak[:top])
|
|
241
213
|
|
|
242
214
|
sigma = ctx.sigma
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
215
|
+
lines.extend(
|
|
216
|
+
residual_hints(
|
|
217
|
+
residual,
|
|
218
|
+
sigma,
|
|
219
|
+
meas_index=setup.meas_index,
|
|
220
|
+
network_pu=network_pu,
|
|
221
|
+
# Equality rows: their σ is a penalty, not a measurement error.
|
|
222
|
+
skip=setup.balance_rows if setup.balance_tol is not None else None,
|
|
223
|
+
part_rows=setup.role_rows,
|
|
224
|
+
top=top,
|
|
225
|
+
)
|
|
226
|
+
)
|
|
254
227
|
|
|
255
228
|
outside: list[tuple[float, str]] = []
|
|
256
229
|
for row in setup.role_rows.tolist():
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Piecewise static load characteristics (``load_characteristic_segments``).
|
|
2
|
+
|
|
3
|
+
A characteristic may define, separately for P and Q (``quantity`` 0 / 1), a set
|
|
4
|
+
of segments: segment ``k`` applies for ``v_lo[k] < V ≤ v_lo[k+1]``
|
|
5
|
+
(``V = |V|/Vnom``; a value on a boundary belongs to the lower segment), the
|
|
6
|
+
first one extends to ``V = 0`` whatever its ``v_lo``. The load factor on a
|
|
7
|
+
segment is ``c0 + c1·V + c2·V² + c3·V³ + c4·V⁴`` (1.0 = nominal).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from itertools import pairwise
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
#: ``quantity`` codes of ``load_characteristic_segments``.
|
|
18
|
+
QUANTITY_P = 0
|
|
19
|
+
QUANTITY_Q = 1
|
|
20
|
+
|
|
21
|
+
#: ``(load_model_id, quantity) → (boundaries, coefficients)``: the upper
|
|
22
|
+
#: boundaries of all segments but the last (``v_lo[1:]``, increasing) and the
|
|
23
|
+
#: ``(n_segments, 5)`` coefficient matrix.
|
|
24
|
+
SegmentPlan = dict[tuple[int, int], tuple[np.ndarray, np.ndarray]]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def segment_problems(segments: np.ndarray, n_models: int) -> list[str]:
|
|
28
|
+
"""Contract violations of a ``load_characteristic_segments`` table (empty = valid).
|
|
29
|
+
|
|
30
|
+
``n_models`` is the number of rows of ``load_characteristics`` (ids are
|
|
31
|
+
0-based positions).
|
|
32
|
+
"""
|
|
33
|
+
problems: list[str] = []
|
|
34
|
+
if len(segments) == 0:
|
|
35
|
+
return problems
|
|
36
|
+
ids = np.asarray(segments["load_model_id"], dtype=np.int64)
|
|
37
|
+
qty = np.asarray(segments["quantity"], dtype=np.int64)
|
|
38
|
+
v_lo = np.asarray(segments["v_lo"], dtype=np.float64)
|
|
39
|
+
unknown = sorted(set(ids[(ids < 0) | (ids >= n_models)].tolist()))
|
|
40
|
+
if unknown:
|
|
41
|
+
problems.append(f"load_model_id not in load_characteristics: {unknown}")
|
|
42
|
+
bad_q = sorted(set(qty[(qty != QUANTITY_P) & (qty != QUANTITY_Q)].tolist()))
|
|
43
|
+
if bad_q:
|
|
44
|
+
problems.append(f"quantity must be 0 (P) or 1 (Q), got {bad_q}")
|
|
45
|
+
coeffs = np.column_stack([np.asarray(segments[f"c{k}"], dtype=np.float64) for k in range(5)])
|
|
46
|
+
if not np.all(np.isfinite(coeffs)):
|
|
47
|
+
problems.append("non-finite coefficients")
|
|
48
|
+
order = np.lexsort((v_lo, qty, ids))
|
|
49
|
+
for a, b in pairwise(order):
|
|
50
|
+
# The first segment's v_lo is ignored, so only later bounds must be
|
|
51
|
+
# finite; equal bounds make an empty segment.
|
|
52
|
+
same_pair = ids[a] == ids[b] and qty[a] == qty[b]
|
|
53
|
+
if same_pair and (not np.isfinite(v_lo[b]) or v_lo[b] <= v_lo[a]):
|
|
54
|
+
problems.append(
|
|
55
|
+
f"v_lo must be finite and strictly increasing within "
|
|
56
|
+
f"(load_model_id={ids[a]}, quantity={qty[a]})"
|
|
57
|
+
)
|
|
58
|
+
return problems
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def segment_plan(segments: np.ndarray, n_models: int) -> SegmentPlan:
|
|
62
|
+
"""Group the segments by ``(load_model_id, quantity)``.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: the table violates the contract (see :func:`segment_problems`).
|
|
66
|
+
"""
|
|
67
|
+
problems = segment_problems(segments, n_models)
|
|
68
|
+
if problems:
|
|
69
|
+
raise ValueError("invalid load_characteristic_segments: " + "; ".join(problems))
|
|
70
|
+
plan: SegmentPlan = {}
|
|
71
|
+
if len(segments) == 0:
|
|
72
|
+
return plan
|
|
73
|
+
ids = np.asarray(segments["load_model_id"], dtype=np.int64)
|
|
74
|
+
qty = np.asarray(segments["quantity"], dtype=np.int64)
|
|
75
|
+
v_lo = np.asarray(segments["v_lo"], dtype=np.float64)
|
|
76
|
+
coeffs = np.column_stack([np.asarray(segments[f"c{k}"], dtype=np.float64) for k in range(5)])
|
|
77
|
+
order = np.lexsort((v_lo, qty, ids))
|
|
78
|
+
for key in sorted(set(zip(ids.tolist(), qty.tolist(), strict=True))):
|
|
79
|
+
rows = order[(ids[order] == key[0]) & (qty[order] == key[1])]
|
|
80
|
+
plan[key] = (v_lo[rows][1:].copy(), coeffs[rows].copy())
|
|
81
|
+
return plan
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def segment_factor(bounds: np.ndarray, coeffs: np.ndarray, v: float) -> float:
|
|
85
|
+
"""Load factor at ``v`` (``bounds`` / ``coeffs`` of one plan entry)."""
|
|
86
|
+
# Number of upper bounds strictly below v: on a boundary, the lower segment.
|
|
87
|
+
k = int(np.searchsorted(bounds, v, side="left"))
|
|
88
|
+
c = coeffs[k]
|
|
89
|
+
return float(c[0] + v * (c[1] + v * (c[2] + v * (c[3] + v * c[4]))))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
__all__ = [
|
|
93
|
+
"QUANTITY_P",
|
|
94
|
+
"QUANTITY_Q",
|
|
95
|
+
"SegmentPlan",
|
|
96
|
+
"segment_factor",
|
|
97
|
+
"segment_plan",
|
|
98
|
+
"segment_problems",
|
|
99
|
+
]
|
|
@@ -94,6 +94,19 @@ class PipelineConfig:
|
|
|
94
94
|
label="Нормализация X короткозамыкателей",
|
|
95
95
|
help="volt-aware X_pu для R=X=0 ветвей (убирает β-выбросы dV_max на LV).",
|
|
96
96
|
)
|
|
97
|
+
breaker_x_pu: float | None = _param(
|
|
98
|
+
None,
|
|
99
|
+
group=_G_INPUT,
|
|
100
|
+
label="Breaker X, p.u.",
|
|
101
|
+
control="number",
|
|
102
|
+
min=1e-6,
|
|
103
|
+
max=1e-1,
|
|
104
|
+
depends={"normalize_breakers": True},
|
|
105
|
+
help="X of a breaker branch given by the input format's marker (R=0, X=1 Ohm). "
|
|
106
|
+
"Unset (default): X = 110·min_x·Vnom Ohm with min_x = 4e-6 (≈0.044/Vnom p.u.: "
|
|
107
|
+
"8.8e-5 at 500 kV, 4e-4 at 110 kV). A number: that X in p.u. on any voltage "
|
|
108
|
+
"class. Breaker branches with any other X are taken from the model as they are.",
|
|
109
|
+
)
|
|
97
110
|
apply_voltage_nominal: bool = _toggle(
|
|
98
111
|
False,
|
|
99
112
|
group=_G_INPUT,
|
|
@@ -372,6 +385,17 @@ class PipelineConfig:
|
|
|
372
385
|
max=500,
|
|
373
386
|
help="default региональной модели 80.",
|
|
374
387
|
)
|
|
388
|
+
implausible_v_ratio: float = _param(
|
|
389
|
+
0.3,
|
|
390
|
+
group=_G_EST,
|
|
391
|
+
label="Implausible V/Vnom",
|
|
392
|
+
control="number",
|
|
393
|
+
min=0.0,
|
|
394
|
+
max=0.9,
|
|
395
|
+
help="A solved state with V/Vnom below this at any node is marked not converged "
|
|
396
|
+
"and logged with hints (e.g. the low-voltage root at a node whose V is barely "
|
|
397
|
+
"observable). 0 disables the check.",
|
|
398
|
+
)
|
|
375
399
|
kkt_solver: str = _param(
|
|
376
400
|
"auto",
|
|
377
401
|
group=_G_EST,
|
|
@@ -84,6 +84,7 @@ def _estimate_kwargs(cfg: PipelineConfig, *, init: str, **overrides: Any) -> dic
|
|
|
84
84
|
"reconcile_balance": cfg.reconcile_balance,
|
|
85
85
|
"reconcile_respect_bounds": cfg.reconcile_respect_bounds,
|
|
86
86
|
"success_policy": cfg.success_policy,
|
|
87
|
+
"implausible_v_ratio": cfg.implausible_v_ratio,
|
|
87
88
|
}
|
|
88
89
|
kw.update(overrides)
|
|
89
90
|
if cfg.algorithm in ("ipm", "ipm_pd"):
|
|
@@ -46,7 +46,7 @@ from gridstate.topology import (
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def _s_normalize_breakers(ctx: _Ctx) -> dict:
|
|
49
|
-
return dict(normalize_breaker_reactance(ctx.model) or {})
|
|
49
|
+
return dict(normalize_breaker_reactance(ctx.model, eps_pu=ctx.cfg.breaker_x_pu) or {})
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
def _s_voltage_nominal(ctx: _Ctx) -> dict:
|
|
@@ -34,6 +34,7 @@ from gridstate.algebra.base import (
|
|
|
34
34
|
BaseAlgebra,
|
|
35
35
|
)
|
|
36
36
|
from gridstate.bounds import is_reactive_only_generation, resolve_bounds
|
|
37
|
+
from gridstate.load_characteristics import QUANTITY_P, QUANTITY_Q, segment_factor, segment_plan
|
|
37
38
|
from gridstate.measurement_ids import ANTI_OVERSHOOT_ID_BASE, IdAllocator
|
|
38
39
|
from gridstate.preprocessing.meas_rows import pseudo_node_measurement
|
|
39
40
|
from gridstate.resolve_pass import (
|
|
@@ -618,6 +619,14 @@ def apply_load_characteristic(model: Working) -> dict[str, int]:
|
|
|
618
619
|
no binding). For PQ-const models (a0=1, a1=a2=0, b0=1, b1=b2=0) the recomputation
|
|
619
620
|
yields exactly ``load_*_nominal``.
|
|
620
621
|
|
|
622
|
+
A characteristic with rows in ``load_characteristic_segments`` for P (resp.
|
|
623
|
+
Q) is piecewise: the segment containing ``V_pu`` (a value on a boundary
|
|
624
|
+
belongs to the lower segment) replaces the quadratic P (resp. Q) row, with
|
|
625
|
+
a polynomial up to ``V_pu⁴`` (see :mod:`gridstate.load_characteristics`).
|
|
626
|
+
A piecewise P (Q) is applied only where the nominal ``load_p`` (``load_q``)
|
|
627
|
+
is positive; elsewhere the estimate is kept. The quadratic rows keep their
|
|
628
|
+
previous behaviour.
|
|
629
|
+
|
|
621
630
|
Applied **only in the WLS mode** (see ``gridstate/api.py::estimate``)
|
|
622
631
|
after ``write_node_estimates_from_inj``. The IPM box vars already account for
|
|
623
632
|
V at the final state — no repeated recomputation is needed.
|
|
@@ -637,6 +646,12 @@ def apply_load_characteristic(model: Working) -> dict[str, int]:
|
|
|
637
646
|
``load_characteristics`` table (a dangling reference).
|
|
638
647
|
* ``no_load_models`` — 1 if the model has no
|
|
639
648
|
``load_characteristics`` table (then zeros are returned).
|
|
649
|
+
* ``piecewise_p`` / ``piecewise_q`` — loads recomputed through a
|
|
650
|
+
piecewise P / Q characteristic.
|
|
651
|
+
|
|
652
|
+
Raises:
|
|
653
|
+
ValueError: ``load_characteristic_segments`` violates the contract
|
|
654
|
+
(unknown ``load_model_id``, bad ``quantity``, non-increasing ``v_lo``).
|
|
640
655
|
"""
|
|
641
656
|
out = {
|
|
642
657
|
"updated": 0,
|
|
@@ -644,6 +659,8 @@ def apply_load_characteristic(model: Working) -> dict[str, int]:
|
|
|
644
659
|
"skipped_no_load": 0,
|
|
645
660
|
"skipped_bad_sxn": 0,
|
|
646
661
|
"no_load_models": 0,
|
|
662
|
+
"piecewise_p": 0,
|
|
663
|
+
"piecewise_q": 0,
|
|
647
664
|
}
|
|
648
665
|
|
|
649
666
|
# The source of the load-characteristic coefficients is the ``load_characteristics`` table; nodes carry
|
|
@@ -662,6 +679,9 @@ def apply_load_characteristic(model: Working) -> dict[str, int]:
|
|
|
662
679
|
b0 = np.asarray(coeff["coeff_q_b0"], dtype=np.float64)
|
|
663
680
|
b1 = np.asarray(coeff["coeff_q_b1"], dtype=np.float64)
|
|
664
681
|
b2 = np.asarray(coeff["coeff_q_b2"], dtype=np.float64)
|
|
682
|
+
seg_coll = getattr(model, "load_characteristic_segments", None)
|
|
683
|
+
seg_arr = seg_coll.to_numpy() if seg_coll is not None else None
|
|
684
|
+
plan = segment_plan(seg_arr, n_lm) if seg_arr is not None else {}
|
|
665
685
|
|
|
666
686
|
for row in nodes_arr:
|
|
667
687
|
if not bool(row["status"]):
|
|
@@ -699,6 +719,20 @@ def apply_load_characteristic(model: Working) -> dict[str, int]:
|
|
|
699
719
|
|
|
700
720
|
load_p_est = load_p_nom * factor_p
|
|
701
721
|
load_q_est = load_q_nom * factor_q
|
|
722
|
+
seg_p = plan.get((idx, QUANTITY_P))
|
|
723
|
+
if seg_p is not None:
|
|
724
|
+
if load_p_nom > 0.0:
|
|
725
|
+
load_p_est = load_p_nom * segment_factor(*seg_p, v_pu)
|
|
726
|
+
out["piecewise_p"] += 1
|
|
727
|
+
else:
|
|
728
|
+
load_p_est = float(row["load_p_estimated"])
|
|
729
|
+
seg_q = plan.get((idx, QUANTITY_Q))
|
|
730
|
+
if seg_q is not None:
|
|
731
|
+
if load_q_nom > 0.0:
|
|
732
|
+
load_q_est = load_q_nom * segment_factor(*seg_q, v_pu)
|
|
733
|
+
out["piecewise_q"] += 1
|
|
734
|
+
else:
|
|
735
|
+
load_q_est = float(row["load_q_estimated"])
|
|
702
736
|
|
|
703
737
|
try:
|
|
704
738
|
model.nodes.update(
|