gridstate 2.3.0__tar.gz → 2.4.1__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.1}/CHANGELOG.md +43 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/PKG-INFO +1 -1
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/_version.py +3 -3
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algorithms/ipm_pd.py +48 -8
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/api.py +33 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/contract/__init__.py +2 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/contract/tables.py +39 -1
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/contract/validate.py +25 -1
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/contract/version.py +3 -1
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/ipm_hints.py +16 -43
- gridstate-2.4.1/gridstate/load_characteristics.py +99 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/pipeline/config.py +24 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/pipeline/passes.py +1 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/pipeline/preparation.py +1 -1
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/post_processing.py +34 -0
- gridstate-2.4.1/gridstate/solution_hints.py +171 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/shunts.py +25 -12
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/working.py +19 -5
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate.egg-info/PKG-INFO +1 -1
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate.egg-info/SOURCES.txt +4 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate.egg-info/scm_file_list.json +4 -0
- gridstate-2.4.1/gridstate.egg-info/scm_version.json +8 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_breaker_reactance.py +13 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case118.json +6 -6
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case118.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case118_degraded.json +6 -6
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case118_degraded.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case14.json +6 -6
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case14.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case14_degraded.json +6 -6
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case14_degraded.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case30.json +6 -6
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case30.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case30_degraded.json +6 -6
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/case30_degraded.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/golden/manifest.json +75 -12
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_ipm_pd.py +66 -1
- gridstate-2.4.1/tests/test_load_characteristic_segments.py +159 -0
- gridstate-2.4.1/tests/test_solution_hints.py +40 -0
- gridstate-2.3.0/gridstate.egg-info/scm_version.json +0 -8
- {gridstate-2.3.0 → gridstate-2.4.1}/.github/workflows/ci.yml +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/.github/workflows/release.yml +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/.gitignore +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/.pre-commit-config.yaml +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/LICENSE +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/Makefile +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/README.md +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/build_cython.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/__init__.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/__pyinstaller/__init__.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/__pyinstaller/hook-gridstate.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/_arrays.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/adapters/__init__.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/adapters/from_pandapower.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algebra/__init__.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algebra/_selinv_kernels.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algebra/base.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algebra/estimators.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algebra/jacobian_pattern.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algebra/normalized_residuals.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algorithms/__init__.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algorithms/_gram_numba.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algorithms/ipm.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algorithms/kkt_solver.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algorithms/status.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/algorithms/wls.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/bad_data_repass.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/bounds.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/constants.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/contract/_legacy.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/contract/containers.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/contract/derived.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/contract/derived_tables.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/contract/serialize.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/losses.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/measurement_ids.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/pipeline/__init__.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/pipeline/context.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/pipeline/manifest.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/pipeline/orchestration.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/pipeline/runner.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/pipeline/steps.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/preprocessing/__init__.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/preprocessing/_scan.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/preprocessing/ipm_setup.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/preprocessing/meas_rows.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/preprocessing/mirror_voltage.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/preprocessing/node_props.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/preprocessing/pseudo_measurements.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/py.typed +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/quality_summary.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/resolve_pass.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/result.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/runtime.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/shunt_sanity.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/state.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/__init__.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/_filters.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/apply_resolved.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/generators.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/kinds.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/load_only_injection.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/loss_filter.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/measurements.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/noise.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/on_line.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/quality.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/rpn.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/sigma_floor.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/units.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/voltage_filter.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/telemetry/voltage_nominal.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/topology.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/units.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/utils.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/v_mirror.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/v_refine.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/validation/__init__.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/validation/_diagnostics.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/validation/bad_data.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/validation/chi2_test.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/validation/observability.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/ybus.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate/z_vector.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate.egg-info/dependency_links.txt +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate.egg-info/entry_points.txt +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate.egg-info/requires.txt +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/gridstate.egg-info/top_level.txt +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/pyproject.toml +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/setup.cfg +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/__init__.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/_golden.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/conftest.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_adapters_lazy_import.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_aggregate_generators.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_algebra_base.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_anti_overshoot.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_arrays_helpers.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_bad_data.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_bad_data_repass.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_bounds.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_boxed_balance.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_chain_mirror.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_contract.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_contract_edge_cases.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_contract_runtime.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/ieee/case118.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/ieee/case118_truth.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/ieee/case14.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/ieee/case14_truth.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/ieee/case30.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_data/ieee/case30_truth.npz +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_derived_tables.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_enum_codes.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_flow_sigma_floor.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_from_pandapower.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_generator_shunt_kernels.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_golden.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_gram_numba.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_ieee_cases.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_ipm.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_ipm_balance_modes.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_ipm_hints.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_ipm_injection_roles.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_ipm_integration.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_jacobian_pattern.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_kkt_solver.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_load_only_injection.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_losses_breakdown.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_losses_q_convention.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_losses_trafo_classification.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_materialize_kernel.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_measurement_filter_kernels.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_measurement_ids.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_mirror_voltage_kernel.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_near_critical_ranking.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_neutral_wording.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_noise_model.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_normalized_residuals.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_numpy_only_runtime.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_observability.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_on_line_kernel.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_perf_synthetic_network.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_pipeline.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_pipeline_config_validation.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_pipeline_idempotent.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_pipeline_invariant_errors.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_pipeline_step_order.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_pipeline_warm_start.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_pipeline_working_layer.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_post_processing.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_preparation_steps.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_prepare_network.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_pseudo_unknown_load.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_pseudo_v_unobservable.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_quality_summary_paths.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_reconcile_balance.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_refine_merge.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_resolve_pass.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_result_quality_summary.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_rn_selected_inversion.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_serialize_npz.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_shunt_sanity.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_solve_status.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_state.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_state_range.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_telemetry_apply_kernel.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_telemetry_bounds.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_telemetry_physics_kernels.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_tm_code_quality.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_topology_kernels.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_topology_port.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_transformer_from_shunt.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_units.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_units_array_kernels.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_v_mirror.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_v_refine.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_variance_charging.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_version.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_voltage_calibration_gen_nodes.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_voltage_range_filter_fallback.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_wls.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_wls_robustness.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_working_container.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_ybus.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tests/test_z_vector.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tools/gen_ieee_fixtures.py +0 -0
- {gridstate-2.3.0 → gridstate-2.4.1}/tools/golden.py +0 -0
|
@@ -9,6 +9,49 @@ 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.1 (2026-10-18)
|
|
13
|
+
|
|
14
|
+
### Behavior changes
|
|
15
|
+
|
|
16
|
+
- `ipm_pd`: the proximal term on the box-variable diagonal adapts to the step
|
|
17
|
+
length: ×10 after a step αp < 0.1, up to `box_prox_max` (new, default 1e-2 of
|
|
18
|
+
the median weight), ÷10 after αp ≥ 0.5, down to `box_prox`. It damps the step
|
|
19
|
+
only; the fixed point is unchanged. Estimates on the test models move by
|
|
20
|
+
≤ 1.6e-8 p.u. V with unchanged iteration counts.
|
|
21
|
+
- `ipm_pd`: a second stopping test, `fit_tol` (new, default 1e-2): the step moves
|
|
22
|
+
no data row by more than that fraction of its sigma (the μ, feasibility and
|
|
23
|
+
equality tests still apply).
|
|
24
|
+
|
|
25
|
+
### Fix
|
|
26
|
+
|
|
27
|
+
- `ipm_pd` no longer runs into its iteration limit and the primal fallback when
|
|
28
|
+
box variables are seen only through near-zero-weight rows (loads with
|
|
29
|
+
effectively free injections): such directions have barrier curvature only,
|
|
30
|
+
their steps dwarf the box and the boundary rule cut every step to a tiny αp.
|
|
31
|
+
A state the data do not determine no longer keeps the V/δ step test from
|
|
32
|
+
passing (it could move, or cycle, without changing any estimate).
|
|
33
|
+
|
|
34
|
+
## v2.4.0 (2026-10-17)
|
|
35
|
+
|
|
36
|
+
### Behavior changes
|
|
37
|
+
|
|
38
|
+
- Breaker branches marked by the input format (R = 0, X = 1 Ohm) get
|
|
39
|
+
X = 110·min_x·Vn Ohm with min_x = 4e-6 (≈ 0.044/Vn p.u.) instead of a fixed
|
|
40
|
+
1e-3 p.u.; `breaker_x_pu` (new) sets a fixed p.u. value instead. Estimates
|
|
41
|
+
move on models carrying the marker (≤ 4.5e-3 p.u. V with `ipm_pd` on the
|
|
42
|
+
test models) (#115)
|
|
43
|
+
- A solved state with V/Vnom below `implausible_v_ratio` (new, default 0.3)
|
|
44
|
+
at any node is marked not converged whatever the solver reports, with a
|
|
45
|
+
WARNING listing the nodes and hints; a non-converged WLS solve logs its
|
|
46
|
+
largest normalized residuals (#114, #115)
|
|
47
|
+
|
|
48
|
+
### Feat
|
|
49
|
+
|
|
50
|
+
- Piecewise static load characteristics: optional input table
|
|
51
|
+
`load_characteristic_segments` (contract 3.2.0) with per-segment
|
|
52
|
+
coefficients up to V⁴ for P and Q; `apply_load_characteristic` uses it in
|
|
53
|
+
place of the quadratic row; without the table nothing changes (#113, #115)
|
|
54
|
+
|
|
12
55
|
## v2.3.0 (2026-10-17)
|
|
13
56
|
|
|
14
57
|
### 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.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 4, 1)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g640125491'
|
|
@@ -50,6 +50,19 @@ corrector's second-order term may spoil descent, and then the centering
|
|
|
50
50
|
direction is taken instead (one more solve on the same factor). The dual step
|
|
51
51
|
keeps its own fraction-to-boundary length.
|
|
52
52
|
|
|
53
|
+
Adaptive box regularisation. A box variable seen only through rows of
|
|
54
|
+
negligible weight (a load whose injection row is a near-zero-weight pseudo
|
|
55
|
+
measurement) or through a combination the data do not resolve (a reactive flow
|
|
56
|
+
circulating between two such loads) has no curvature but the barrier's, and
|
|
57
|
+
the Newton step along it is orders of magnitude longer than the box. The
|
|
58
|
+
fraction-to-boundary rule then cuts every step to ``αp ≪ 1``, the
|
|
59
|
+
complementarity stops decreasing and the iteration crawls to its limit. The
|
|
60
|
+
proximal term on the box diagonal is therefore raised tenfold after every
|
|
61
|
+
step shorter than 0.1 (up to ``box_prox_max``) and lowered tenfold after
|
|
62
|
+
every step of 0.5 or longer (down to ``box_prox``). The term damps the step
|
|
63
|
+
only, so the fixed point is unchanged; a well-scaled problem takes long steps
|
|
64
|
+
and keeps the base term.
|
|
65
|
+
|
|
53
66
|
Start on the central path: ``s`` = half-range of a two-sided constraint (the
|
|
54
67
|
distance for a one-sided one), ``y = μ0/s``; the initial point need not satisfy
|
|
55
68
|
``g − l = s``.
|
|
@@ -96,6 +109,14 @@ def _blocking_constraint(
|
|
|
96
109
|
return ku, False
|
|
97
110
|
|
|
98
111
|
|
|
112
|
+
# Adaptive box regularisation (see the module docstring): a primal step shorter
|
|
113
|
+
# than _PROX_SHORT raises the term by _PROX_GROW, one of _PROX_LONG or longer
|
|
114
|
+
# lowers it by the same factor.
|
|
115
|
+
_PROX_SHORT = 0.1
|
|
116
|
+
_PROX_LONG = 0.5
|
|
117
|
+
_PROX_GROW = 10.0
|
|
118
|
+
|
|
119
|
+
|
|
99
120
|
Direction = tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]
|
|
100
121
|
|
|
101
122
|
|
|
@@ -212,6 +233,7 @@ def solve_ipm_pd(
|
|
|
212
233
|
armijo_max: int = 10,
|
|
213
234
|
max_search_failures: int = 20,
|
|
214
235
|
step_tol: float = 1e-4,
|
|
236
|
+
fit_tol: float = 1e-2,
|
|
215
237
|
feas_tol: float = 1e-6,
|
|
216
238
|
max_iter: int = 100,
|
|
217
239
|
box_eps: float = 1e-3,
|
|
@@ -225,6 +247,7 @@ def solve_ipm_pd(
|
|
|
225
247
|
kkt_solver: KKTSolver | None = None,
|
|
226
248
|
eq_prox: float = 1e-9,
|
|
227
249
|
box_prox: float = 1e-6,
|
|
250
|
+
box_prox_max: float = 1e-2,
|
|
228
251
|
) -> IPMResult:
|
|
229
252
|
"""Mehrotra predictor-corrector IPM (see the module docstring).
|
|
230
253
|
|
|
@@ -251,6 +274,11 @@ def solve_ipm_pd(
|
|
|
251
274
|
outside ``box_idx`` (V/δ) is below it. Box variables may still move
|
|
252
275
|
along directions the data do not see (the common level of a node's
|
|
253
276
|
generation and load), which change neither the state nor the residuals.
|
|
277
|
+
fit_tol: alternatively, stop once that step moves no data row by more
|
|
278
|
+
than ``fit_tol`` of its sigma (``max √w·|H·αp·Δx|``). A state the data
|
|
279
|
+
do not determine (a node behind loads whose injection rows carry
|
|
280
|
+
near-zero weight) can keep moving, or cycle, without changing any
|
|
281
|
+
estimated measurement; the fit has converged all the same.
|
|
254
282
|
feas_tol: admissible primal infeasibility of the inequalities (p.u.).
|
|
255
283
|
max_iter: iteration limit (one factorization each).
|
|
256
284
|
box_eps: margin from the bounds when projecting ``x_init``.
|
|
@@ -271,6 +299,9 @@ def solve_ipm_pd(
|
|
|
271
299
|
has no curvature along that common level but the barrier's, so a
|
|
272
300
|
cold start produces huge box steps cut to tiny ``αp`` by the boundary
|
|
273
301
|
rule; the term bounds those steps without moving the fixed point.
|
|
302
|
+
box_prox_max: ceiling of the adaptive box term (same units as
|
|
303
|
+
``box_prox``, see the module docstring); a value not above
|
|
304
|
+
``box_prox`` keeps the term fixed.
|
|
274
305
|
|
|
275
306
|
Returns:
|
|
276
307
|
:class:`~gridstate.algorithms.ipm.IPMResult`; ``status`` is ``"kkt"`` when
|
|
@@ -325,10 +356,9 @@ def solve_ipm_pd(
|
|
|
325
356
|
is_eq = np.zeros(m, dtype=bool)
|
|
326
357
|
is_eq[eq_rows] = True
|
|
327
358
|
rho = w_base[eq_rows]
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
)
|
|
359
|
+
eq_prox_abs = eq_prox * float(np.max(rho)) if rho.size else 0.0
|
|
360
|
+
w_median = float(np.median(w_base[~is_eq])) if (~is_eq).any() else 0.0
|
|
361
|
+
box_scale = box_prox
|
|
332
362
|
|
|
333
363
|
huber_mask_arr = (
|
|
334
364
|
np.asarray(huber_mask, dtype=bool) if huber_mask is not None else np.zeros(m, dtype=bool)
|
|
@@ -378,6 +408,7 @@ def solve_ipm_pd(
|
|
|
378
408
|
w = w_base
|
|
379
409
|
mu = mu0 if n_pairs else 0.0
|
|
380
410
|
step_inf = float("inf")
|
|
411
|
+
fit_inf = float("inf")
|
|
381
412
|
eq_inf = 0.0
|
|
382
413
|
grad_inf = float("inf")
|
|
383
414
|
status = "stalled"
|
|
@@ -399,7 +430,7 @@ def solve_ipm_pd(
|
|
|
399
430
|
and mu <= mu_min
|
|
400
431
|
and feas_inf <= feas_tol
|
|
401
432
|
and bool(np.all(np.abs(c) <= eq_tol_arr))
|
|
402
|
-
and step_inf <= step_tol
|
|
433
|
+
and (step_inf <= step_tol or fit_inf <= fit_tol)
|
|
403
434
|
):
|
|
404
435
|
status = "kkt"
|
|
405
436
|
break
|
|
@@ -413,7 +444,7 @@ def solve_ipm_pd(
|
|
|
413
444
|
w_mat[eq_rows] = rho
|
|
414
445
|
w_mat[mb_rows] += d_k[nb:]
|
|
415
446
|
diag = np.zeros(n)
|
|
416
|
-
diag[box_idx] = d_k[:nb] +
|
|
447
|
+
diag[box_idx] = d_k[:nb] + max(eq_prox_abs, box_scale * w_median)
|
|
417
448
|
# Stationarity residual of the current point (for reporting).
|
|
418
449
|
y_dual = np.zeros(nb + mb_rows.size)
|
|
419
450
|
y_dual[il] += y_l
|
|
@@ -490,7 +521,8 @@ def solve_ipm_pd(
|
|
|
490
521
|
il=il,
|
|
491
522
|
iu=iu,
|
|
492
523
|
)
|
|
493
|
-
|
|
524
|
+
hdx = H @ dx
|
|
525
|
+
slope = merit.slope(lin, hdx, ds_l, ds_u)
|
|
494
526
|
if slope >= 0.0 and n_pairs:
|
|
495
527
|
# The corrector's second-order term can spoil descent; the pure
|
|
496
528
|
# centering direction is a Newton step on the merit and descends
|
|
@@ -498,9 +530,11 @@ def solve_ipm_pd(
|
|
|
498
530
|
dx_c, ds_l_c, ds_u_c, dy_l_c, dy_u_c, dlam_c = lin.direction(
|
|
499
531
|
target - s_l * y_l, target - s_u * y_u
|
|
500
532
|
)
|
|
501
|
-
|
|
533
|
+
hdx_c = H @ dx_c
|
|
534
|
+
slope_c = merit.slope(lin, hdx_c, ds_l_c, ds_u_c)
|
|
502
535
|
if slope_c < 0.0 and np.all(np.isfinite(dx_c)):
|
|
503
536
|
dx, ds_l, ds_u, dy_l, dy_u, dlam = dx_c, ds_l_c, ds_u_c, dy_l_c, dy_u_c, dlam_c
|
|
537
|
+
hdx = hdx_c
|
|
504
538
|
slope = slope_c
|
|
505
539
|
n_centering += 1
|
|
506
540
|
|
|
@@ -521,6 +555,7 @@ def solve_ipm_pd(
|
|
|
521
555
|
a_p = tr_radius / free_inf
|
|
522
556
|
# The stopping test measures the Newton step, not how far the search cut it.
|
|
523
557
|
step_inf = a_p * free_inf
|
|
558
|
+
fit_inf = a_p * float(np.max(np.sqrt(w_data) * np.abs(hdx))) if m else 0.0
|
|
524
559
|
a_full = a_p
|
|
525
560
|
r_new = None
|
|
526
561
|
# A direction that does not descend (inconsistent slacks of a nonlinear
|
|
@@ -563,6 +598,11 @@ def solve_ipm_pd(
|
|
|
563
598
|
eq_inf,
|
|
564
599
|
feas_inf,
|
|
565
600
|
)
|
|
601
|
+
if box_prox_max > box_prox:
|
|
602
|
+
if a_p < _PROX_SHORT:
|
|
603
|
+
box_scale = min(box_scale * _PROX_GROW, box_prox_max)
|
|
604
|
+
elif a_p >= _PROX_LONG:
|
|
605
|
+
box_scale = max(box_scale / _PROX_GROW, box_prox)
|
|
566
606
|
x = x + a_p * dx
|
|
567
607
|
s_l = s_l + a_p * ds_l
|
|
568
608
|
s_u = s_u + a_p * ds_u
|
|
@@ -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
|
+
]
|