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,660 @@
|
|
|
1
|
+
"""Commodity price ladder — tier-based stepwise pricing.
|
|
2
|
+
|
|
3
|
+
Mirrors the .mod's ``commodity_with_ladder`` family of constraints
|
|
4
|
+
(flextool.mod:3641-3745) and the per-tier objective term
|
|
5
|
+
(flextool.mod:2017-2028).
|
|
6
|
+
|
|
7
|
+
Concept
|
|
8
|
+
-------
|
|
9
|
+
For commodities listed in ``commodity_with_ladder`` (price_method =
|
|
10
|
+
``price_ladder_annual`` / ``price_ladder_cumulative``), the legacy
|
|
11
|
+
single-price commodity term is replaced by:
|
|
12
|
+
|
|
13
|
+
* ``v_trade[c, n, d, i] >= 0``: per-tier MWh purchased.
|
|
14
|
+
* **Balance**: Σ_i v_trade[c,n,d,i] · unitsize == period-aggregate
|
|
15
|
+
commodity flow through (c, n) (the same buy − sell expression that
|
|
16
|
+
the legacy ``pdtCommodity`` price term applies).
|
|
17
|
+
* **Per-tier cap** (annual):
|
|
18
|
+
Σ_n v_trade[c,n,d,i] · unitsize <= p_ladder_ann_quantity[c,i,d]
|
|
19
|
+
· f_d_k[d] − p_ladder_cum_realized_mwh[c,i,d]
|
|
20
|
+
(only when quantity < 1e29 sentinel).
|
|
21
|
+
* **Per-tier cap** (cumulative):
|
|
22
|
+
Σ_{n,d} v_trade[c,n,d,i] · unitsize
|
|
23
|
+
<= p_ladder_cum_quantity[c,i] · Σ_d f_d_k[d]
|
|
24
|
+
− Σ_d p_ladder_cum_realized_mwh[c,i,d]
|
|
25
|
+
(one constraint per (c, i) ∈ ci_ladder_cumulative).
|
|
26
|
+
* **Objective term**:
|
|
27
|
+
+ Σ price[c,i] · v_trade[c,n,d,i] · unitsize
|
|
28
|
+
· inflation_op[d] / period_share[d]
|
|
29
|
+
|
|
30
|
+
For single-solve fixtures, ``f_d_k[d] = 1.0`` for every realized
|
|
31
|
+
period and ``p_ladder_cum_realized_mwh = 0``, so the caps reduce to
|
|
32
|
+
their pre-refactor form:
|
|
33
|
+
|
|
34
|
+
annual_single: Σ_n v_trade · unitsize <= quantity[c,i,d]
|
|
35
|
+
cumulative_single: Σ_{n,d} v_trade · unitsize <= quantity[c,i] · |period_in_use|
|
|
36
|
+
|
|
37
|
+
The infinite-tier sentinel (1e30, written by flextool's input writer
|
|
38
|
+
for ``+Inf`` quantity) is filtered out — those tiers carry no cap,
|
|
39
|
+
matching the .mod's ``< 1e29`` predicate.
|
|
40
|
+
|
|
41
|
+
Inputs
|
|
42
|
+
------
|
|
43
|
+
* ``input/commodity_ladder_annual.csv`` (commodity, period, tier, price, quantity)
|
|
44
|
+
* ``input/commodity_ladder_cumulative.csv`` (commodity, tier, price, quantity)
|
|
45
|
+
* ``input/p_commodity_unitsize.csv`` (commodity, p_commodity_unitsize)
|
|
46
|
+
* ``solve_data/commodity_with_ladder.csv`` (commodity)
|
|
47
|
+
* ``solve_data/commodity_with_ladder_annual.csv`` (commodity)
|
|
48
|
+
* ``solve_data/commodity_with_ladder_cumulative.csv`` (commodity)
|
|
49
|
+
* ``solve_data/cnd_ladder_set.csv`` (commodity, node, period)
|
|
50
|
+
* ``solve_data/cndi_ladder_set.csv`` (commodity, node, period, tier)
|
|
51
|
+
* ``solve_data/cndi_ladder_ann_set.csv`` (commodity, node, period, tier)
|
|
52
|
+
* ``solve_data/cndi_ladder_cum_set.csv`` (commodity, node, period, tier)
|
|
53
|
+
* ``solve_data/ci_ladder_cumulative.csv`` (commodity, tier)
|
|
54
|
+
* ``solve_data/commodity__tier_ann.csv`` (commodity, tier)
|
|
55
|
+
* ``solve_data/f_d_k.csv`` (period, value)
|
|
56
|
+
* ``solve_data/ladder_cum_realized_mwh.csv`` (commodity, tier, period, p_ladder_cum_realized_mwh)
|
|
57
|
+
"""
|
|
58
|
+
from __future__ import annotations
|
|
59
|
+
|
|
60
|
+
from pathlib import Path
|
|
61
|
+
from typing import TYPE_CHECKING
|
|
62
|
+
|
|
63
|
+
import polars as pl
|
|
64
|
+
|
|
65
|
+
from polar_high import Param, Sum, Where
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if TYPE_CHECKING:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# .mod uses 1e30 as +Infinity sentinel; the constraint filter is
|
|
73
|
+
# ``quantity < 1e29``. Re-using the same threshold keeps fixtures and
|
|
74
|
+
# flextool source-of-truth aligned bit-for-bit.
|
|
75
|
+
_INF_SENTINEL = 1e29
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
# Feature detection
|
|
80
|
+
|
|
81
|
+
def has_feature(d) -> bool:
|
|
82
|
+
"""True iff at least one commodity uses a ladder price method."""
|
|
83
|
+
cwl = getattr(d, "commodity_with_ladder", None)
|
|
84
|
+
return cwl is not None and cwl.height > 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# Data loading
|
|
89
|
+
|
|
90
|
+
def _read_single_col(
|
|
91
|
+
path: Path,
|
|
92
|
+
col_in: str,
|
|
93
|
+
col_out: str,
|
|
94
|
+
*,
|
|
95
|
+
provider: "object | None" = None,
|
|
96
|
+
provider_key: str | None = None,
|
|
97
|
+
) -> pl.DataFrame | None:
|
|
98
|
+
df = _provider_get(provider, provider_key, path)
|
|
99
|
+
if df is None or df.height == 0:
|
|
100
|
+
return None
|
|
101
|
+
if col_in in df.columns and col_in != col_out:
|
|
102
|
+
df = df.rename({col_in: col_out})
|
|
103
|
+
return df.select(col_out)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _read_long_csv(
|
|
107
|
+
path: Path,
|
|
108
|
+
rename: dict[str, str],
|
|
109
|
+
*,
|
|
110
|
+
provider: "object | None" = None,
|
|
111
|
+
provider_key: str | None = None,
|
|
112
|
+
) -> pl.DataFrame | None:
|
|
113
|
+
df = _provider_get(provider, provider_key, path)
|
|
114
|
+
if df is None or df.height == 0:
|
|
115
|
+
return None
|
|
116
|
+
cols_present = {c: r for c, r in rename.items() if c in df.columns}
|
|
117
|
+
if cols_present:
|
|
118
|
+
df = df.rename(cols_present)
|
|
119
|
+
return df
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _provider_get(
|
|
123
|
+
provider: "object | None",
|
|
124
|
+
key: str | None,
|
|
125
|
+
path: Path, # noqa: ARG001 — kept for API; disk arm removed in Step 2.5
|
|
126
|
+
) -> "pl.DataFrame | None":
|
|
127
|
+
"""Resolve *key* via the Provider. Returns ``None`` when the
|
|
128
|
+
Provider doesn't carry *key* or the frame is empty. The
|
|
129
|
+
disk-fallback arm was removed in Step 2.5 — cascade modules
|
|
130
|
+
consume frames exclusively from the Provider.
|
|
131
|
+
"""
|
|
132
|
+
if provider is None or key is None or not provider.has(key):
|
|
133
|
+
return None
|
|
134
|
+
df = provider.get(key)
|
|
135
|
+
return df if df is not None and df.height > 0 else None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def load_data(
|
|
139
|
+
inp_dir: str | Path,
|
|
140
|
+
sd_dir: str | Path,
|
|
141
|
+
*,
|
|
142
|
+
provider: "object | None" = None,
|
|
143
|
+
) -> dict:
|
|
144
|
+
"""Load ladder frames via the Provider (disk-fallback for
|
|
145
|
+
off-cascade fixture loaders).
|
|
146
|
+
|
|
147
|
+
Returns a dict with keys matching ``FlexData`` field names. Values
|
|
148
|
+
are ``None`` (or empty frames) when the feature is inactive (every
|
|
149
|
+
fixture without ``price_method = price_ladder_*`` has header-only
|
|
150
|
+
frames, which return None here).
|
|
151
|
+
|
|
152
|
+
Step 2.5-F Phases D + E
|
|
153
|
+
-----------------------
|
|
154
|
+
|
|
155
|
+
``input/commodity_ladder_cumulative`` and
|
|
156
|
+
``input/commodity_ladder_annual`` are produced by
|
|
157
|
+
:func:`flextool.input_derivation._commodity_ladder.derive_commodity_ladder_cumulative`
|
|
158
|
+
/ ``derive_commodity_ladder_annual`` and placed on the
|
|
159
|
+
cascade-input Provider; in-cascade the Provider is the
|
|
160
|
+
authoritative source. The ``solve_data/commodity_with_ladder*``
|
|
161
|
+
frames are produced by
|
|
162
|
+
:mod:`flextool.input_derivation._commodity_ladder_sets`.
|
|
163
|
+
"""
|
|
164
|
+
inp = Path(inp_dir)
|
|
165
|
+
sd = Path(sd_dir)
|
|
166
|
+
|
|
167
|
+
blank = dict(
|
|
168
|
+
commodity_with_ladder=None,
|
|
169
|
+
commodity_with_ladder_annual=None,
|
|
170
|
+
commodity_with_ladder_cumulative=None,
|
|
171
|
+
cnd_ladder=None,
|
|
172
|
+
cndi_ladder=None,
|
|
173
|
+
cndi_ladder_ann=None,
|
|
174
|
+
cndi_ladder_cum=None,
|
|
175
|
+
ci_ladder_cumulative=None,
|
|
176
|
+
commodity__tier_ann=None,
|
|
177
|
+
commodity__tier_cum=None,
|
|
178
|
+
p_ladder_ann_price=None,
|
|
179
|
+
p_ladder_ann_quantity=None,
|
|
180
|
+
p_ladder_cum_price=None,
|
|
181
|
+
p_ladder_cum_quantity=None,
|
|
182
|
+
p_commodity_unitsize=None,
|
|
183
|
+
p_f_d_k=None,
|
|
184
|
+
p_ladder_cum_realized_mwh=None,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Provider key layout for ladder per-solve frames:
|
|
188
|
+
# * ``solve_data/commodity_with_ladder*`` — emitted by
|
|
189
|
+
# :func:`flextool.input_derivation._commodity_ladder_sets.derive_commodity_ladder_sets`
|
|
190
|
+
# into the cascade-input Provider under the parent-qualified key.
|
|
191
|
+
# * ``solve_data/cnd_ladder_set.csv`` & ``solve_data/cndi_ladder_*_set.csv``
|
|
192
|
+
# — emitted by :func:`_emit_per_solve.emit_per_solve_sets` under both
|
|
193
|
+
# the bare ``basename.csv`` and ``solve_data/basename.csv`` keys.
|
|
194
|
+
# * ``solve_data/ci_ladder_cumulative.csv`` — emitted by
|
|
195
|
+
# :func:`_emit_mid_sets.emit_ci_ladder_cumulative`.
|
|
196
|
+
# * ``solve_data/commodity__tier_ann.csv`` — emitted by
|
|
197
|
+
# :func:`_emit_leaf_sets`.
|
|
198
|
+
# In cascade mode (``csv_dump=False``) these files never reach disk; the
|
|
199
|
+
# Provider is the authoritative source. The disk arm survives for
|
|
200
|
+
# off-cascade fixture loaders that seed the workdir without a Provider.
|
|
201
|
+
cwl = _read_single_col(
|
|
202
|
+
sd / "commodity_with_ladder.csv", "commodity", "c",
|
|
203
|
+
provider=provider, provider_key="solve_data/commodity_with_ladder",
|
|
204
|
+
)
|
|
205
|
+
if cwl is None:
|
|
206
|
+
return blank
|
|
207
|
+
|
|
208
|
+
cwla = _read_single_col(
|
|
209
|
+
sd / "commodity_with_ladder_annual.csv", "commodity", "c",
|
|
210
|
+
provider=provider,
|
|
211
|
+
provider_key="solve_data/commodity_with_ladder_annual",
|
|
212
|
+
)
|
|
213
|
+
cwlc = _read_single_col(
|
|
214
|
+
sd / "commodity_with_ladder_cumulative.csv", "commodity", "c",
|
|
215
|
+
provider=provider,
|
|
216
|
+
provider_key="solve_data/commodity_with_ladder_cumulative",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
cnd = _read_long_csv(
|
|
220
|
+
sd / "cnd_ladder_set.csv",
|
|
221
|
+
{"commodity": "c", "node": "n", "period": "d"},
|
|
222
|
+
provider=provider, provider_key="solve_data/cnd_ladder_set.csv",
|
|
223
|
+
)
|
|
224
|
+
if cnd is not None:
|
|
225
|
+
cnd = cnd.select("c", "n", "d")
|
|
226
|
+
|
|
227
|
+
def _read_cndi(path: Path, key: str) -> pl.DataFrame | None:
|
|
228
|
+
df = _read_long_csv(
|
|
229
|
+
path,
|
|
230
|
+
{"commodity": "c", "node": "n", "period": "d", "tier": "i"},
|
|
231
|
+
provider=provider, provider_key=key,
|
|
232
|
+
)
|
|
233
|
+
if df is None:
|
|
234
|
+
return None
|
|
235
|
+
# Tier is integer-as-string in the CSV; keep it as string for
|
|
236
|
+
# consistent join keys (Param tables also keep it as Utf8).
|
|
237
|
+
return df.with_columns(pl.col("i").cast(pl.Utf8)).select("c", "n", "d", "i")
|
|
238
|
+
|
|
239
|
+
cndi = _read_cndi(
|
|
240
|
+
sd / "cndi_ladder_set.csv", "solve_data/cndi_ladder_set.csv")
|
|
241
|
+
cndi_ann = _read_cndi(
|
|
242
|
+
sd / "cndi_ladder_ann_set.csv", "solve_data/cndi_ladder_ann_set.csv")
|
|
243
|
+
cndi_cum = _read_cndi(
|
|
244
|
+
sd / "cndi_ladder_cum_set.csv", "solve_data/cndi_ladder_cum_set.csv")
|
|
245
|
+
|
|
246
|
+
ci_cum = _read_long_csv(
|
|
247
|
+
sd / "ci_ladder_cumulative.csv",
|
|
248
|
+
{"commodity": "c", "tier": "i"},
|
|
249
|
+
provider=provider, provider_key="solve_data/ci_ladder_cumulative.csv",
|
|
250
|
+
)
|
|
251
|
+
if ci_cum is not None:
|
|
252
|
+
ci_cum = ci_cum.with_columns(pl.col("i").cast(pl.Utf8)).select("c", "i")
|
|
253
|
+
|
|
254
|
+
ct_ann = _read_long_csv(
|
|
255
|
+
sd / "commodity__tier_ann.csv",
|
|
256
|
+
{"commodity": "c", "tier": "i"},
|
|
257
|
+
provider=provider, provider_key="solve_data/commodity__tier_ann.csv",
|
|
258
|
+
)
|
|
259
|
+
if ct_ann is not None:
|
|
260
|
+
ct_ann = ct_ann.with_columns(pl.col("i").cast(pl.Utf8)).select("c", "i")
|
|
261
|
+
# commodity__tier_cum mirrors the input CSV's (commodity, tier) projection.
|
|
262
|
+
ct_cum = None
|
|
263
|
+
cum_inp = _provider_get(
|
|
264
|
+
provider, "input/commodity_ladder_cumulative",
|
|
265
|
+
inp / "commodity_ladder_cumulative.csv",
|
|
266
|
+
)
|
|
267
|
+
if cum_inp is not None:
|
|
268
|
+
ct_cum = (cum_inp.rename({"commodity": "c", "tier": "i"})
|
|
269
|
+
.with_columns(pl.col("i").cast(pl.Utf8))
|
|
270
|
+
.select("c", "i").unique())
|
|
271
|
+
|
|
272
|
+
# ── Annual price/quantity Params (c, i, d) ─────────────────────────
|
|
273
|
+
p_ann_price = None
|
|
274
|
+
p_ann_quantity = None
|
|
275
|
+
ann = _provider_get(
|
|
276
|
+
provider, "input/commodity_ladder_annual",
|
|
277
|
+
inp / "commodity_ladder_annual.csv",
|
|
278
|
+
)
|
|
279
|
+
if ann is not None:
|
|
280
|
+
ann = (ann.rename({"commodity": "c", "tier": "i", "period": "d"})
|
|
281
|
+
.with_columns(pl.col("i").cast(pl.Utf8))
|
|
282
|
+
.select("c", "i", "d", "price", "quantity"))
|
|
283
|
+
# The CSV's "1e30" sentinel is written as a Float64 by polars.
|
|
284
|
+
p_ann_price = Param(
|
|
285
|
+
("c", "i", "d"),
|
|
286
|
+
ann.select("c", "i", "d",
|
|
287
|
+
value=pl.col("price").cast(pl.Float64)),
|
|
288
|
+
)
|
|
289
|
+
p_ann_quantity = Param(
|
|
290
|
+
("c", "i", "d"),
|
|
291
|
+
ann.select("c", "i", "d",
|
|
292
|
+
value=pl.col("quantity").cast(pl.Float64)),
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# ── Cumulative price/quantity Params (c, i) ─────────────────────────
|
|
296
|
+
p_cum_price = None
|
|
297
|
+
p_cum_quantity = None
|
|
298
|
+
cum = _provider_get(
|
|
299
|
+
provider, "input/commodity_ladder_cumulative",
|
|
300
|
+
inp / "commodity_ladder_cumulative.csv",
|
|
301
|
+
)
|
|
302
|
+
if cum is not None:
|
|
303
|
+
cum = (cum.rename({"commodity": "c", "tier": "i"})
|
|
304
|
+
.with_columns(pl.col("i").cast(pl.Utf8))
|
|
305
|
+
.select("c", "i", "price", "quantity"))
|
|
306
|
+
p_cum_price = Param(
|
|
307
|
+
("c", "i"),
|
|
308
|
+
cum.select("c", "i", value=pl.col("price").cast(pl.Float64)),
|
|
309
|
+
)
|
|
310
|
+
p_cum_quantity = Param(
|
|
311
|
+
("c", "i"),
|
|
312
|
+
cum.select("c", "i",
|
|
313
|
+
value=pl.col("quantity").cast(pl.Float64)),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Δ.12-drop: ``p_commodity_unitsize`` produced authoritatively by
|
|
317
|
+
# ``apply_direct_params.p_commodity_unitsize_from_source`` (Δ.4b).
|
|
318
|
+
# Seed dropped. The default-1.0 fallback in
|
|
319
|
+
# ``_commodity_unitsize_param`` covers the case when the override
|
|
320
|
+
# leaves the field None (unset feature).
|
|
321
|
+
p_unitsize = None
|
|
322
|
+
|
|
323
|
+
# ``p_f_d_k`` and ``p_ladder_cum_realized_mwh`` are produced by
|
|
324
|
+
# ``apply_derived_g`` BUT the 7 mismatch fixtures
|
|
325
|
+
# (``work_commodity_ladder_*`` etc.) skip auto-resolution and rely
|
|
326
|
+
# on the seed. Keep CSV reads.
|
|
327
|
+
# TODO(Δ.12c+): retire when ``_find_scenario`` covers underscore-
|
|
328
|
+
# variant fixtures or all fixtures explicitly pass db_reader=.
|
|
329
|
+
p_f_d_k = None
|
|
330
|
+
fdk = _provider_get(
|
|
331
|
+
provider, "solve_data/f_d_k.csv", sd / "f_d_k.csv",
|
|
332
|
+
)
|
|
333
|
+
if fdk is not None and fdk.height > 0:
|
|
334
|
+
# Provider-captured frame stores ``value`` as Utf8 (see
|
|
335
|
+
# ``_emit_period_calc._keyed_frame``); cast to Float64 to match
|
|
336
|
+
# the disk arm's ``pl.read_csv`` numeric inference.
|
|
337
|
+
fdk = (fdk.rename({"period": "d"})
|
|
338
|
+
.with_columns(pl.col("value").cast(pl.Float64))
|
|
339
|
+
.select("d", "value"))
|
|
340
|
+
p_f_d_k = Param(("d",), fdk)
|
|
341
|
+
|
|
342
|
+
# Phase 4.1a / 4.2-1g — the cross-roll cumulative-realized MWh
|
|
343
|
+
# accumulator rides on ``handoff/cumulative_commodity`` (written by
|
|
344
|
+
# the iteration-start translator from the prior solve's SolveHandoff).
|
|
345
|
+
# Empty/missing handoff (first solve or no prior carrier) leaves
|
|
346
|
+
# ``p_realized`` ``None`` so the mod default of 0 applies. The
|
|
347
|
+
# legacy ``solve_data/ladder_cum_realized_mwh`` CSV fallback was
|
|
348
|
+
# dropped in 4.2-1g — the translator pipeline is the only source.
|
|
349
|
+
from flextool.engine_polars import _provider_keys as K
|
|
350
|
+
from flextool.engine_polars._provider_translators import read_handoff_frame
|
|
351
|
+
p_realized = None
|
|
352
|
+
rel = read_handoff_frame(provider, K.HANDOFF_CUMULATIVE_COMMODITY)
|
|
353
|
+
if rel is not None and rel.height > 0:
|
|
354
|
+
value_col = "p_ladder_cum_realized_mwh" if "p_ladder_cum_realized_mwh" in rel.columns else "value"
|
|
355
|
+
rel = (rel.rename({"commodity": "c", "tier": "i", "period": "d",
|
|
356
|
+
value_col: "value"})
|
|
357
|
+
.with_columns(
|
|
358
|
+
pl.col("i").cast(pl.Utf8),
|
|
359
|
+
pl.col("value").cast(pl.Float64),
|
|
360
|
+
)
|
|
361
|
+
.select("c", "i", "d", "value"))
|
|
362
|
+
p_realized = Param(("c", "i", "d"), rel)
|
|
363
|
+
|
|
364
|
+
return dict(
|
|
365
|
+
commodity_with_ladder=cwl,
|
|
366
|
+
commodity_with_ladder_annual=cwla,
|
|
367
|
+
commodity_with_ladder_cumulative=cwlc,
|
|
368
|
+
cnd_ladder=cnd,
|
|
369
|
+
cndi_ladder=cndi,
|
|
370
|
+
cndi_ladder_ann=cndi_ann,
|
|
371
|
+
cndi_ladder_cum=cndi_cum,
|
|
372
|
+
ci_ladder_cumulative=ci_cum,
|
|
373
|
+
commodity__tier_ann=ct_ann,
|
|
374
|
+
commodity__tier_cum=ct_cum,
|
|
375
|
+
p_ladder_ann_price=p_ann_price,
|
|
376
|
+
p_ladder_ann_quantity=p_ann_quantity,
|
|
377
|
+
p_ladder_cum_price=p_cum_price,
|
|
378
|
+
p_ladder_cum_quantity=p_cum_quantity,
|
|
379
|
+
p_commodity_unitsize=p_unitsize,
|
|
380
|
+
p_f_d_k=p_f_d_k,
|
|
381
|
+
p_ladder_cum_realized_mwh=p_realized,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
# ---------------------------------------------------------------------------
|
|
386
|
+
# Variable + constraint emission
|
|
387
|
+
|
|
388
|
+
def add_variables(m, d) -> dict:
|
|
389
|
+
"""Declare ``v_trade[c, n, d, i]`` over ``cndi_ladder``.
|
|
390
|
+
|
|
391
|
+
Index domain is the union of the cumulative-tier and annual-tier
|
|
392
|
+
sets (``cndi_ladder_cum_set.csv`` ∪ ``cndi_ladder_ann_set.csv``).
|
|
393
|
+
"""
|
|
394
|
+
if not has_feature(d):
|
|
395
|
+
return {}
|
|
396
|
+
cndi = getattr(d, "cndi_ladder", None)
|
|
397
|
+
if cndi is None or cndi.height == 0:
|
|
398
|
+
return {}
|
|
399
|
+
v_trade = m.add_var("v_trade", ("c", "n", "d", "i"), cndi, lower=0.0)
|
|
400
|
+
return {"v_trade": v_trade}
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _commodity_unitsize_param(d) -> "Param":
|
|
404
|
+
"""Return the ``p_commodity_unitsize`` Param, defaulting to 1.0 for
|
|
405
|
+
every commodity in ``commodity_with_ladder``.
|
|
406
|
+
|
|
407
|
+
The .mod declares ``p_commodity_unitsize default 1.0`` so even an
|
|
408
|
+
empty CSV must yield a unit-mass coefficient.
|
|
409
|
+
"""
|
|
410
|
+
p = getattr(d, "p_commodity_unitsize", None)
|
|
411
|
+
if p is not None:
|
|
412
|
+
return p
|
|
413
|
+
cwl = d.commodity_with_ladder
|
|
414
|
+
return Param(
|
|
415
|
+
("c",),
|
|
416
|
+
cwl.select("c").unique().with_columns(value=pl.lit(1.0)),
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def add_constraints(
|
|
421
|
+
m, d, vars: dict, *,
|
|
422
|
+
v_flow=None,
|
|
423
|
+
p_unitsize=None,
|
|
424
|
+
p_slope=None,
|
|
425
|
+
p_step_duration=None,
|
|
426
|
+
p_timestep_weight=None,
|
|
427
|
+
flow_from_commodity_eff=None,
|
|
428
|
+
flow_from_commodity_noEff=None,
|
|
429
|
+
flow_to_commodity=None,
|
|
430
|
+
) -> None:
|
|
431
|
+
"""Emit balance constraint + per-tier cap constraints.
|
|
432
|
+
|
|
433
|
+
Parameters
|
|
434
|
+
----------
|
|
435
|
+
v_flow : Var
|
|
436
|
+
The model's process flow variable, ``v_flow[p, source, sink, d, t]``.
|
|
437
|
+
p_unitsize : Param
|
|
438
|
+
Process unitsize ``p_entity_unitsize[p]``.
|
|
439
|
+
p_slope : Param
|
|
440
|
+
Process efficiency slope ``pdtProcess_slope[p, d, t]``.
|
|
441
|
+
p_step_duration, p_timestep_weight : Param
|
|
442
|
+
Time-weight params.
|
|
443
|
+
flow_from_commodity_eff / flow_from_commodity_noEff : pl.DataFrame
|
|
444
|
+
Buy-side index frames ``(p, source, sink, c)``. ``source``
|
|
445
|
+
is the commodity node (n).
|
|
446
|
+
flow_to_commodity : pl.DataFrame
|
|
447
|
+
Sell-side index frame ``(p, source, sink, c)``. ``sink`` is
|
|
448
|
+
the commodity node (n).
|
|
449
|
+
"""
|
|
450
|
+
if not has_feature(d):
|
|
451
|
+
return
|
|
452
|
+
v_trade = vars.get("v_trade")
|
|
453
|
+
if v_trade is None:
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
p_unit_c = _commodity_unitsize_param(d)
|
|
457
|
+
cnd = d.cnd_ladder
|
|
458
|
+
|
|
459
|
+
# ── 1. commodity_ladder_balance: Σ_i v_trade · unitsize == flow ────
|
|
460
|
+
# LHS: Σ_i v_trade[c, n, d, i] * p_commodity_unitsize[c]
|
|
461
|
+
lhs_sum = Sum(v_trade * p_unit_c, over=("i",))
|
|
462
|
+
|
|
463
|
+
# RHS: period-aggregate commodity flow through (c, n) — sums over
|
|
464
|
+
# processes and timesteps. Mirrors the .mod's
|
|
465
|
+
# commodity_ladder_balance RHS (flextool.mod:3651-3670).
|
|
466
|
+
#
|
|
467
|
+
# The RHS is built by taking the legacy commodity-price flow
|
|
468
|
+
# expression and dropping the price coefficient. Here we have
|
|
469
|
+
# three index frames:
|
|
470
|
+
# * flow_from_commodity_eff (p, source=n, sink, c) — buy via efficient process
|
|
471
|
+
# * flow_from_commodity_noEff (p, source=n, sink, c) — buy via no-eff process
|
|
472
|
+
# * flow_to_commodity (p, source, sink=n, c) — sell into priced node
|
|
473
|
+
#
|
|
474
|
+
# Each contributes weighted v_flow. Result is summed over
|
|
475
|
+
# (p, source, sink, t) to give a (c, n, d) aggregate.
|
|
476
|
+
rhs_terms: list = []
|
|
477
|
+
weight = (p_step_duration * p_timestep_weight) if p_step_duration is not None else None
|
|
478
|
+
|
|
479
|
+
if (flow_from_commodity_eff is not None
|
|
480
|
+
and flow_from_commodity_eff.height > 0
|
|
481
|
+
and v_flow is not None and p_unitsize is not None
|
|
482
|
+
and p_slope is not None):
|
|
483
|
+
# source-as-n rename so the sum-over (p, source, sink) collapses
|
|
484
|
+
# into the (c, n, d) index; n equals source for buy flows.
|
|
485
|
+
idx_eff = (flow_from_commodity_eff
|
|
486
|
+
.with_columns(n=pl.col("source"))
|
|
487
|
+
.select("p", "source", "sink", "c", "n"))
|
|
488
|
+
term = Where(v_flow * p_unitsize * p_slope, idx_eff)
|
|
489
|
+
if weight is not None:
|
|
490
|
+
term = term * weight
|
|
491
|
+
rhs_terms.append(("buy_eff",
|
|
492
|
+
Sum(term, over=("p", "source", "sink", "t"))))
|
|
493
|
+
|
|
494
|
+
if (flow_from_commodity_noEff is not None
|
|
495
|
+
and flow_from_commodity_noEff.height > 0
|
|
496
|
+
and v_flow is not None and p_unitsize is not None):
|
|
497
|
+
idx_noEff = (flow_from_commodity_noEff
|
|
498
|
+
.with_columns(n=pl.col("source"))
|
|
499
|
+
.select("p", "source", "sink", "c", "n"))
|
|
500
|
+
term = Where(v_flow * p_unitsize, idx_noEff)
|
|
501
|
+
if weight is not None:
|
|
502
|
+
term = term * weight
|
|
503
|
+
rhs_terms.append(("buy_noEff",
|
|
504
|
+
Sum(term, over=("p", "source", "sink", "t"))))
|
|
505
|
+
|
|
506
|
+
if (flow_to_commodity is not None
|
|
507
|
+
and flow_to_commodity.height > 0
|
|
508
|
+
and v_flow is not None and p_unitsize is not None):
|
|
509
|
+
idx_sell = (flow_to_commodity
|
|
510
|
+
.with_columns(n=pl.col("sink"))
|
|
511
|
+
.select("p", "source", "sink", "c", "n"))
|
|
512
|
+
term = Where(v_flow * p_unitsize, idx_sell)
|
|
513
|
+
if weight is not None:
|
|
514
|
+
term = term * weight
|
|
515
|
+
rhs_terms.append(("sell",
|
|
516
|
+
-Sum(term, over=("p", "source", "sink", "t"))))
|
|
517
|
+
|
|
518
|
+
rhs_dict = dict(rhs_terms) if rhs_terms else {}
|
|
519
|
+
|
|
520
|
+
m.add_cstr(
|
|
521
|
+
"commodity_ladder_balance",
|
|
522
|
+
over = cnd,
|
|
523
|
+
sense = "==",
|
|
524
|
+
lhs_terms = {"trade": lhs_sum},
|
|
525
|
+
rhs_terms = rhs_dict,
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
# ── 2. ladder_tier_cap_annual_roll ─────────────────────────────────
|
|
529
|
+
# Σ_n v_trade[c, n, d, i] · unitsize <= quantity[c, i, d] · f_d_k[d]
|
|
530
|
+
# − realized[c, i, d]
|
|
531
|
+
# Active only when quantity < 1e29 (drop infinite-tier rows).
|
|
532
|
+
cndi_ann = d.cndi_ladder_ann
|
|
533
|
+
if (cndi_ann is not None and cndi_ann.height > 0
|
|
534
|
+
and d.p_ladder_ann_quantity is not None):
|
|
535
|
+
# Restrict cndi_ann to (c, i, d) tuples where quantity < 1e29.
|
|
536
|
+
ann_q = d.p_ladder_ann_quantity.frame
|
|
537
|
+
ann_q_finite = ann_q.filter(pl.col("value") < _INF_SENTINEL).select("c", "i", "d")
|
|
538
|
+
cndi_ann_finite = cndi_ann.join(
|
|
539
|
+
ann_q_finite, on=["c", "i", "d"], how="inner")
|
|
540
|
+
if cndi_ann_finite.height > 0:
|
|
541
|
+
cap_idx = cndi_ann_finite.select("c", "i", "d").unique()
|
|
542
|
+
# LHS: Σ_n v_trade · unitsize over the finite-cap rows.
|
|
543
|
+
lhs_cap_ann = Sum(
|
|
544
|
+
Where(v_trade * p_unit_c, cndi_ann_finite),
|
|
545
|
+
over=("n",),
|
|
546
|
+
)
|
|
547
|
+
# RHS: quantity · f_d_k − realized. When f_d_k is missing
|
|
548
|
+
# (single-solve fixtures may omit it for non-realized periods)
|
|
549
|
+
# we treat it as 1.0. The realized accumulator is
|
|
550
|
+
# likewise 0 by default.
|
|
551
|
+
rhs_cap_ann_terms: dict = {}
|
|
552
|
+
if d.p_f_d_k is not None:
|
|
553
|
+
rhs_cap_ann_terms["cap"] = (
|
|
554
|
+
d.p_ladder_ann_quantity * d.p_f_d_k
|
|
555
|
+
)
|
|
556
|
+
else:
|
|
557
|
+
rhs_cap_ann_terms["cap"] = d.p_ladder_ann_quantity
|
|
558
|
+
if d.p_ladder_cum_realized_mwh is not None:
|
|
559
|
+
rhs_cap_ann_terms["realized"] = -d.p_ladder_cum_realized_mwh
|
|
560
|
+
m.add_cstr(
|
|
561
|
+
"ladder_tier_cap_annual_roll",
|
|
562
|
+
over = cap_idx,
|
|
563
|
+
sense = "<=",
|
|
564
|
+
lhs_terms = {"trade": lhs_cap_ann},
|
|
565
|
+
rhs_terms = rhs_cap_ann_terms,
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
# ── 3. ladder_tier_cap_cumulative_roll ─────────────────────────────
|
|
569
|
+
# Σ_{n, d} v_trade[c, n, d, i] · unitsize
|
|
570
|
+
# <= quantity[c, i] · Σ_d f_d_k[d] − Σ_d realized[c, i, d]
|
|
571
|
+
# One constraint per (c, i) ∈ ci_ladder_cumulative with quantity < 1e29.
|
|
572
|
+
ci_cum = d.ci_ladder_cumulative
|
|
573
|
+
if (ci_cum is not None and ci_cum.height > 0
|
|
574
|
+
and d.p_ladder_cum_quantity is not None
|
|
575
|
+
and d.cndi_ladder_cum is not None
|
|
576
|
+
and d.cndi_ladder_cum.height > 0):
|
|
577
|
+
cum_q = d.p_ladder_cum_quantity.frame
|
|
578
|
+
cum_q_finite = cum_q.filter(pl.col("value") < _INF_SENTINEL).select("c", "i")
|
|
579
|
+
ci_cum_finite = ci_cum.join(cum_q_finite, on=["c", "i"], how="inner")
|
|
580
|
+
if ci_cum_finite.height > 0:
|
|
581
|
+
# LHS: Σ_{n, d} v_trade · unitsize over the cumulative-tier
|
|
582
|
+
# rows. Index by (c, i).
|
|
583
|
+
cndi_cum_finite = (d.cndi_ladder_cum
|
|
584
|
+
.join(ci_cum_finite, on=["c", "i"], how="inner"))
|
|
585
|
+
lhs_cap_cum = Sum(
|
|
586
|
+
Where(v_trade * p_unit_c, cndi_cum_finite),
|
|
587
|
+
over=("n", "d"),
|
|
588
|
+
)
|
|
589
|
+
# RHS: quantity · Σ_d f_d_k − Σ_d realized.
|
|
590
|
+
# Σ_d f_d_k is a scalar per the .mod's "sum {d in
|
|
591
|
+
# period_in_use} f_d_k[d]" convention. Compute it eagerly
|
|
592
|
+
# from the f_d_k Param.
|
|
593
|
+
sum_f_d_k = 1.0
|
|
594
|
+
if d.p_f_d_k is not None:
|
|
595
|
+
sum_f_d_k = float(d.p_f_d_k.frame["value"].sum())
|
|
596
|
+
rhs_cap_cum_terms: dict = {
|
|
597
|
+
"cap": d.p_ladder_cum_quantity * sum_f_d_k,
|
|
598
|
+
}
|
|
599
|
+
if d.p_ladder_cum_realized_mwh is not None:
|
|
600
|
+
# Σ_d realized — collapse the (c, i, d) Param to (c, i).
|
|
601
|
+
realized_ci = (d.p_ladder_cum_realized_mwh.frame
|
|
602
|
+
.group_by(["c", "i"]).agg(pl.col("value").sum())
|
|
603
|
+
.select("c", "i", "value"))
|
|
604
|
+
rhs_cap_cum_terms["realized"] = -Param(
|
|
605
|
+
("c", "i"), realized_ci)
|
|
606
|
+
m.add_cstr(
|
|
607
|
+
"ladder_tier_cap_cumulative_roll",
|
|
608
|
+
over = ci_cum_finite,
|
|
609
|
+
sense = "<=",
|
|
610
|
+
lhs_terms = {"trade": lhs_cap_cum},
|
|
611
|
+
rhs_terms = rhs_cap_cum_terms,
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def add_objective_terms(m, d, vars: dict, *,
|
|
616
|
+
p_unitsize_c=None,
|
|
617
|
+
p_inflation_op=None,
|
|
618
|
+
p_period_share=None):
|
|
619
|
+
"""Return the ladder per-tier price contribution to the objective.
|
|
620
|
+
|
|
621
|
+
Mirrors .mod:2017-2028:
|
|
622
|
+
|
|
623
|
+
+ Σ_{(c,n,d,i) ∈ cndi_ladder_cum} cum_price[c,i] · v_trade
|
|
624
|
+
· unitsize · inflation_op[d] / period_share[d]
|
|
625
|
+
+ Σ_{(c,n,d,i) ∈ cndi_ladder_ann} ann_price[c,i,d] · v_trade
|
|
626
|
+
· unitsize · inflation_op[d] / period_share[d]
|
|
627
|
+
"""
|
|
628
|
+
if not has_feature(d):
|
|
629
|
+
return None
|
|
630
|
+
v_trade = vars.get("v_trade")
|
|
631
|
+
if v_trade is None:
|
|
632
|
+
return None
|
|
633
|
+
if p_unitsize_c is None:
|
|
634
|
+
p_unitsize_c = _commodity_unitsize_param(d)
|
|
635
|
+
annualization = None
|
|
636
|
+
if p_inflation_op is not None and p_period_share is not None:
|
|
637
|
+
annualization = p_inflation_op / p_period_share
|
|
638
|
+
|
|
639
|
+
obj = None
|
|
640
|
+
|
|
641
|
+
if (d.cndi_ladder_cum is not None and d.cndi_ladder_cum.height > 0
|
|
642
|
+
and d.p_ladder_cum_price is not None):
|
|
643
|
+
term = Where(v_trade * p_unitsize_c, d.cndi_ladder_cum) \
|
|
644
|
+
* d.p_ladder_cum_price
|
|
645
|
+
if annualization is not None:
|
|
646
|
+
term = term * annualization
|
|
647
|
+
obj = Sum(term)
|
|
648
|
+
|
|
649
|
+
if (d.cndi_ladder_ann is not None and d.cndi_ladder_ann.height > 0
|
|
650
|
+
and d.p_ladder_ann_price is not None):
|
|
651
|
+
term = Where(v_trade * p_unitsize_c, d.cndi_ladder_ann) \
|
|
652
|
+
* d.p_ladder_ann_price
|
|
653
|
+
if annualization is not None:
|
|
654
|
+
term = term * annualization
|
|
655
|
+
if obj is None:
|
|
656
|
+
obj = Sum(term)
|
|
657
|
+
else:
|
|
658
|
+
obj = obj + Sum(term)
|
|
659
|
+
|
|
660
|
+
return obj
|