flextool 4.0.0__py3-none-any.whl
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.
- flextool/__init__.py +41 -0
- flextool/_mem_sampler.py +193 -0
- flextool/_resources.py +43 -0
- flextool/calibrate/__init__.py +51 -0
- flextool/calibrate/__main__.py +11 -0
- flextool/calibrate/_cli.py +316 -0
- flextool/calibrate/_db_alt.py +166 -0
- flextool/calibrate/_final_outputs.py +110 -0
- flextool/calibrate/_guard.py +151 -0
- flextool/calibrate/_loop.py +558 -0
- flextool/calibrate/_readers.py +223 -0
- flextool/calibrate/_report.py +263 -0
- flextool/calibrate/_sizing.py +699 -0
- flextool/calibrate/_solve.py +134 -0
- flextool/calibrate/_solve_status.py +495 -0
- flextool/cli/__init__.py +9 -0
- flextool/cli/_console.py +51 -0
- flextool/cli/_timing.py +147 -0
- flextool/cli/cmd_execute_flextool_workflow.py +187 -0
- flextool/cli/cmd_export_to_tabular.py +56 -0
- flextool/cli/cmd_import_sensitivities.py +75 -0
- flextool/cli/cmd_migrate_database.py +13 -0
- flextool/cli/cmd_open_results_db.py +269 -0
- flextool/cli/cmd_read_matpower.py +66 -0
- flextool/cli/cmd_read_old_flextool.py +63 -0
- flextool/cli/cmd_read_self_describing_tabular_input.py +50 -0
- flextool/cli/cmd_read_tabular_input.py +81 -0
- flextool/cli/cmd_run_flextool.py +1095 -0
- flextool/cli/cmd_scenario_results.py +284 -0
- flextool/cli/cmd_solve_mps.py +169 -0
- flextool/cli/cmd_update_flextool.py +17 -0
- flextool/cli/cmd_write_outputs.py +125 -0
- flextool/common_utils/__init__.py +1 -0
- flextool/common_utils/plot_mem_shape.py +77 -0
- flextool/common_utils/precision.py +451 -0
- flextool/decomposition/__init__.py +0 -0
- flextool/decomposition/region_decomposition.py +128 -0
- flextool/decomposition/region_filter.py +1261 -0
- flextool/engine_polars/__init__.py +110 -0
- flextool/engine_polars/_axis_enums.py +742 -0
- flextool/engine_polars/_benders.py +3462 -0
- flextool/engine_polars/_block_layout.py +1479 -0
- flextool/engine_polars/_blocks.py +1515 -0
- flextool/engine_polars/_commodity_ladder.py +660 -0
- flextool/engine_polars/_cumulative_invest.py +1165 -0
- flextool/engine_polars/_db_loader.py +153 -0
- flextool/engine_polars/_db_reader.py +127 -0
- flextool/engine_polars/_dc_power_flow.py +445 -0
- flextool/engine_polars/_delay.py +442 -0
- flextool/engine_polars/_derived_arithmetic.py +432 -0
- flextool/engine_polars/_derived_block.py +990 -0
- flextool/engine_polars/_derived_branch.py +769 -0
- flextool/engine_polars/_derived_existing.py +1353 -0
- flextool/engine_polars/_derived_npv.py +1297 -0
- flextool/engine_polars/_derived_params.py +9850 -0
- flextool/engine_polars/_derived_profile.py +881 -0
- flextool/engine_polars/_derived_walks.py +276 -0
- flextool/engine_polars/_determinism.py +70 -0
- flextool/engine_polars/_direct_params.py +2186 -0
- flextool/engine_polars/_dump_csvs.py +1009 -0
- flextool/engine_polars/_emit_arc_unions.py +1631 -0
- flextool/engine_polars/_emit_calc_params.py +729 -0
- flextool/engine_polars/_emit_chain_params.py +709 -0
- flextool/engine_polars/_emit_co2_accumulators.py +400 -0
- flextool/engine_polars/_emit_dispatchers.py +690 -0
- flextool/engine_polars/_emit_energy_margin.py +125 -0
- flextool/engine_polars/_emit_energy_margin_adder.py +290 -0
- flextool/engine_polars/_emit_entity_annual.py +428 -0
- flextool/engine_polars/_emit_inflow_scaling.py +1420 -0
- flextool/engine_polars/_emit_leaf_sets.py +550 -0
- flextool/engine_polars/_emit_lp_scaling.py +665 -0
- flextool/engine_polars/_emit_mid_sets.py +859 -0
- flextool/engine_polars/_emit_pdt_params.py +759 -0
- flextool/engine_polars/_emit_per_solve.py +774 -0
- flextool/engine_polars/_emit_period_calc.py +504 -0
- flextool/engine_polars/_emit_period_params.py +2398 -0
- flextool/engine_polars/_emit_provider_io.py +141 -0
- flextool/engine_polars/_emit_reserve.py +574 -0
- flextool/engine_polars/_emit_solve_time.py +311 -0
- flextool/engine_polars/_emit_solve_writers.py +1249 -0
- flextool/engine_polars/_flex_data_accumulator.py +388 -0
- flextool/engine_polars/_flex_data_provider.py +478 -0
- flextool/engine_polars/_group_slack.py +1253 -0
- flextool/engine_polars/_inmemory_reader.py +140 -0
- flextool/engine_polars/_input_source.py +336 -0
- flextool/engine_polars/_invest_seeds.py +191 -0
- flextool/engine_polars/_native_input_writer.py +100 -0
- flextool/engine_polars/_native_run_model.py +1348 -0
- flextool/engine_polars/_orchestration.py +4314 -0
- flextool/engine_polars/_output_writer.py +439 -0
- flextool/engine_polars/_param_shapes.py +1595 -0
- flextool/engine_polars/_parquet_bundle.py +723 -0
- flextool/engine_polars/_pdt_join.py +167 -0
- flextool/engine_polars/_pdt_lookup.py +547 -0
- flextool/engine_polars/_per_solve_sets.py +335 -0
- flextool/engine_polars/_projection_params.py +2056 -0
- flextool/engine_polars/_provider_keys.py +173 -0
- flextool/engine_polars/_provider_translators.py +225 -0
- flextool/engine_polars/_recursive_solve.py +703 -0
- flextool/engine_polars/_region_filter.py +2508 -0
- flextool/engine_polars/_reserve.py +649 -0
- flextool/engine_polars/_solve_acceptance.py +331 -0
- flextool/engine_polars/_solve_config.py +1001 -0
- flextool/engine_polars/_solve_context.py +885 -0
- flextool/engine_polars/_solve_handoff.py +164 -0
- flextool/engine_polars/_solve_state.py +232 -0
- flextool/engine_polars/_solver_base.py +36 -0
- flextool/engine_polars/_solver_dispatch.py +511 -0
- flextool/engine_polars/_spinedb_reader.py +1165 -0
- flextool/engine_polars/_stochastic.py +593 -0
- flextool/engine_polars/_subprocess_solve.py +1838 -0
- flextool/engine_polars/_timeline.py +1416 -0
- flextool/engine_polars/_vectorize.py +438 -0
- flextool/engine_polars/_warm.py +858 -0
- flextool/engine_polars/autoscale/__init__.py +107 -0
- flextool/engine_polars/autoscale/_config.py +218 -0
- flextool/engine_polars/autoscale/_layer2.py +1253 -0
- flextool/engine_polars/autoscale/_layer2_types.py +584 -0
- flextool/engine_polars/autoscale/_quantity_types.py +621 -0
- flextool/engine_polars/autoscale/_report.py +336 -0
- flextool/engine_polars/chain.py +259 -0
- flextool/engine_polars/input.py +6638 -0
- flextool/engine_polars/model.py +4754 -0
- flextool/env_check.py +388 -0
- flextool/export_to_tabular/__init__.py +5 -0
- flextool/export_to_tabular/db_reader.py +224 -0
- flextool/export_to_tabular/excel_writer.py +3559 -0
- flextool/export_to_tabular/export_settings.yaml +377 -0
- flextool/export_to_tabular/export_to_excel.py +227 -0
- flextool/export_to_tabular/formatting.py +543 -0
- flextool/export_to_tabular/sheet_config.py +876 -0
- flextool/gui/__init__.py +0 -0
- flextool/gui/__main__.py +118 -0
- flextool/gui/calibrate_commands.py +184 -0
- flextool/gui/calibrate_jobs.py +424 -0
- flextool/gui/check_tree.py +142 -0
- flextool/gui/cli_format.py +83 -0
- flextool/gui/config_parser.py +68 -0
- flextool/gui/data_models.py +362 -0
- flextool/gui/db_editor_integration.py +202 -0
- flextool/gui/db_version_check.py +269 -0
- flextool/gui/dialogs/__init__.py +0 -0
- flextool/gui/dialogs/add_dialog.py +1098 -0
- flextool/gui/dialogs/calibrate_dialog.py +1259 -0
- flextool/gui/dialogs/file_picker.py +473 -0
- flextool/gui/dialogs/group_picker.py +299 -0
- flextool/gui/dialogs/migration_consent_dialog.py +106 -0
- flextool/gui/dialogs/migration_progress_dialog.py +237 -0
- flextool/gui/dialogs/plot_dialog.py +459 -0
- flextool/gui/dialogs/plot_settings_picker.py +2184 -0
- flextool/gui/dialogs/project_dialog.py +426 -0
- flextool/gui/dialogs/update_dialog.py +212 -0
- flextool/gui/downsampling.py +88 -0
- flextool/gui/error_handling.py +50 -0
- flextool/gui/execution_manager.py +1715 -0
- flextool/gui/execution_window.py +1377 -0
- flextool/gui/hover_tooltip.py +111 -0
- flextool/gui/input_sources.py +730 -0
- flextool/gui/main_window.py +6181 -0
- flextool/gui/network_graph.py +215 -0
- flextool/gui/output_actions.py +393 -0
- flextool/gui/output_log_window.py +159 -0
- flextool/gui/platform_utils.py +421 -0
- flextool/gui/plot_cache.py +88 -0
- flextool/gui/plot_canvas.py +543 -0
- flextool/gui/plot_config_reader.py +272 -0
- flextool/gui/project_utils.py +100 -0
- flextool/gui/result_viewer.py +4394 -0
- flextool/gui/scenario_key.py +162 -0
- flextool/gui/scenario_lists.py +516 -0
- flextool/gui/settings_io.py +360 -0
- flextool/gui/solve_reader.py +103 -0
- flextool/gui/tree_reorder.py +88 -0
- flextool/gui/ui_metrics.py +420 -0
- flextool/input_derivation/__init__.py +281 -0
- flextool/input_derivation/_commodity_ladder.py +375 -0
- flextool/input_derivation/_commodity_ladder_sets.py +70 -0
- flextool/input_derivation/_dc_power_flow.py +377 -0
- flextool/input_derivation/_method_constants.py +77 -0
- flextool/input_derivation/_process_method.py +258 -0
- flextool/input_derivation/_specs.py +1026 -0
- flextool/input_derivation/_validators.py +321 -0
- flextool/lean_parquet.py +159 -0
- flextool/model_builder/__init__.py +5 -0
- flextool/model_builder/build_model.py +589 -0
- flextool/model_builder/encoding.py +67 -0
- flextool/model_builder/names.py +34 -0
- flextool/model_builder/profiles.py +129 -0
- flextool/plot_outputs/__init__.py +14 -0
- flextool/plot_outputs/axis_helpers.py +355 -0
- flextool/plot_outputs/color_template.py +888 -0
- flextool/plot_outputs/config.py +171 -0
- flextool/plot_outputs/format_helpers.py +345 -0
- flextool/plot_outputs/legend_helpers.py +143 -0
- flextool/plot_outputs/orchestrator.py +1141 -0
- flextool/plot_outputs/perf.py +37 -0
- flextool/plot_outputs/plan.py +1787 -0
- flextool/plot_outputs/plot_bars.py +1510 -0
- flextool/plot_outputs/plot_bars_detail.py +753 -0
- flextool/plot_outputs/plot_lines.py +951 -0
- flextool/plot_outputs/shared_manifest.py +564 -0
- flextool/plot_outputs/subplot_helpers.py +137 -0
- flextool/process_inputs/__init__.py +188 -0
- flextool/process_inputs/import_old_excel_input.json +4159 -0
- flextool/process_inputs/read_matpower.py +451 -0
- flextool/process_inputs/read_old_flextool.py +1288 -0
- flextool/process_inputs/read_self_describing_excel.py +1423 -0
- flextool/process_inputs/read_tabular_with_specification.py +1114 -0
- flextool/process_inputs/write_old_flextool_to_db.py +3077 -0
- flextool/process_inputs/write_self_describing_to_db.py +977 -0
- flextool/process_inputs/write_to_input_db.py +269 -0
- flextool/process_outputs/__init__.py +7 -0
- flextool/process_outputs/_annualize.py +55 -0
- flextool/process_outputs/_inmemory_helpers.py +292 -0
- flextool/process_outputs/_output_meta.py +672 -0
- flextool/process_outputs/calc_capacity_flows.py +107 -0
- flextool/process_outputs/calc_connections.py +136 -0
- flextool/process_outputs/calc_costs.py +260 -0
- flextool/process_outputs/calc_group_flows.py +192 -0
- flextool/process_outputs/calc_slacks.py +103 -0
- flextool/process_outputs/calc_storage_vre.py +160 -0
- flextool/process_outputs/drop_levels.py +208 -0
- flextool/process_outputs/handoff_writers.py +1315 -0
- flextool/process_outputs/out_ancillary.py +544 -0
- flextool/process_outputs/out_capacity.py +179 -0
- flextool/process_outputs/out_costs.py +334 -0
- flextool/process_outputs/out_flowgroup.py +189 -0
- flextool/process_outputs/out_flows.py +301 -0
- flextool/process_outputs/out_group.py +475 -0
- flextool/process_outputs/out_node.py +190 -0
- flextool/process_outputs/persist_realized_slice.py +601 -0
- flextool/process_outputs/process_results.py +24 -0
- flextool/process_outputs/read_highs_solution.py +2256 -0
- flextool/process_outputs/read_parameters.py +1799 -0
- flextool/process_outputs/read_sets.py +1095 -0
- flextool/process_outputs/read_variables.py +553 -0
- flextool/process_outputs/solve_order.py +81 -0
- flextool/process_outputs/spinedb_replay.py +412 -0
- flextool/process_outputs/union_realized_slice.py +224 -0
- flextool/process_outputs/write_outputs.py +1286 -0
- flextool/process_outputs/write_spinedb.py +1267 -0
- flextool/representative_periods/__init__.py +5 -0
- flextool/representative_periods/clustering.py +165 -0
- flextool/representative_periods/force_include.py +563 -0
- flextool/representative_periods/netload.py +365 -0
- flextool/representative_periods/netload_inputs.py +345 -0
- flextool/representative_periods/netload_iterate.py +722 -0
- flextool/representative_periods/preprocess.py +948 -0
- flextool/representative_periods/scenario_stack.py +195 -0
- flextool/representative_periods/weights.py +124 -0
- flextool/scenario_comparison/__init__.py +13 -0
- flextool/scenario_comparison/config_builder.py +158 -0
- flextool/scenario_comparison/constants.py +20 -0
- flextool/scenario_comparison/data_models.py +222 -0
- flextool/scenario_comparison/db_reader.py +399 -0
- flextool/scenario_comparison/dispatch_data.py +1002 -0
- flextool/scenario_comparison/dispatch_mappings.py +205 -0
- flextool/scenario_comparison/dispatch_plots.py +691 -0
- flextool/scenario_comparison/input_entity_colors.py +319 -0
- flextool/scenario_comparison/orchestrator.py +453 -0
- flextool/scenario_comparison/plan_union.py +244 -0
- flextool/scenario_comparison/plot_settings_seed.py +205 -0
- flextool/schemas/AXIS_CONTRACT.md +71 -0
- flextool/schemas/canonical_databases/howto_aggregate_output.json +6225 -0
- flextool/schemas/canonical_databases/howto_connections.json +5606 -0
- flextool/schemas/canonical_databases/howto_demand.json +5518 -0
- flextool/schemas/canonical_databases/howto_hydro_reservoir.json +6239 -0
- flextool/schemas/canonical_databases/howto_hydro_reservoir_with_pump.json +5933 -0
- flextool/schemas/canonical_databases/howto_non_sync_and_curtailment.json +5794 -0
- flextool/schemas/canonical_databases/howto_ramp_and_start_up.json +5707 -0
- flextool/schemas/canonical_databases/howto_stochastics.json +6032 -0
- flextool/schemas/canonical_databases/templates_examples.json +13532 -0
- flextool/schemas/canonical_databases/templates_time_settings_only.json +5340 -0
- flextool/schemas/comparison_settings_template.json +197 -0
- flextool/schemas/default_plot_settings.yaml +260 -0
- flextool/schemas/default_plots.yaml +2293 -0
- flextool/schemas/flextool_axis_contract.json +303 -0
- flextool/schemas/flextool_axis_contract.schema.json +247 -0
- flextool/schemas/old_flextool_import_template.json +4443 -0
- flextool/schemas/output_info_template.json +48 -0
- flextool/schemas/output_settings_template.json +256 -0
- flextool/schemas/pre_v26/flextool_template_constant_default.json +2105 -0
- flextool/schemas/pre_v26/flextool_template_default_optional_output.json +2152 -0
- flextool/schemas/pre_v26/flextool_template_default_value.json +2094 -0
- flextool/schemas/pre_v26/flextool_template_drop_down.json +2080 -0
- flextool/schemas/pre_v26/flextool_template_lifetime_method.json +1990 -0
- flextool/schemas/pre_v26/flextool_template_optional_outputs.json +2094 -0
- flextool/schemas/pre_v26/flextool_template_output_node_flows.json +2105 -0
- flextool/schemas/pre_v26/flextool_template_results_master.json +493 -0
- flextool/schemas/pre_v26/flextool_template_rolling_start_remove.json +2087 -0
- flextool/schemas/pre_v26/flextool_template_rolling_window.json +2059 -0
- flextool/schemas/pre_v26/flextool_template_storage_binding_defaults.json +46 -0
- flextool/schemas/pre_v26/flextool_template_v2.json +1990 -0
- flextool/schemas/pre_v26/flextool_template_v25.json +3864 -0
- flextool/schemas/spinedb_results_schema.json +581 -0
- flextool/schemas/spinedb_schema.json +4636 -0
- flextool/solver_config/copt.opt.template +18 -0
- flextool/solver_config/cplex.opt.template +25 -0
- flextool/solver_config/gurobi.opt.template +18 -0
- flextool/solver_config/highs.opt.template +18 -0
- flextool/solver_config/xpress.opt.template +26 -0
- flextool/spinedb_backend/__init__.py +26 -0
- flextool/spinedb_backend/_axis_enums.py +1119 -0
- flextool/spinedb_backend/_backend.py +1139 -0
- flextool/update_flextool/__init__.py +12 -0
- flextool/update_flextool/canonical_databases.py +251 -0
- flextool/update_flextool/db_migration.py +7108 -0
- flextool/update_flextool/ensure_settings_db.py +138 -0
- flextool/update_flextool/export_database.py +103 -0
- flextool/update_flextool/extend_tests_fixture.py +772 -0
- flextool/update_flextool/generate_canonical.py +274 -0
- flextool/update_flextool/initialize_database.py +42 -0
- flextool/update_flextool/install_info.py +225 -0
- flextool/update_flextool/self_update.py +464 -0
- flextool/update_flextool/sync_master_json_template.py +125 -0
- flextool/update_flextool/test_fixtures.py +187 -0
- flextool-4.0.0.dist-info/METADATA +217 -0
- flextool-4.0.0.dist-info/RECORD +322 -0
- flextool-4.0.0.dist-info/WHEEL +5 -0
- flextool-4.0.0.dist-info/entry_points.txt +17 -0
- flextool-4.0.0.dist-info/licenses/LICENSE.txt +19 -0
- flextool-4.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"""DC power flow physics — linearised AC transmission constraints.
|
|
2
|
+
|
|
3
|
+
Mirrors the .mod's ``dc_flow_eq`` family (flextool.mod:3236-3244)::
|
|
4
|
+
|
|
5
|
+
s.t. dc_flow_eq {p in connection_dc_power_flow,
|
|
6
|
+
(p, source, sink) in process_source_toSink,
|
|
7
|
+
(p, 'sink', b_out) in process__side__block,
|
|
8
|
+
(b_out, d, t) in block__period__step
|
|
9
|
+
: source in node_dc_power_flow
|
|
10
|
+
&& sink in node_dc_power_flow} :
|
|
11
|
+
v_flow[p, source, sink, d, t] * p_entity_unitsize[p]
|
|
12
|
+
=
|
|
13
|
+
p_connection_susceptance[p] * (v_angle[source, d, t] - v_angle[sink, d, t])
|
|
14
|
+
|
|
15
|
+
In flextool's V1 DC PF scenarios all participating nodes sit on the
|
|
16
|
+
default (hourly) block, so the block-aware filter
|
|
17
|
+
(``(p, 'sink', b_out) in process__side__block`` × ``(b_out, d, t) in
|
|
18
|
+
block__period__step``) reduces to the plain ``(d, t)`` set. Our
|
|
19
|
+
emission follows that V1 simplification: we index dc_flow_eq over
|
|
20
|
+
``connection_dc_power_flow × process_source_toSink × dt`` filtered to
|
|
21
|
+
``source, sink in node_dc_power_flow``. Block-aware extension is
|
|
22
|
+
out of scope until a fixture exercises non-default blocks for DC PF
|
|
23
|
+
nodes (none do as of v51).
|
|
24
|
+
|
|
25
|
+
Inputs
|
|
26
|
+
------
|
|
27
|
+
* ``input/node_dc_power_flow.csv`` single-column ``node``
|
|
28
|
+
* ``input/connection_dc_power_flow.csv`` single-column ``process``
|
|
29
|
+
* ``input/node_reference_angle.csv`` single-column ``node`` (angle pinned to 0)
|
|
30
|
+
* ``input/p_connection_susceptance.csv`` two-column ``process,p_connection_susceptance``
|
|
31
|
+
|
|
32
|
+
Reference-angle policy
|
|
33
|
+
----------------------
|
|
34
|
+
Reference selection is handled by the
|
|
35
|
+
:mod:`flextool.input_derivation._dc_power_flow` derivation, which
|
|
36
|
+
emits the ``node_reference_angle.csv`` file. Per connected component
|
|
37
|
+
of the DC PF subnetwork, exactly one node carries angle = 0:
|
|
38
|
+
|
|
39
|
+
* If the group has ``reference_node`` set explicitly, that node is used.
|
|
40
|
+
* Otherwise BFS finds connected components (via
|
|
41
|
+
``connection__node__node`` adjacency) and picks the node with the
|
|
42
|
+
largest ``existing`` capacity in each component.
|
|
43
|
+
|
|
44
|
+
The engine reads the resulting CSV verbatim and pins
|
|
45
|
+
``v_angle[ref, d, t] = 0`` via the angle Var's per-row tight bound (the
|
|
46
|
+
.mod uses the same trick — ``p_angle_lower = p_angle_upper = 0`` for
|
|
47
|
+
ref nodes). The non-reference angle bounds are ±π (the .mod literal
|
|
48
|
+
``3.14159265``); polar_high's scalar-bound Var declaration uses ±π as
|
|
49
|
+
the loose bound, then emits an explicit equality ``v_angle[ref] = 0``
|
|
50
|
+
for reference nodes (since polar_high Var bounds are scalar).
|
|
51
|
+
"""
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
from pathlib import Path
|
|
55
|
+
from typing import TYPE_CHECKING
|
|
56
|
+
|
|
57
|
+
import polars as pl
|
|
58
|
+
|
|
59
|
+
from polar_high import Param, Where
|
|
60
|
+
|
|
61
|
+
from ._axis_enums import cast_dim
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if TYPE_CHECKING:
|
|
65
|
+
from polar_high.engine import Var
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# Feature detection
|
|
70
|
+
|
|
71
|
+
def has_feature(d) -> bool:
|
|
72
|
+
"""True iff DC power flow data is populated and non-empty.
|
|
73
|
+
|
|
74
|
+
Activation requires both at least one node in ``node_dc_power_flow``
|
|
75
|
+
AND at least one connection in ``connection_dc_power_flow``. A
|
|
76
|
+
fixture with header-only CSVs (most of the v51 fixtures) returns
|
|
77
|
+
False.
|
|
78
|
+
"""
|
|
79
|
+
nd = getattr(d, "node_dc_power_flow", None)
|
|
80
|
+
cd = getattr(d, "connection_dc_power_flow", None)
|
|
81
|
+
if nd is None or cd is None:
|
|
82
|
+
return False
|
|
83
|
+
return nd.height > 0 and cd.height > 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
# Data loading
|
|
88
|
+
|
|
89
|
+
def load_data(
|
|
90
|
+
inp_dir: str | Path,
|
|
91
|
+
*,
|
|
92
|
+
provider: "object | None" = None,
|
|
93
|
+
) -> dict:
|
|
94
|
+
"""Read DC power flow frames via the Provider (disk-fallback for
|
|
95
|
+
off-cascade test harnesses).
|
|
96
|
+
|
|
97
|
+
Returns a dict with keys matching the FlexData field names:
|
|
98
|
+
|
|
99
|
+
node_dc_power_flow pl.DataFrame | None # cols: (n,)
|
|
100
|
+
connection_dc_power_flow pl.DataFrame | None # cols: (p,)
|
|
101
|
+
node_reference_angle pl.DataFrame | None # cols: (n,)
|
|
102
|
+
p_connection_susceptance Param | None # dims: (p,)
|
|
103
|
+
|
|
104
|
+
All values are ``None`` (or empty) when the feature is inactive
|
|
105
|
+
(header-only frames, which is what the
|
|
106
|
+
:func:`flextool.input_derivation._dc_power_flow.derive_dc_power_flow`
|
|
107
|
+
pass writes for non-DC-PF scenarios).
|
|
108
|
+
|
|
109
|
+
Step 2.5-F Phase B
|
|
110
|
+
------------------
|
|
111
|
+
|
|
112
|
+
The four frames are now produced by
|
|
113
|
+
:mod:`flextool.input_derivation._dc_power_flow` and placed on the
|
|
114
|
+
cascade-input :class:`FlexDataProvider` under
|
|
115
|
+
``input/node_dc_power_flow``, ``input/connection_dc_power_flow``,
|
|
116
|
+
``input/node_reference_angle``, ``input/p_connection_susceptance``.
|
|
117
|
+
In-cascade the Provider is the authoritative source; the disk-read
|
|
118
|
+
arm is preserved exclusively for off-cascade fixture loaders that
|
|
119
|
+
seed inputs to ``input/`` and invoke the loader without a Provider.
|
|
120
|
+
"""
|
|
121
|
+
inp = Path(inp_dir)
|
|
122
|
+
|
|
123
|
+
blank = dict(
|
|
124
|
+
node_dc_power_flow = None,
|
|
125
|
+
connection_dc_power_flow = None,
|
|
126
|
+
node_reference_angle = None,
|
|
127
|
+
p_connection_susceptance = None,
|
|
128
|
+
process_source_toSink_dc = None,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def _frame_for(key: str, path: Path) -> "pl.DataFrame | None":
|
|
132
|
+
if provider is None or not provider.has(key):
|
|
133
|
+
return None
|
|
134
|
+
df = provider.get(key)
|
|
135
|
+
if df is None or df.height == 0:
|
|
136
|
+
return None
|
|
137
|
+
return df
|
|
138
|
+
|
|
139
|
+
def _project_single(df: "pl.DataFrame | None", col_in: str,
|
|
140
|
+
col_out: str) -> "pl.DataFrame | None":
|
|
141
|
+
if df is None:
|
|
142
|
+
return None
|
|
143
|
+
if col_in in df.columns and col_in != col_out:
|
|
144
|
+
df = df.rename({col_in: col_out})
|
|
145
|
+
return df.select(col_out)
|
|
146
|
+
|
|
147
|
+
nd_raw = _frame_for("input/node_dc_power_flow",
|
|
148
|
+
inp / "node_dc_power_flow.csv")
|
|
149
|
+
cd_raw = _frame_for("input/connection_dc_power_flow",
|
|
150
|
+
inp / "connection_dc_power_flow.csv")
|
|
151
|
+
rd_raw = _frame_for("input/node_reference_angle",
|
|
152
|
+
inp / "node_reference_angle.csv")
|
|
153
|
+
|
|
154
|
+
nd = _project_single(nd_raw, "node", "n")
|
|
155
|
+
cd = _project_single(cd_raw, "process", "p")
|
|
156
|
+
rd = _project_single(rd_raw, "node", "n")
|
|
157
|
+
|
|
158
|
+
pcs_raw = _frame_for("input/p_connection_susceptance",
|
|
159
|
+
inp / "p_connection_susceptance.csv")
|
|
160
|
+
pcs_param = None
|
|
161
|
+
if pcs_raw is not None:
|
|
162
|
+
df = pcs_raw.rename(
|
|
163
|
+
{c: r for c, r in [
|
|
164
|
+
("process", "p"),
|
|
165
|
+
("p_connection_susceptance", "value"),
|
|
166
|
+
] if c in pcs_raw.columns}
|
|
167
|
+
).select("p", "value")
|
|
168
|
+
# The Provider may carry the value as Utf8 (mirroring the legacy
|
|
169
|
+
# CSV emission); cast to Float64 so Param's downstream consumers
|
|
170
|
+
# see numeric values regardless of source.
|
|
171
|
+
if df["value"].dtype == pl.Utf8:
|
|
172
|
+
df = df.with_columns(pl.col("value").cast(pl.Float64))
|
|
173
|
+
pcs_param = Param(("p",), df)
|
|
174
|
+
|
|
175
|
+
if nd is None and cd is None and rd is None and pcs_param is None:
|
|
176
|
+
return blank
|
|
177
|
+
|
|
178
|
+
# Forward-direction (p, source, sink) for DC PF arcs. The cascade's
|
|
179
|
+
# ``process_source_sink`` doubles up 2-way connections (both arc
|
|
180
|
+
# orientations), but the .mod's dc_flow_eq is indexed over
|
|
181
|
+
# ``process_source_toSink`` which is one direction per arc. Read it
|
|
182
|
+
# from the Provider's ``solve_data/process_source_toSink`` key (the
|
|
183
|
+
# cascade writes it via _emit_calc_params.derive_process_source_toSink).
|
|
184
|
+
sst = None
|
|
185
|
+
if provider is not None and provider.has("solve_data/process_source_toSink"):
|
|
186
|
+
sst = provider.get("solve_data/process_source_toSink")
|
|
187
|
+
if sst is not None and sst.height > 0:
|
|
188
|
+
sst = sst.rename({
|
|
189
|
+
c: r for c, r in [
|
|
190
|
+
("process", "p"),
|
|
191
|
+
("source", "source"),
|
|
192
|
+
("sink", "sink"),
|
|
193
|
+
] if c in sst.columns
|
|
194
|
+
}).select("p", "source", "sink")
|
|
195
|
+
else:
|
|
196
|
+
sst = None
|
|
197
|
+
|
|
198
|
+
return dict(
|
|
199
|
+
node_dc_power_flow = nd,
|
|
200
|
+
connection_dc_power_flow = cd,
|
|
201
|
+
node_reference_angle = rd,
|
|
202
|
+
p_connection_susceptance = pcs_param,
|
|
203
|
+
process_source_toSink_dc = sst,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
# Variable + constraint emission
|
|
209
|
+
|
|
210
|
+
# Same float literal flextool's preprocessing (preprocessing/dc_angle_bounds.py)
|
|
211
|
+
# uses for the non-reference upper / lower angle bound. An 8-digit
|
|
212
|
+
# truncation of π — preserved here for parity with flextool's MPS bit
|
|
213
|
+
# pattern. See flextool.mod:1680-1681.
|
|
214
|
+
_PI_LITERAL = 3.14159265
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def dc_arcs_frame(d) -> "pl.DataFrame | None":
|
|
218
|
+
"""Return the DC-power-flow arc set ``(p, source, sink)``.
|
|
219
|
+
|
|
220
|
+
A DC arc is a ``connection_dc_power_flow`` connection whose both
|
|
221
|
+
endpoints sit in ``node_dc_power_flow``, in the one-direction-per-arc
|
|
222
|
+
orientation (``process_source_toSink_dc`` when available, else the
|
|
223
|
+
dual-direction ``process_source_sink`` fallback for off-cascade
|
|
224
|
+
harnesses). Returns ``None`` when no such arcs exist.
|
|
225
|
+
|
|
226
|
+
Factored out of :func:`add_variables` so ``model.py`` can compute the
|
|
227
|
+
DC subset that ``dc_flow_eq`` ranges over without owning the
|
|
228
|
+
``v_flow_back`` Var (which is now shared across ALL
|
|
229
|
+
``method_2way_1var_off`` arcs, DC + non-DC).
|
|
230
|
+
"""
|
|
231
|
+
if (d.process_source_sink is None
|
|
232
|
+
or d.connection_dc_power_flow is None
|
|
233
|
+
or d.connection_dc_power_flow.height == 0):
|
|
234
|
+
return None
|
|
235
|
+
# source/sink carry the entity-union (``e``) Enum; ``n`` of
|
|
236
|
+
# node_dc_power_flow carries the node-only Enum. Lift the latter
|
|
237
|
+
# to ``e`` so ``is_in`` matches dtypes.
|
|
238
|
+
_dc_n_e = d.node_dc_power_flow.with_columns(
|
|
239
|
+
cast_dim(pl.col("n"), None, "e"))["n"]
|
|
240
|
+
arcs_src = (getattr(d, "process_source_toSink_dc", None)
|
|
241
|
+
if getattr(d, "process_source_toSink_dc", None) is not None
|
|
242
|
+
else d.process_source_sink)
|
|
243
|
+
if arcs_src is not d.process_source_sink:
|
|
244
|
+
for col in ("p", "source", "sink"):
|
|
245
|
+
target_dtype = d.process_source_sink.schema[col]
|
|
246
|
+
if arcs_src.schema[col] != target_dtype:
|
|
247
|
+
arcs_src = arcs_src.with_columns(
|
|
248
|
+
pl.col(col).cast(target_dtype, strict=False)
|
|
249
|
+
)
|
|
250
|
+
dc_arcs = (arcs_src
|
|
251
|
+
.join(d.connection_dc_power_flow, on="p", how="inner")
|
|
252
|
+
.filter(pl.col("source").is_in(_dc_n_e))
|
|
253
|
+
.filter(pl.col("sink").is_in(_dc_n_e)))
|
|
254
|
+
return dc_arcs if dc_arcs.height > 0 else None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def add_variables(m, d, *, v_flow_back=None) -> "dict[str, Var]":
|
|
258
|
+
"""Declare ``v_angle[n, d, t]`` and stash the DC arc frame.
|
|
259
|
+
|
|
260
|
+
``v_angle`` is indexed by (n, d, t) where n ∈ ``node_dc_power_flow``.
|
|
261
|
+
Bounds are set to the loose ±π for non-reference nodes; reference-angle
|
|
262
|
+
pin to 0 is enforced by ``dc_reference_angle_eq`` below (polar_high
|
|
263
|
+
Vars have scalar bounds, so we can't pin per-row in the Var declaration).
|
|
264
|
+
|
|
265
|
+
The reverse-flow auxiliary ``v_flow_back[p, source, sink, d, t]`` is no
|
|
266
|
+
longer created here — ``model.py`` declares it ONCE over the union of
|
|
267
|
+
all ``method_2way_1var_off`` arcs (DC + non-DC) so the single-signed
|
|
268
|
+
flow ``v_flow ∈ [-cap, +cap]`` can run sink→source on any such arc.
|
|
269
|
+
The shared Var is passed in via *v_flow_back* and stashed in the
|
|
270
|
+
returned dict so :func:`add_constraints` can splice it into
|
|
271
|
+
``dc_flow_eq`` as ``v_flow - v_flow_back``. The nodeBalance injection
|
|
272
|
+
and the capacity cap (``maxFlow_back``) for the back auxiliary are
|
|
273
|
+
likewise owned by ``model.py`` over the full set.
|
|
274
|
+
"""
|
|
275
|
+
if not has_feature(d):
|
|
276
|
+
return {}
|
|
277
|
+
if d.dt is None or d.dt.height == 0:
|
|
278
|
+
return {}
|
|
279
|
+
|
|
280
|
+
# v_angle's index is the cross of node_dc_power_flow × dt.
|
|
281
|
+
angle_idx = d.node_dc_power_flow.join(d.dt, how="cross").select("n", "d", "t")
|
|
282
|
+
if angle_idx.height == 0:
|
|
283
|
+
return {}
|
|
284
|
+
|
|
285
|
+
v_angle = m.add_var(
|
|
286
|
+
"v_angle", ("n", "d", "t"), angle_idx,
|
|
287
|
+
lower=-_PI_LITERAL, upper=_PI_LITERAL,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
out: dict = {"v_angle": v_angle}
|
|
291
|
+
|
|
292
|
+
dc_arcs = dc_arcs_frame(d)
|
|
293
|
+
if dc_arcs is not None:
|
|
294
|
+
out["dc_arcs"] = dc_arcs
|
|
295
|
+
if v_flow_back is not None:
|
|
296
|
+
out["v_flow_back"] = v_flow_back
|
|
297
|
+
|
|
298
|
+
return out
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def add_constraints(m, d, vars: dict, *,
|
|
302
|
+
v_flow=None, p_unitsize=None,
|
|
303
|
+
p_flow_upper_existing=None) -> None:
|
|
304
|
+
"""Emit the dc_flow_eq + reference-angle pin.
|
|
305
|
+
|
|
306
|
+
``v_flow``: the model's ``v_flow[p, source, sink, d, t]`` Var. Required
|
|
307
|
+
when ``connection_dc_power_flow`` is non-empty — without flow the
|
|
308
|
+
angle-only LP would be vacuous.
|
|
309
|
+
|
|
310
|
+
``p_unitsize``: the ``p_entity_unitsize`` Param indexed by (p,).
|
|
311
|
+
Required for the same reason.
|
|
312
|
+
|
|
313
|
+
``p_flow_upper_existing``: accepted for backward-compatibility but no
|
|
314
|
+
longer consumed here — the back-flow capacity cap is emitted by
|
|
315
|
+
``model.py`` as ``maxFlow_back`` over the full ``method_2way_1var_off``
|
|
316
|
+
arc set (a superset of the DC arcs), so capping again here would be
|
|
317
|
+
redundant.
|
|
318
|
+
"""
|
|
319
|
+
if not has_feature(d):
|
|
320
|
+
return
|
|
321
|
+
v_angle = vars.get("v_angle")
|
|
322
|
+
if v_angle is None:
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
# ── 1. Reference-angle pin ───────────────────────────────────────────
|
|
326
|
+
# v_angle[ref, d, t] == 0 for ref ∈ node_reference_angle.
|
|
327
|
+
# The .mod preprocesses this as a tight Var bound (p_angle_lower =
|
|
328
|
+
# p_angle_upper = 0 on those rows). polar_high Var bounds are scalar,
|
|
329
|
+
# so emit an explicit equality constraint instead — same algebra.
|
|
330
|
+
if (d.node_reference_angle is not None
|
|
331
|
+
and d.node_reference_angle.height > 0):
|
|
332
|
+
ref_idx = (d.node_reference_angle
|
|
333
|
+
.join(d.dt, how="cross")
|
|
334
|
+
.select("n", "d", "t"))
|
|
335
|
+
if ref_idx.height > 0:
|
|
336
|
+
m.add_cstr(
|
|
337
|
+
"dc_reference_angle_eq",
|
|
338
|
+
over = ref_idx,
|
|
339
|
+
sense = "==",
|
|
340
|
+
lhs_terms = {"angle": Where(v_angle, ref_idx)},
|
|
341
|
+
rhs_terms = {},
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# ── 2. dc_flow_eq ────────────────────────────────────────────────────
|
|
345
|
+
# (v_flow - v_flow_back) * unitsize[p]
|
|
346
|
+
# == susceptance[p] * (v_angle[source, d, t] - v_angle[sink, d, t])
|
|
347
|
+
# Indexed over (p, source, sink, d, t) where p ∈
|
|
348
|
+
# connection_dc_power_flow AND source, sink ∈ node_dc_power_flow.
|
|
349
|
+
if v_flow is None or p_unitsize is None or d.p_connection_susceptance is None:
|
|
350
|
+
return
|
|
351
|
+
if d.process_source_sink is None:
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
dc_arcs = vars.get("dc_arcs")
|
|
355
|
+
if dc_arcs is None or dc_arcs.height == 0:
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
over = dc_arcs.join(d.dt, how="cross").select("p", "source", "sink", "d", "t")
|
|
359
|
+
|
|
360
|
+
# LHS: (v_flow - v_flow_back) * unitsize. v_flow_back is the
|
|
361
|
+
# non-negative reverse-direction auxiliary (see add_variables). In
|
|
362
|
+
# fixtures with no negative-flow demand the LP keeps v_flow_back at
|
|
363
|
+
# zero and the term reduces to v_flow * unitsize.
|
|
364
|
+
v_flow_back = vars.get("v_flow_back")
|
|
365
|
+
flow_signed = Where(v_flow, dc_arcs)
|
|
366
|
+
if v_flow_back is not None:
|
|
367
|
+
flow_signed = flow_signed - v_flow_back
|
|
368
|
+
lhs_flow = flow_signed * p_unitsize
|
|
369
|
+
|
|
370
|
+
# RHS: susceptance[p] * (v_angle[source, d, t] - v_angle[sink, d, t]).
|
|
371
|
+
# v_angle is indexed by (n, d, t). We need it twice — once aliased as
|
|
372
|
+
# ``source`` and once aliased as ``sink`` — so the join with the per-arc
|
|
373
|
+
# ``over`` frame matches the right column on each side. Build virtual
|
|
374
|
+
# Vars sharing v_angle's column ids but with renamed dim columns.
|
|
375
|
+
from polar_high.engine import Var
|
|
376
|
+
|
|
377
|
+
v_angle_src = Var(
|
|
378
|
+
name=v_angle.name + "__as_source",
|
|
379
|
+
dims=("source", "d", "t"),
|
|
380
|
+
frame=v_angle.frame.rename({"n": "source"}),
|
|
381
|
+
lower=v_angle.lower, upper=v_angle.upper,
|
|
382
|
+
)
|
|
383
|
+
v_angle_snk = Var(
|
|
384
|
+
name=v_angle.name + "__as_sink",
|
|
385
|
+
dims=("sink", "d", "t"),
|
|
386
|
+
frame=v_angle.frame.rename({"n": "sink"}),
|
|
387
|
+
lower=v_angle.lower, upper=v_angle.upper,
|
|
388
|
+
)
|
|
389
|
+
susc = d.p_connection_susceptance # Param over (p,)
|
|
390
|
+
rhs_angle_diff = (Where(v_angle_src, dc_arcs.select("p", "source"))
|
|
391
|
+
- Where(v_angle_snk, dc_arcs.select("p", "sink"))) * susc
|
|
392
|
+
|
|
393
|
+
m.add_cstr(
|
|
394
|
+
"dc_flow_eq",
|
|
395
|
+
over = over,
|
|
396
|
+
sense = "==",
|
|
397
|
+
lhs_terms = {"flow": lhs_flow},
|
|
398
|
+
rhs_terms = {"angle_diff": rhs_angle_diff},
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def nodeBalance_back_flow_terms(arcs, v_flow_back, p_unitsize,
|
|
403
|
+
p_step_duration) -> dict:
|
|
404
|
+
"""Return the v_flow_back contribution to nodeBalance, keyed by name.
|
|
405
|
+
|
|
406
|
+
The signed flow on a ``method_2way_1var_off`` arc is
|
|
407
|
+
``v_flow - v_flow_back``. ``v_flow`` is already plumbed into
|
|
408
|
+
``model.py``'s nb_terms via ``flow_to_n`` / source-side
|
|
409
|
+
``flow_from_nodeBalance_*`` sets. The ``-v_flow_back`` half mirrors
|
|
410
|
+
those terms with reversed signs:
|
|
411
|
+
|
|
412
|
+
* sink-side gets ``-v_flow_back × unitsize`` (back flow LEAVES sink)
|
|
413
|
+
* source-side gets ``+v_flow_back × unitsize`` (back flow ARRIVES at source)
|
|
414
|
+
|
|
415
|
+
*arcs* is the ``(p, source, sink)`` frame the back auxiliary spans
|
|
416
|
+
(the full ``method_2way_1var_off`` set — DC + non-DC). Returns ``{}``
|
|
417
|
+
when no back flow is active.
|
|
418
|
+
"""
|
|
419
|
+
if v_flow_back is None or arcs is None or arcs.height == 0:
|
|
420
|
+
return {}
|
|
421
|
+
|
|
422
|
+
from polar_high import Sum
|
|
423
|
+
|
|
424
|
+
# Sink side: back flow leaves the sink — subtract from sink balance.
|
|
425
|
+
# Build ``flow_from_n_back`` = (p, source, sink, n=sink): same shape
|
|
426
|
+
# as flow_to_n but flips the sign in nodeBalance. We use
|
|
427
|
+
# `Where(... )` with the frame having an explicit ``n`` column to
|
|
428
|
+
# collapse the (p, source, sink) dims into (n, d, t) via Sum.
|
|
429
|
+
sink_as_n = arcs.with_columns(n=pl.col("sink")).select(
|
|
430
|
+
"p", "source", "sink", "n")
|
|
431
|
+
src_as_n = arcs.with_columns(n=pl.col("source")).select(
|
|
432
|
+
"p", "source", "sink", "n")
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
# back flow at sink: -v_flow_back × unitsize × step_duration
|
|
436
|
+
"back_at_sink": -Sum(
|
|
437
|
+
Where(v_flow_back * p_unitsize, sink_as_n) * p_step_duration,
|
|
438
|
+
over=("p", "source", "sink"),
|
|
439
|
+
),
|
|
440
|
+
# back flow at source: +v_flow_back × unitsize × step_duration
|
|
441
|
+
"back_at_source": Sum(
|
|
442
|
+
Where(v_flow_back * p_unitsize, src_as_n) * p_step_duration,
|
|
443
|
+
over=("p", "source", "sink"),
|
|
444
|
+
),
|
|
445
|
+
}
|