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,442 @@
|
|
|
1
|
+
"""Delayed-process feature (canonical use case: hydro / river chains).
|
|
2
|
+
|
|
3
|
+
This module covers the .mod's ``process_delayed`` family — processes
|
|
4
|
+
whose sink-side delivery at time ``t`` is fed by source-side flows at
|
|
5
|
+
*earlier* times ``t_``, weighted by a per-process delay profile. The
|
|
6
|
+
canonical use case is a **hydro / river network**: water released
|
|
7
|
+
upstream takes a known time to arrive downstream, so the
|
|
8
|
+
``conversion_indirect`` balance for the downstream unit must aggregate
|
|
9
|
+
upstream inflows over a delay window rather than read instantaneous
|
|
10
|
+
flow. Other plausible uses include thermal-storage charging chains
|
|
11
|
+
and freight-style logistics. ``water_pump`` / ``water_pump_delayed``
|
|
12
|
+
are the test fixtures that currently exercise this.
|
|
13
|
+
|
|
14
|
+
**Note on demand response.** An earlier draft of this module also
|
|
15
|
+
handled DR (``dr_decrease_demand`` / ``dr_increase_demand`` /
|
|
16
|
+
``dr_shift_demand``). DR was promoted out: those scenarios do not use
|
|
17
|
+
the delay tables — DR is just a regular storage + process pattern with
|
|
18
|
+
``bind_within_solve`` storage and a sign-bearing inflow at the demand
|
|
19
|
+
node, already handled by the storage and process blocks in
|
|
20
|
+
``flextool.model.build_flextool``. The DR fixtures' delay CSVs are
|
|
21
|
+
empty, so this module's ``has_feature`` returns False on them.
|
|
22
|
+
|
|
23
|
+
================================================================
|
|
24
|
+
What this module implements
|
|
25
|
+
================================================================
|
|
26
|
+
|
|
27
|
+
The .mod's ``conversion_indirect`` constraint (flextool.mod:2343)
|
|
28
|
+
splits the source-side flow term into an *undelayed* part (current-
|
|
29
|
+
time) and a *delayed* part (time-shifted via ``dtt__delay_duration``,
|
|
30
|
+
weighted by ``p_process_delay_weight``)::
|
|
31
|
+
|
|
32
|
+
sum {source : (p, source) in process_source_undelayed}
|
|
33
|
+
+ v_flow[p, source, p, d, t] * unitsize * source_coef
|
|
34
|
+
+ sum {source : (p, source) in process_source_delayed}
|
|
35
|
+
+ sum {(d, t_, t, td) in dtt__delay_duration
|
|
36
|
+
: (p, td) in process_delayed__duration}
|
|
37
|
+
+ v_flow[p, source, p, d, t_]
|
|
38
|
+
* unitsize * source_coef * delay_weight[p, td]
|
|
39
|
+
|
|
40
|
+
That is, for delayed (p, source), the inflow to the conversion
|
|
41
|
+
balance at sink-side time ``t`` is the *weighted sum* of source-side
|
|
42
|
+
flows at times ``t_`` paired with ``t`` through ``dtt__delay_duration``.
|
|
43
|
+
|
|
44
|
+
The .mod also has a *commented-out* analogous shift in
|
|
45
|
+
``nodeBalance_eq`` (flextool.mod:2148-2154) — kept commented in
|
|
46
|
+
upstream .mod, so we do **not** mirror it here. If a future scenario
|
|
47
|
+
needs it, the same machinery applies to ``flow_from_nodeBalance_*``.
|
|
48
|
+
|
|
49
|
+
================================================================
|
|
50
|
+
Module API
|
|
51
|
+
================================================================
|
|
52
|
+
|
|
53
|
+
has_feature(d) -> bool
|
|
54
|
+
True iff ``d.process_delayed`` is non-empty.
|
|
55
|
+
|
|
56
|
+
load_data(inp_dir, sd_dir) -> dict[str, ...]
|
|
57
|
+
Reads the delay CSVs. Returns a dict of new FlexData fields:
|
|
58
|
+
process_delayed, process_source_delayed,
|
|
59
|
+
process_source_undelayed,
|
|
60
|
+
dtt__delay_duration, p_process_delay_weight.
|
|
61
|
+
All values are ``None`` (or empty) when the feature is inactive.
|
|
62
|
+
|
|
63
|
+
delayed_input_expr(d, v_flow) -> Expr | None
|
|
64
|
+
Returns the delay-shifted source-side LHS aggregate that the
|
|
65
|
+
``conversion_indirect`` emission in ``model.py`` threads into
|
|
66
|
+
``lhs_terms["input_delayed"]`` alongside the undelayed term.
|
|
67
|
+
|
|
68
|
+
add_constraints(m, d, vars) -> None
|
|
69
|
+
Currently a no-op (the delay term is woven into the existing
|
|
70
|
+
``conversion_indirect`` via ``delayed_input_expr`` rather than
|
|
71
|
+
emitting a separate constraint).
|
|
72
|
+
|
|
73
|
+
add_objective_terms(m, d, vars, op_factor) -> None
|
|
74
|
+
No-op. Delayed processes do not add objective terms; their
|
|
75
|
+
costs propagate through commodity prices and slack penalties
|
|
76
|
+
already wired into ``build_flextool``.
|
|
77
|
+
|
|
78
|
+
================================================================
|
|
79
|
+
Integration with ``conversion_indirect`` in ``model.py``
|
|
80
|
+
================================================================
|
|
81
|
+
|
|
82
|
+
``model.py``'s ``conversion_indirect`` builds the source-side input
|
|
83
|
+
term from ``d.process_input_flows``. When the delay feature is
|
|
84
|
+
active, that block anti-joins ``process_input_flows`` against
|
|
85
|
+
``d.process_delayed`` (so the undelayed term skips delayed rows) and
|
|
86
|
+
adds the delay-shifted contribution by reading
|
|
87
|
+
``delayed_input_expr(d, v_flow)`` from this module — woven into the
|
|
88
|
+
same ``add_cstr`` call as the named ``lhs_terms["input_delayed"]``
|
|
89
|
+
entry, so a single constraint per (p, d, t) is preserved.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
from __future__ import annotations
|
|
93
|
+
|
|
94
|
+
from pathlib import Path
|
|
95
|
+
import polars as pl
|
|
96
|
+
|
|
97
|
+
from polar_high import Sum, Where, Param
|
|
98
|
+
# Engine imports kept light — we don't introduce new variable types.
|
|
99
|
+
|
|
100
|
+
from ._axis_enums import cast_dim
|
|
101
|
+
from ._emit_provider_io import _provider_key
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _provider_get(provider, path: "Path") -> "pl.DataFrame | None":
|
|
105
|
+
"""Provider-only fetch. Returns ``None`` when the Provider is
|
|
106
|
+
missing or doesn't carry *path*'s canonical key.
|
|
107
|
+
"""
|
|
108
|
+
if provider is None:
|
|
109
|
+
return None
|
|
110
|
+
key = _provider_key(path)
|
|
111
|
+
if not provider.has(key):
|
|
112
|
+
return None
|
|
113
|
+
return provider.get(key)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
# Feature detection
|
|
118
|
+
|
|
119
|
+
def has_feature(d) -> bool:
|
|
120
|
+
"""True iff ``d`` carries non-empty delayed-flow data."""
|
|
121
|
+
pd_set = getattr(d, "process_delayed", None)
|
|
122
|
+
return pd_set is not None and pd_set.height > 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# Data loading
|
|
127
|
+
|
|
128
|
+
def load_data(inp_dir: str | Path, sd_dir: str | Path, *,
|
|
129
|
+
provider=None) -> dict:
|
|
130
|
+
"""Read the delay-related solve_data CSVs.
|
|
131
|
+
|
|
132
|
+
Reads from ``solve_data/`` (where flextool.mod's preprocessing emits
|
|
133
|
+
the delay sets). ``inp_dir`` is accepted for API symmetry with the
|
|
134
|
+
other ``_load_*`` helpers in ``input.py`` but is not currently used —
|
|
135
|
+
the canonical sources are all in ``solve_data/`` (see flextool.mod
|
|
136
|
+
lines 525-527).
|
|
137
|
+
|
|
138
|
+
Post-Step-2.5 the loader consumes the live
|
|
139
|
+
:class:`FlexDataProvider`; the disk-fallback arms that previously
|
|
140
|
+
re-read the eight delay CSVs from disk are gone.
|
|
141
|
+
|
|
142
|
+
Returns a dict whose keys correspond to (proposed) ``FlexData`` field
|
|
143
|
+
names. When the feature is inactive (every scenario with no delayed
|
|
144
|
+
processes) every value is ``None``.
|
|
145
|
+
|
|
146
|
+
Proposed FlexData fields::
|
|
147
|
+
|
|
148
|
+
process_delayed pl.DataFrame | None # cols: (p,)
|
|
149
|
+
process_delayed__duration pl.DataFrame | None # cols: (p, td)
|
|
150
|
+
process_source_delayed pl.DataFrame | None # cols: (p, source)
|
|
151
|
+
process_source_undelayed pl.DataFrame | None # cols: (p, source)
|
|
152
|
+
process_source_sink_delayed pl.DataFrame | None # cols: (p, source, sink)
|
|
153
|
+
process_source_sink_undelayed pl.DataFrame | None # cols: (p, source, sink)
|
|
154
|
+
dtt__delay_duration pl.DataFrame | None # cols: (d, t_source, t_sink, td)
|
|
155
|
+
p_process_delay_weight Param | None # dims: (p, td)
|
|
156
|
+
"""
|
|
157
|
+
sd = Path(sd_dir)
|
|
158
|
+
|
|
159
|
+
blank = dict(
|
|
160
|
+
process_delayed = None,
|
|
161
|
+
process_delayed__duration = None,
|
|
162
|
+
process_source_delayed = None,
|
|
163
|
+
process_source_undelayed = None,
|
|
164
|
+
process_source_sink_delayed = None,
|
|
165
|
+
process_source_sink_undelayed = None,
|
|
166
|
+
dtt__delay_duration = None,
|
|
167
|
+
p_process_delay_weight = None,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
pd_path = sd / "process_delayed.csv"
|
|
171
|
+
pd_df = _provider_get(provider, pd_path)
|
|
172
|
+
if pd_df is None or pd_df.height == 0:
|
|
173
|
+
# Header-only / missing — flextool emits these even when no
|
|
174
|
+
# process is delayed (DR scenarios without the delay feature).
|
|
175
|
+
return blank
|
|
176
|
+
|
|
177
|
+
pd_df = pd_df.rename({"process": "p"}) if "process" in pd_df.columns else pd_df
|
|
178
|
+
|
|
179
|
+
# Δ.12-drop: ``process_delayed__duration`` (set frame, not Param)
|
|
180
|
+
# produced authoritatively by
|
|
181
|
+
# ``apply_direct_params.process_delayed__duration_from_source``.
|
|
182
|
+
# Seed dropped.
|
|
183
|
+
pdd_df = None
|
|
184
|
+
|
|
185
|
+
# process_source_(un)delayed: (process, source) frames
|
|
186
|
+
def _read_pse(name: str) -> pl.DataFrame | None:
|
|
187
|
+
p = sd / f"{name}.csv"
|
|
188
|
+
df = _provider_get(provider, p)
|
|
189
|
+
if df is None or df.height == 0:
|
|
190
|
+
return None
|
|
191
|
+
if "process" in df.columns:
|
|
192
|
+
df = df.rename({"process": "p"})
|
|
193
|
+
return df.select("p", "source")
|
|
194
|
+
|
|
195
|
+
pse_delayed = _read_pse("process_source_delayed")
|
|
196
|
+
pse_undelayed = _read_pse("process_source_undelayed")
|
|
197
|
+
|
|
198
|
+
# process_source_sink_(un)delayed: (process, source, sink) frames
|
|
199
|
+
def _read_psse(name: str) -> pl.DataFrame | None:
|
|
200
|
+
p = sd / f"{name}.csv"
|
|
201
|
+
df = _provider_get(provider, p)
|
|
202
|
+
if df is None or df.height == 0:
|
|
203
|
+
return None
|
|
204
|
+
if "process" in df.columns:
|
|
205
|
+
df = df.rename({"process": "p"})
|
|
206
|
+
return df.select("p", "source", "sink")
|
|
207
|
+
|
|
208
|
+
psse_delayed = _read_psse("process_source_sink_delayed")
|
|
209
|
+
psse_undelayed = _read_psse("process_source_sink_undelayed")
|
|
210
|
+
|
|
211
|
+
# Filter out (p, source) pairs where
|
|
212
|
+
# p_process_source_conversion_flow_coeff == 0: the .mod's
|
|
213
|
+
# conversion_indirect LHS multiplies each source-side flow by this
|
|
214
|
+
# coefficient, so a zero coef effectively drops the row from the
|
|
215
|
+
# input balance. Mirror that filter on the delayed side —
|
|
216
|
+
# _load_indirect already does the same on the undelayed side.
|
|
217
|
+
# Without this, water_pump's west→water_pump delayed input is
|
|
218
|
+
# double-counted in conversion_indirect, forcing the LP to dispatch
|
|
219
|
+
# extra battery/coal to compensate (visible as a ~0.099% gap on
|
|
220
|
+
# test_a_lot's multi-period parity).
|
|
221
|
+
inp = Path(inp_dir)
|
|
222
|
+
src_path = inp / "p_process_source_conversion_flow_coeff.csv"
|
|
223
|
+
srcdf = _provider_get(provider, src_path)
|
|
224
|
+
if srcdf is not None and srcdf.height > 0 and "p_process_source_conversion_flow_coeff" in srcdf.columns:
|
|
225
|
+
zero_src = (srcdf
|
|
226
|
+
.rename({"process": "p",
|
|
227
|
+
"p_process_source_conversion_flow_coeff": "coef"})
|
|
228
|
+
.with_columns(pl.col("coef").cast(pl.Float64, strict=False))
|
|
229
|
+
.filter(pl.col("coef") == 0.0)
|
|
230
|
+
.select("p", "source"))
|
|
231
|
+
if zero_src.height > 0:
|
|
232
|
+
if pse_delayed is not None:
|
|
233
|
+
pse_delayed = pse_delayed.join(
|
|
234
|
+
zero_src, on=["p", "source"], how="anti")
|
|
235
|
+
if psse_delayed is not None:
|
|
236
|
+
psse_delayed = psse_delayed.join(
|
|
237
|
+
zero_src, on=["p", "source"], how="anti")
|
|
238
|
+
|
|
239
|
+
# ``dtt__delay_duration`` / ``p_process_delay_weight`` are produced
|
|
240
|
+
# by ``apply_derived_g`` BUT the mismatch fixture
|
|
241
|
+
# ``work_delay_source_coef`` skips auto-resolution and relies on
|
|
242
|
+
# the seed. Keep CSV reads.
|
|
243
|
+
# TODO(Δ.12c+): retire when ``_find_scenario`` covers the mismatch
|
|
244
|
+
# fixtures.
|
|
245
|
+
dtt_path = sd / "dtt__delay_duration.csv"
|
|
246
|
+
dtt_df = None
|
|
247
|
+
raw = _provider_get(provider, dtt_path)
|
|
248
|
+
if raw is not None and raw.height > 0:
|
|
249
|
+
rename_map = {}
|
|
250
|
+
if "period" in raw.columns:
|
|
251
|
+
rename_map["period"] = "d"
|
|
252
|
+
if "time_source" in raw.columns:
|
|
253
|
+
rename_map["time_source"] = "t_source"
|
|
254
|
+
if "time_sink" in raw.columns:
|
|
255
|
+
rename_map["time_sink"] = "t_sink"
|
|
256
|
+
if "delay_duration" in raw.columns:
|
|
257
|
+
rename_map["delay_duration"] = "td"
|
|
258
|
+
dtt_df = raw.rename(rename_map).select("d", "t_source", "t_sink", "td")
|
|
259
|
+
|
|
260
|
+
pw_path = sd / "p_process_delay_weight.csv"
|
|
261
|
+
pw_param = None
|
|
262
|
+
raw = _provider_get(provider, pw_path)
|
|
263
|
+
if raw is not None and raw.height > 0:
|
|
264
|
+
rename_map = {}
|
|
265
|
+
if "process" in raw.columns:
|
|
266
|
+
rename_map["process"] = "p"
|
|
267
|
+
if "delay_duration" in raw.columns:
|
|
268
|
+
rename_map["delay_duration"] = "td"
|
|
269
|
+
pw_long = raw.rename(rename_map).select("p", "td", "value")
|
|
270
|
+
pw_param = Param(("p", "td"), pw_long)
|
|
271
|
+
|
|
272
|
+
return dict(
|
|
273
|
+
process_delayed = pd_df.select("p"),
|
|
274
|
+
process_delayed__duration = pdd_df,
|
|
275
|
+
process_source_delayed = pse_delayed,
|
|
276
|
+
process_source_undelayed = pse_undelayed,
|
|
277
|
+
process_source_sink_delayed = psse_delayed,
|
|
278
|
+
process_source_sink_undelayed = psse_undelayed,
|
|
279
|
+
dtt__delay_duration = dtt_df,
|
|
280
|
+
p_process_delay_weight = pw_param,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
# Constraint contribution
|
|
286
|
+
|
|
287
|
+
def delayed_input_expr(d, v_flow):
|
|
288
|
+
"""Return an Expr representing the delay-shifted source-side input
|
|
289
|
+
contribution to ``conversion_indirect``.
|
|
290
|
+
|
|
291
|
+
Shape: an Expr with open dims ``(p, d, t)`` (where ``t`` is the
|
|
292
|
+
*sink-side* time, matching ``conversion_indirect``'s axes).
|
|
293
|
+
|
|
294
|
+
Definition (mirrors flextool.mod:2348-2353)::
|
|
295
|
+
|
|
296
|
+
Σ_{source : (p, source) ∈ process_source_delayed}
|
|
297
|
+
Σ_{(d, t_source, t_sink, td) ∈ dtt__delay_duration
|
|
298
|
+
: (p, td) ∈ process_delayed__duration}
|
|
299
|
+
v_flow[p, source, p, d, t_source]
|
|
300
|
+
· unitsize[p] · delay_weight[p, td]
|
|
301
|
+
|
|
302
|
+
Implementation:
|
|
303
|
+
|
|
304
|
+
* Rename v_flow's ``t`` → ``t_source`` so the index aligns with
|
|
305
|
+
``dtt__delay_duration``.
|
|
306
|
+
* Rename v_flow's ``sink`` → ``p_sink`` and inner-join against the
|
|
307
|
+
delayed input flows (where sink = p, the process's own indirect
|
|
308
|
+
balance node).
|
|
309
|
+
* Inner-join with ``dtt__delay_duration`` on (d, t_source) and with
|
|
310
|
+
``process_delayed__duration`` on (p, td) — this attaches the
|
|
311
|
+
sink-side ``t`` (renamed back to ``t``) and the delay weight.
|
|
312
|
+
* Multiply by ``p_unitsize[p]`` and ``p_process_delay_weight[p, td]``.
|
|
313
|
+
* ``Sum`` over (source, td) leaves dims (p, d, t).
|
|
314
|
+
|
|
315
|
+
Returns ``None`` when there's no delayed-process data (caller should
|
|
316
|
+
use a no-op).
|
|
317
|
+
"""
|
|
318
|
+
if not has_feature(d):
|
|
319
|
+
return None
|
|
320
|
+
if d.dtt__delay_duration is None or d.p_process_delay_weight is None:
|
|
321
|
+
return None
|
|
322
|
+
if d.process_source_delayed is None or d.process_delayed__duration is None:
|
|
323
|
+
return None
|
|
324
|
+
if d.p_unitsize is None:
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
# Delayed input flows: (p, source, sink=p) — restrict process_input_flows
|
|
328
|
+
# (or build directly from process_source_sink_delayed where sink == p).
|
|
329
|
+
psse_delayed = d.process_source_sink_delayed
|
|
330
|
+
if psse_delayed is None or psse_delayed.height == 0:
|
|
331
|
+
return None
|
|
332
|
+
indirect_inputs_delayed = psse_delayed.filter(
|
|
333
|
+
# Cross-axis value compare: "sink" is e-axis, "p" is process
|
|
334
|
+
# axis. Per contract p ⊂ e; up-cast p to e for the equality
|
|
335
|
+
# so the compare runs in Enum without Utf8 materialisation.
|
|
336
|
+
pl.col("sink") == cast_dim(pl.col("p"), None, "e")
|
|
337
|
+
).select("p", "source", "sink")
|
|
338
|
+
if indirect_inputs_delayed.height == 0:
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
# Build the delay-mapping table: (p, source, sink, d, t_source, t, td, weight)
|
|
342
|
+
# Step 1: cross-product of (p, source, sink) ∈ delayed inputs with
|
|
343
|
+
# (p, td) ∈ process_delayed__duration → (p, source, sink, td)
|
|
344
|
+
pdd = d.process_delayed__duration
|
|
345
|
+
pst_td = indirect_inputs_delayed.join(pdd, on="p", how="inner")
|
|
346
|
+
if pst_td.height == 0:
|
|
347
|
+
return None
|
|
348
|
+
|
|
349
|
+
# Step 2: cross-join with dtt__delay_duration on (d, t_source, t_sink, td)
|
|
350
|
+
# via td (and the per-period mapping). dtt has columns
|
|
351
|
+
# (d, t_source, t_sink, td); join on td, get all (d, t_source, t_sink).
|
|
352
|
+
dtt = d.dtt__delay_duration
|
|
353
|
+
full_map = pst_td.join(dtt, on="td", how="inner")
|
|
354
|
+
if full_map.height == 0:
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
# Step 3: rename t_sink → t for the constraint axes; keep t_source
|
|
358
|
+
# as the v_flow time index, td for the weight lookup.
|
|
359
|
+
full_map = full_map.rename({"t_sink": "t"})
|
|
360
|
+
# Cols now: (p, source, sink, td, d, t_source, t)
|
|
361
|
+
|
|
362
|
+
# The Where filter against v_flow needs a frame whose columns are
|
|
363
|
+
# exactly the dims of v_flow (with t renamed to t_source on the
|
|
364
|
+
# variable side). We use Sum with an explicit ``where`` argument
|
|
365
|
+
# in two steps: rename + Where.
|
|
366
|
+
#
|
|
367
|
+
# v_flow has dims (p, source, sink, d, t). We first build a
|
|
368
|
+
# virtual variable with dims (p, source, sink, d, t_source) by
|
|
369
|
+
# renaming the v_flow frame, then Where it against full_map (which
|
|
370
|
+
# has all those plus t and td). The Where adds t and td as new
|
|
371
|
+
# open dims.
|
|
372
|
+
from polar_high.engine import Var as _Var
|
|
373
|
+
v_flow_at_source = _Var(
|
|
374
|
+
name=v_flow.name + "__at_t_source",
|
|
375
|
+
dims=("p", "source", "sink", "d", "t_source"),
|
|
376
|
+
frame=v_flow.frame.rename({"t": "t_source"}),
|
|
377
|
+
lower=v_flow.lower, upper=v_flow.upper, integer=v_flow.integer,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# Where(v_flow_at_source, full_map) joins on (p, source, sink, d, t_source)
|
|
381
|
+
# and adds (td, t) as new open dims — matching what we need.
|
|
382
|
+
expr = Where(v_flow_at_source, full_map)
|
|
383
|
+
# Multiply by unitsize[p] and delay weight[p, td].
|
|
384
|
+
expr = expr * d.p_unitsize * d.p_process_delay_weight
|
|
385
|
+
# Per flextool.mod:2573, the delayed source-side term also carries the
|
|
386
|
+
# ``p_process_source_conversion_flow_coeff[p, source]`` multiplier —
|
|
387
|
+
# same factor that the undelayed source-side term in
|
|
388
|
+
# ``conversion_indirect`` applies (model.py:1424-1425). When the
|
|
389
|
+
# Param is None (every coef=1 default) the multiplication is skipped
|
|
390
|
+
# to keep the Expr's open dims unchanged; when present it covers all
|
|
391
|
+
# surviving (p, source) pairs (defaulted to 1.0 by the loader where
|
|
392
|
+
# the CSV is silent). Without this, fixtures combining a delay with
|
|
393
|
+
# a non-default source coefficient diverge from flextool — see
|
|
394
|
+
# ``tests/test_flex_delay_source_coef.py``.
|
|
395
|
+
if getattr(d, "p_process_source_conversion_flow_coeff", None) is not None:
|
|
396
|
+
expr = expr * d.p_process_source_conversion_flow_coeff
|
|
397
|
+
|
|
398
|
+
# Sum over source, sink, td, t_source → leaves (p, d, t).
|
|
399
|
+
# NOTE: original version listed only ("source", "sink", "td") — but
|
|
400
|
+
# t_source is also an open dim of v_flow_at_source after the Where
|
|
401
|
+
# join. Without summing it, the constraint engine refuses the term
|
|
402
|
+
# because t_source is not in the constraint axes (p, d, t). See
|
|
403
|
+
# ``audit/integration_manifest.md`` "## merge step 4 issues".
|
|
404
|
+
expr = Sum(expr, over=("source", "sink", "td", "t_source"))
|
|
405
|
+
return expr
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def add_constraints(m, d, vars: dict) -> None:
|
|
409
|
+
"""Add the delayed-flow constraint contribution.
|
|
410
|
+
|
|
411
|
+
*PENDING DOWNSTREAM PATCH (see module docstring):* The merge agent
|
|
412
|
+
must wire the Expr returned by :func:`delayed_input_expr` into
|
|
413
|
+
``model.py``'s ``conversion_indirect`` ``add_cstr`` call as an
|
|
414
|
+
additional ``lhs_terms`` entry, **and** filter
|
|
415
|
+
``d.process_input_flows`` to exclude delayed processes. Until that
|
|
416
|
+
patch lands, this function is a no-op (delayed processes will not
|
|
417
|
+
solve correctly — their source-side input will be summed at the
|
|
418
|
+
*current* time without the delay shift).
|
|
419
|
+
|
|
420
|
+
``vars`` is the dict of decision-variable handles passed in by the
|
|
421
|
+
caller; it should contain ``v_flow`` for the Expr construction. When
|
|
422
|
+
the merge wiring is complete, this function will simply call
|
|
423
|
+
:func:`delayed_input_expr` and feed the result into the existing
|
|
424
|
+
``conversion_indirect`` constraint via the engine's named-term API.
|
|
425
|
+
"""
|
|
426
|
+
if not has_feature(d):
|
|
427
|
+
return
|
|
428
|
+
# Self-contained: nothing to emit until model.py is patched.
|
|
429
|
+
# The Expr is exposed via `delayed_input_expr` for the merge agent.
|
|
430
|
+
return
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def add_objective_terms(m, d, vars: dict, op_factor):
|
|
434
|
+
"""No delay-specific objective terms.
|
|
435
|
+
|
|
436
|
+
Costs are propagated through commodity prices on the source flows
|
|
437
|
+
(the ``v_flow`` referenced by the delay-shifted aggregate is the
|
|
438
|
+
same variable priced in the existing commodity-buy obj term) and
|
|
439
|
+
through any storage-state slacks the surrounding scenario already
|
|
440
|
+
emits. Returns ``None`` to signal "no contribution".
|
|
441
|
+
"""
|
|
442
|
+
return None
|