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,544 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def connection(par, s, v, r, debug):
|
|
5
|
+
"""Connection flow for periods and time"""
|
|
6
|
+
|
|
7
|
+
results = []
|
|
8
|
+
|
|
9
|
+
# Return timestep results
|
|
10
|
+
results.append((r.connection_dt, 'connection_dt_eee'))
|
|
11
|
+
results.append((r.connection_losses_dt, 'connection_losses_dt_eee'))
|
|
12
|
+
|
|
13
|
+
# Annualize: r.connection_d / r.connection_losses_d from calc_connections
|
|
14
|
+
# are already in MWh (v_flow × step_duration summed per period); dividing
|
|
15
|
+
# by complete_period_share_of_year scales them to MWh/year.
|
|
16
|
+
r.connection_d = r.connection_d.div(par.complete_period_share_of_year, axis=0)
|
|
17
|
+
r.connection_losses_d = r.connection_losses_d.div(par.complete_period_share_of_year, axis=0)
|
|
18
|
+
|
|
19
|
+
# Return period results
|
|
20
|
+
results.append((r.connection_d, 'connection_d_eee'))
|
|
21
|
+
results.append((r.connection_losses_d, 'connection_losses_d_eee'))
|
|
22
|
+
|
|
23
|
+
return results
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def connection_wards(par, s, v, r, debug):
|
|
27
|
+
"""Connection flow to right node and to left node for periods and time"""
|
|
28
|
+
|
|
29
|
+
results = []
|
|
30
|
+
|
|
31
|
+
# Return timestep results
|
|
32
|
+
results.append((r.connection_to_left_node__dt, 'connection_leftward_dt_eee'))
|
|
33
|
+
results.append((r.connection_to_right_node__dt, 'connection_rightward_dt_eee'))
|
|
34
|
+
|
|
35
|
+
# Return period results
|
|
36
|
+
results.append((r.connection_to_right_node__d, 'connection_rightward_d_eee'))
|
|
37
|
+
results.append((r.connection_to_left_node__d, 'connection_leftward_d_eee'))
|
|
38
|
+
|
|
39
|
+
return results
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def connection_cf(par, s, v, r, debug):
|
|
43
|
+
"""Connection capacity factors for periods"""
|
|
44
|
+
complete_hours = par.complete_period_share_of_year * 8760
|
|
45
|
+
connection_cols = r.process_sink_flow_d.columns[r.process_sink_flow_d.columns.get_level_values(0).isin(s.process_connection)]
|
|
46
|
+
connection_capacity = r.entity_all_capacity[connection_cols.droplevel(1).unique()].rename_axis('process', axis=1)
|
|
47
|
+
connection_capacity.columns = connection_capacity.columns.get_level_values(0)
|
|
48
|
+
# v_flow is MW (MWh/h); multiply by step_duration for MWh before the CF ratio.
|
|
49
|
+
results = (
|
|
50
|
+
r.connection_dt.abs()
|
|
51
|
+
.mul(par.step_duration, axis=0)
|
|
52
|
+
.groupby('period').sum()
|
|
53
|
+
.div(connection_capacity, level=0)
|
|
54
|
+
.div(complete_hours, axis=0)
|
|
55
|
+
)
|
|
56
|
+
# CF undefined for zero-capacity connections -> NaN (see unit_cf_outputNode).
|
|
57
|
+
results = results.replace([float('inf'), float('-inf')], float('nan'))
|
|
58
|
+
results.columns.names = ['connection']
|
|
59
|
+
return results, 'connection_cf_d_e'
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def reserves(par, s, v, r, debug):
|
|
63
|
+
"""Process reserves for timesteps and periods"""
|
|
64
|
+
results = []
|
|
65
|
+
|
|
66
|
+
# Timestep-level reserves
|
|
67
|
+
results.append((r.reserves_dt, 'process_reserve_upDown_node_dt_eppe'))
|
|
68
|
+
|
|
69
|
+
# Period-level reserves (average)
|
|
70
|
+
results.append((r.reserves_d, 'process_reserve_average_d_eppe'))
|
|
71
|
+
|
|
72
|
+
# Reserve price results
|
|
73
|
+
results.append((v.dual_reserve_balance, 'reserve_prices_dt_ppg'))
|
|
74
|
+
|
|
75
|
+
return results
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def investment_duals(par, s, v, r, debug):
|
|
79
|
+
"""Additional node results: prices, state, and slacks"""
|
|
80
|
+
results = []
|
|
81
|
+
|
|
82
|
+
# Synthesized effective investment dual per entity type.
|
|
83
|
+
# Complete signed marginal value (objective per MW) of one more MW of
|
|
84
|
+
# investment capacity, folding every binding regime (a/b/c). Positive
|
|
85
|
+
# means more investment in this entity would lower the objective.
|
|
86
|
+
combined = _synthesize_invest_dual(v, par, s)
|
|
87
|
+
if not combined.empty:
|
|
88
|
+
units = [c for c in combined.columns if c in s.process_unit]
|
|
89
|
+
connections = [c for c in combined.columns if c in s.process_connection]
|
|
90
|
+
nodes = [c for c in combined.columns if c in s.node]
|
|
91
|
+
if units:
|
|
92
|
+
results.append((combined[units], 'dual_invest_effective_unit_d_e'))
|
|
93
|
+
if connections:
|
|
94
|
+
results.append((combined[connections], 'dual_invest_effective_connection_d_e'))
|
|
95
|
+
if nodes:
|
|
96
|
+
results.append((combined[nodes], 'dual_invest_effective_node_d_e'))
|
|
97
|
+
|
|
98
|
+
return results
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _synthesize_invest_dual(v, par, s=None) -> "pd.DataFrame":
|
|
102
|
+
"""Complete SIGNED effective investment dual, per (entity, period).
|
|
103
|
+
|
|
104
|
+
Returns the full marginal value of one more MW of investment capacity in
|
|
105
|
+
an entity, in objective units per MW. Sign convention (user-confirmed):
|
|
106
|
+
POSITIVE means more investment in this entity would IMPROVE (lower) the
|
|
107
|
+
objective.
|
|
108
|
+
|
|
109
|
+
The value folds every binding investment regime (≤1 nonzero per
|
|
110
|
+
entity-period at a non-degenerate optimum). Each source is first put in
|
|
111
|
+
objective-per-MW units, then NEGATED, then summed:
|
|
112
|
+
|
|
113
|
+
- (b) upper-cap binds — the ``<=`` row duals
|
|
114
|
+
(``maxInvest_period``/``maxInvest_total``/``maxCumulative`` and the
|
|
115
|
+
group ``maxInvestGroup_*`` caps). Their raw HiGHS dual is NEGATIVE;
|
|
116
|
+
negate → POSITIVE (the cap holds the entity back, so more would help).
|
|
117
|
+
- (c) lower-floor binds — the ``>=`` row duals
|
|
118
|
+
(``minInvest_period``/``minInvest_total``/``minCumulative`` and the
|
|
119
|
+
group ``minInvestGroup_*`` floors). Their raw dual is POSITIVE;
|
|
120
|
+
negate → NEGATIVE (the floor over-forces capacity, so more would hurt).
|
|
121
|
+
- (a) not built — the v_invest COLUMN reduced cost
|
|
122
|
+
(``dual_invest_unit``/``connection``/``node``), correctly scaled by
|
|
123
|
+
``1/scale_the_objective`` (Increment 2). It is in objective per
|
|
124
|
+
v_invest-unit, so divide by ``entity_unitsize`` (mirroring the
|
|
125
|
+
standalone path in ``investment_duals``) to reach objective per MW;
|
|
126
|
+
the reduced cost is ≥0, so negate → NEGATIVE (unprofitable to build).
|
|
127
|
+
- (d) interior, binds nothing → 0.
|
|
128
|
+
|
|
129
|
+
ASYMMETRY (read off ``drop_levels.py``): ``dual_maxInvest_total`` is in
|
|
130
|
+
``_V_SOLVE_ONLY`` — it is collapsed to a single-row ``RangeIndex(1)``
|
|
131
|
+
(period-less) and so must be broadcast across the period axis of its
|
|
132
|
+
siblings before combining. ``dual_minInvest_total`` is in ``_V_DROP`` —
|
|
133
|
+
after dropping the solve level it retains a ``period`` index and is
|
|
134
|
+
ALREADY per-(entity, period), so it is added directly without broadcast.
|
|
135
|
+
|
|
136
|
+
The broadcast period axis is taken from a per-period sibling dual when
|
|
137
|
+
one is non-empty; otherwise (a binding total cap is the SOLE family, so
|
|
138
|
+
every per-period dual AND every regime-(a) col_dual is empty because
|
|
139
|
+
presolve dropped the fixed columns) it falls back to ``s.ed_invest`` —
|
|
140
|
+
the realized invest (entity, period) set, solution- and
|
|
141
|
+
presolve-independent — broadcasting each capped entity's value over
|
|
142
|
+
only that entity's own invest periods.
|
|
143
|
+
|
|
144
|
+
All duals are obj/MW; non-binding constraints contribute 0.
|
|
145
|
+
"""
|
|
146
|
+
import pandas as pd
|
|
147
|
+
|
|
148
|
+
# Collect all entity-level constraint duals, aligned to the same columns
|
|
149
|
+
# and index. Every contribution is appended already NEGATED so the final
|
|
150
|
+
# sum carries the signed obj/MW convention documented above.
|
|
151
|
+
entity_duals: list[pd.DataFrame] = []
|
|
152
|
+
|
|
153
|
+
# A reference (entity, period) frame to broadcast the period-less
|
|
154
|
+
# ``dual_maxInvest_total`` across. Prefer a per-period sibling that
|
|
155
|
+
# actually carries the period axis. The per-period constraint-dual
|
|
156
|
+
# families are each independently emission-gated, so when a per-entity
|
|
157
|
+
# ``maxInvest_total`` cap is the ONLY binding family they are all empty.
|
|
158
|
+
# The regime-(a) column reduced-cost frames
|
|
159
|
+
# (``dual_invest_unit``/``connection``/``node``) usually carry a
|
|
160
|
+
# realized-``period`` index, but a binding total cap FIXES ``v_invest``
|
|
161
|
+
# at its bound, so presolve drops those columns and their col_dual is
|
|
162
|
+
# absent too — leaving all eight candidates empty. Only
|
|
163
|
+
# ``period_ref.index`` is used for the broadcast, so the differing
|
|
164
|
+
# column axis name on these fallbacks is irrelevant.
|
|
165
|
+
period_ref: pd.DataFrame | None = next(
|
|
166
|
+
(
|
|
167
|
+
df
|
|
168
|
+
for df in (
|
|
169
|
+
v.dual_maxInvest_period,
|
|
170
|
+
v.dual_minInvest_period,
|
|
171
|
+
v.dual_maxCumulative,
|
|
172
|
+
v.dual_minCumulative,
|
|
173
|
+
v.dual_minInvest_total,
|
|
174
|
+
v.dual_invest_unit,
|
|
175
|
+
v.dual_invest_connection,
|
|
176
|
+
v.dual_invest_node,
|
|
177
|
+
)
|
|
178
|
+
if not df.empty
|
|
179
|
+
),
|
|
180
|
+
None,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Solution-/presolve-INDEPENDENT fallback for the realized invest-period
|
|
184
|
+
# axis: the ``s.ed_invest`` set (entity, period), already restricted to
|
|
185
|
+
# realized invest periods by ``drop_levels`` (joined to
|
|
186
|
+
# ``d_realize_invest``). When every dual-frame candidate above is empty
|
|
187
|
+
# (sole-binding total cap), broadcast each capped entity's max-total
|
|
188
|
+
# value across ONLY that entity's own invest periods — NOT the union of
|
|
189
|
+
# all model periods, which would fabricate spurious (entity, period)
|
|
190
|
+
# rows for entities not investable in those periods.
|
|
191
|
+
ed_invest_idx = None
|
|
192
|
+
if s is not None and getattr(s, 'ed_invest', None) is not None:
|
|
193
|
+
ei = s.ed_invest
|
|
194
|
+
if getattr(ei, 'names', None) is not None and 'entity' in ei.names \
|
|
195
|
+
and 'period' in ei.names:
|
|
196
|
+
ed_invest_idx = ei
|
|
197
|
+
|
|
198
|
+
# (b) Max-side ``<=`` duals: raw < 0 → negate → POSITIVE.
|
|
199
|
+
# ``dual_maxInvest_total`` is solve-only → broadcast over periods.
|
|
200
|
+
for df in (v.dual_maxInvest_period, v.dual_maxInvest_total, v.dual_maxCumulative):
|
|
201
|
+
if df.empty:
|
|
202
|
+
continue
|
|
203
|
+
if df is v.dual_maxInvest_total:
|
|
204
|
+
if period_ref is not None:
|
|
205
|
+
df = pd.DataFrame(
|
|
206
|
+
{c: df.iloc[0][c] for c in df.columns},
|
|
207
|
+
index=period_ref.index,
|
|
208
|
+
)
|
|
209
|
+
df.columns.name = 'entity'
|
|
210
|
+
elif ed_invest_idx is not None:
|
|
211
|
+
# Build per-(entity, period) from the invest set: each capped
|
|
212
|
+
# entity's value goes only on its own realized invest periods.
|
|
213
|
+
cols = {}
|
|
214
|
+
row = df.iloc[0]
|
|
215
|
+
for c in df.columns:
|
|
216
|
+
periods = ed_invest_idx[
|
|
217
|
+
ed_invest_idx.get_level_values('entity') == c
|
|
218
|
+
].get_level_values('period').unique()
|
|
219
|
+
if len(periods) == 0:
|
|
220
|
+
continue
|
|
221
|
+
cols[c] = pd.Series(row[c], index=periods)
|
|
222
|
+
if not cols:
|
|
223
|
+
continue
|
|
224
|
+
df = pd.DataFrame(cols)
|
|
225
|
+
df.index.name = 'period'
|
|
226
|
+
df.columns.name = 'entity'
|
|
227
|
+
else:
|
|
228
|
+
continue
|
|
229
|
+
entity_duals.append(df.mul(-1.0))
|
|
230
|
+
|
|
231
|
+
# (c) Min-side ``>=`` duals: raw > 0 → negate → NEGATIVE.
|
|
232
|
+
# ``dual_minInvest_total`` is ALREADY per-(entity, period) — add it
|
|
233
|
+
# directly, NO broadcast (the max-total asymmetry above).
|
|
234
|
+
for df in (v.dual_minInvest_period, v.dual_minInvest_total, v.dual_minCumulative):
|
|
235
|
+
if df.empty:
|
|
236
|
+
continue
|
|
237
|
+
entity_duals.append(df.mul(-1.0))
|
|
238
|
+
|
|
239
|
+
# (b)/(c) group caps/floors: expand group → member entities, then negate.
|
|
240
|
+
if not v.group_entity_invest.empty:
|
|
241
|
+
group_map = v.group_entity_invest # columns: group, entity
|
|
242
|
+
for group_df in (
|
|
243
|
+
v.dual_maxInvestGroup_period, v.dual_maxInvestGroup_total,
|
|
244
|
+
v.dual_maxInvestGroup_cumulative,
|
|
245
|
+
v.dual_minInvestGroup_period, v.dual_minInvestGroup_total,
|
|
246
|
+
v.dual_minInvestGroup_cumulative,
|
|
247
|
+
):
|
|
248
|
+
if group_df.empty:
|
|
249
|
+
continue
|
|
250
|
+
for group_name in group_df.columns:
|
|
251
|
+
members = group_map.loc[group_map['group'] == group_name, 'entity'].tolist()
|
|
252
|
+
if not members:
|
|
253
|
+
continue
|
|
254
|
+
# Create per-entity columns with the group's dual value.
|
|
255
|
+
group_series = group_df[group_name]
|
|
256
|
+
expanded = pd.DataFrame(
|
|
257
|
+
{entity: group_series for entity in members},
|
|
258
|
+
index=group_series.index,
|
|
259
|
+
)
|
|
260
|
+
expanded.columns.name = 'entity'
|
|
261
|
+
entity_duals.append(expanded.mul(-1.0))
|
|
262
|
+
|
|
263
|
+
# (a) Not-built regime — v_invest COLUMN reduced cost. Divide by
|
|
264
|
+
# entity_unitsize (obj/v_invest-unit → obj/MW), then negate. These
|
|
265
|
+
# frames have a period index and a 'unit'/'connection'/'node' column
|
|
266
|
+
# axis; rename to the common 'entity' axis before combining.
|
|
267
|
+
for col_dual in (v.dual_invest_unit, v.dual_invest_connection, v.dual_invest_node):
|
|
268
|
+
if col_dual.empty:
|
|
269
|
+
continue
|
|
270
|
+
per_mw = col_dual.div(par.entity_unitsize[col_dual.columns])
|
|
271
|
+
per_mw = per_mw.rename_axis('entity', axis=1)
|
|
272
|
+
entity_duals.append(per_mw.mul(-1.0))
|
|
273
|
+
|
|
274
|
+
if not entity_duals:
|
|
275
|
+
return pd.DataFrame()
|
|
276
|
+
|
|
277
|
+
# Align all DataFrames to a common (index, columns) and sum.
|
|
278
|
+
combined = entity_duals[0]
|
|
279
|
+
for df in entity_duals[1:]:
|
|
280
|
+
combined = combined.add(df, fill_value=0.0)
|
|
281
|
+
combined = combined.fillna(0.0)
|
|
282
|
+
|
|
283
|
+
# The synthesized dual is naturally in CUR/MW (units & connections) and
|
|
284
|
+
# CUR/MWh (storage nodes): the LP capacity is in MW(h) and the objective
|
|
285
|
+
# coefficient was lifted ×1000 from the per-kW input invest_cost
|
|
286
|
+
# (_derived_npv.py annuity). Divide by 1000 so the REPORTED marginal
|
|
287
|
+
# matches the input convention the user types — CUR/kW (units &
|
|
288
|
+
# connections) and CUR/kWh (storage nodes). Single choke point: every
|
|
289
|
+
# regime flows through `combined`, and the node-vs-process split happens
|
|
290
|
+
# downstream by column membership, so one divide is correct for all.
|
|
291
|
+
return combined.div(1000.0)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def co2_duals(par, s, v, r, debug):
|
|
295
|
+
"""CO2 emission-cap shadow prices in Currency / tCO2 (positive = cost).
|
|
296
|
+
|
|
297
|
+
Units: the polars ``co2_max_period`` / ``co2_max_total`` constraints
|
|
298
|
+
(``model.py``) carry the cap RHS in **raw tonnes** (input
|
|
299
|
+
``co2_max_period`` is ``[tCO2]``) against a tonnes LHS
|
|
300
|
+
(``v_flow·unitsize·p_co2_content``), so the raw dual is already
|
|
301
|
+
**Δobj / Δ(tonnes) = Currency/tCO2** — there is NO ÷1000. (The legacy
|
|
302
|
+
``.mod`` divided both sides by 1000 and the dual needed a ÷1000 to undo
|
|
303
|
+
it; the polars port does not, so applying ÷1000 here would under-report
|
|
304
|
+
the price 1000× — see the CO2-units investigation.) The dual is in NPV
|
|
305
|
+
currency because the objective discounts operational costs; for the
|
|
306
|
+
per-period cap we divide by inflation_factor_operations_yearly[d] to
|
|
307
|
+
recover nominal Currency/tCO2 at period d (mirroring
|
|
308
|
+
v_dual_node_balance). Sign is always flipped so binding caps show as
|
|
309
|
+
positive costs — negatives should ring alarm bells.
|
|
310
|
+
|
|
311
|
+
The cumulative (total) cap has no single period to un-discount against
|
|
312
|
+
(it spans all periods in the sum), so it is reported in NPV currency
|
|
313
|
+
(same convention as investment duals). Its scalar per group is
|
|
314
|
+
broadcast across periods for uniform (period × group) shape.
|
|
315
|
+
|
|
316
|
+
Caveat: the constraint LHS does not include p_timestep_weight whereas
|
|
317
|
+
the objective does. In rp (representative-period) scenarios the raw
|
|
318
|
+
dual therefore picks up an extra timestep_weight factor, inflating
|
|
319
|
+
shadow prices relative to chrono runs. Fixing that requires either
|
|
320
|
+
adding the weight to the constraint LHS in the mod, or post-dividing
|
|
321
|
+
by a representative value of timestep_weight per period.
|
|
322
|
+
"""
|
|
323
|
+
import pandas as pd
|
|
324
|
+
|
|
325
|
+
results = []
|
|
326
|
+
periods = s.d_realized_period
|
|
327
|
+
|
|
328
|
+
# Period-limited shadow price: (period × group) in nominal Currency/tCO2
|
|
329
|
+
if not v.dual_co2_max_period.empty:
|
|
330
|
+
co2_period_price = (-v.dual_co2_max_period).astype(float)
|
|
331
|
+
co2_period_price = co2_period_price.div(
|
|
332
|
+
par.inflation_factor_operations_yearly, axis=0
|
|
333
|
+
)
|
|
334
|
+
co2_period_price.columns.name = 'group'
|
|
335
|
+
co2_period_price.index.name = 'period'
|
|
336
|
+
results.append((co2_period_price, 'co2_price_period_d_g'))
|
|
337
|
+
|
|
338
|
+
# Cumulative limit shadow price: scalar per group, broadcast across periods.
|
|
339
|
+
# Kept in NPV currency (no single period to un-discount against).
|
|
340
|
+
if not v.dual_co2_max_total.empty:
|
|
341
|
+
total_row = v.dual_co2_max_total.iloc[0] # Series indexed by group
|
|
342
|
+
total_price = (-total_row).astype(float)
|
|
343
|
+
co2_total_price = pd.DataFrame(
|
|
344
|
+
{g: [total_price[g]] * len(periods) for g in total_price.index},
|
|
345
|
+
index=pd.Index(periods, name='period'),
|
|
346
|
+
)
|
|
347
|
+
co2_total_price.columns.name = 'group'
|
|
348
|
+
results.append((co2_total_price, 'co2_price_total_d_g'))
|
|
349
|
+
|
|
350
|
+
return results
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def inertia_results(par, s, v, r, debug):
|
|
354
|
+
"""Inertia results for groups and individual entities"""
|
|
355
|
+
|
|
356
|
+
results = []
|
|
357
|
+
|
|
358
|
+
# 1. Calculate unit_inertia for all (process, node) without groups
|
|
359
|
+
unit_inertia = pd.DataFrame(index=s.dt_realize_dispatch, columns=pd.MultiIndex.from_tuples([], names=['process', 'node']), dtype=float)
|
|
360
|
+
|
|
361
|
+
# === SOURCE-BASED INERTIA ===
|
|
362
|
+
s.process_source_with_inertia = par.process_source.columns[list(par.process_source.loc['inertia_constant'] > 0)]
|
|
363
|
+
pss_source_inertia = s.process_source_sink_alwaysProcess[
|
|
364
|
+
s.process_source_sink_alwaysProcess.droplevel('sink').isin(s.process_source_with_inertia)
|
|
365
|
+
]
|
|
366
|
+
s.process_source_with_inertia.names = ['process', 'node']
|
|
367
|
+
|
|
368
|
+
# Online processes - group by (process, source) since online_dt is indexed by process only
|
|
369
|
+
pss_source_online_inertia = pss_source_inertia[pss_source_inertia.get_level_values('process').isin(s.process_online)]
|
|
370
|
+
process_online_inertia = pss_source_online_inertia.droplevel('sink').unique()
|
|
371
|
+
process_online_inertia.names = ['process', 'node']
|
|
372
|
+
online_inertia_procs = process_online_inertia.get_level_values('process').unique()
|
|
373
|
+
|
|
374
|
+
unit_inertia[process_online_inertia] = ( r.process_online_dt[online_inertia_procs]
|
|
375
|
+
.mul(par.entity_unitsize[online_inertia_procs], axis=1, level=0)
|
|
376
|
+
.mul(par.process_source.loc['inertia_constant'][process_online_inertia]) )
|
|
377
|
+
|
|
378
|
+
# Flow processes
|
|
379
|
+
pss_source_flow_inertia = pss_source_inertia[~pss_source_inertia.get_level_values('process').isin(s.process_online)]
|
|
380
|
+
flow_inertia_cols = pss_source_flow_inertia.intersection(r.flow_dt.columns)
|
|
381
|
+
process_flow = flow_inertia_cols.droplevel('sink').unique()
|
|
382
|
+
par_process_source_inertia = par.process_source[process_flow].loc['inertia_constant']
|
|
383
|
+
par_process_source_inertia.index = flow_inertia_cols.join(par_process_source_inertia.index)
|
|
384
|
+
flows_weighted_source = (
|
|
385
|
+
r.flow_dt[flow_inertia_cols]
|
|
386
|
+
.mul(par_process_source_inertia) )
|
|
387
|
+
|
|
388
|
+
# Sum across sinks for each (process, source)
|
|
389
|
+
unit_inertia_source_flow = flows_weighted_source.T.groupby(level=['process', 'source']).sum().T
|
|
390
|
+
unit_inertia_source_flow.columns.names = ['process', 'node']
|
|
391
|
+
unit_inertia[unit_inertia_source_flow.columns] = unit_inertia_source_flow
|
|
392
|
+
|
|
393
|
+
# === SINK-BASED INERTIA ===
|
|
394
|
+
s.process_sink_with_inertia = par.process_sink.columns[list(par.process_sink.loc['inertia_constant'] > 0)]
|
|
395
|
+
pss_sink_inertia = s.process_source_sink_alwaysProcess[
|
|
396
|
+
s.process_source_sink_alwaysProcess.droplevel('source').isin(s.process_sink_with_inertia)
|
|
397
|
+
]
|
|
398
|
+
s.process_sink_with_inertia.names = ['process', 'node']
|
|
399
|
+
|
|
400
|
+
# Online processes - group by (process, sink) since online_dt is indexed by process only
|
|
401
|
+
pss_sink_online_inertia = pss_sink_inertia[pss_sink_inertia.get_level_values('process').isin(s.process_online)]
|
|
402
|
+
process_online_inertia = pss_sink_online_inertia.droplevel('source').unique()
|
|
403
|
+
process_online_inertia.names = ['process', 'node']
|
|
404
|
+
online_inertia_procs = process_online_inertia.get_level_values('process').unique()
|
|
405
|
+
|
|
406
|
+
unit_inertia[process_online_inertia] = ( r.process_online_dt[online_inertia_procs]
|
|
407
|
+
.mul(par.entity_unitsize[online_inertia_procs], axis=1, level=0)
|
|
408
|
+
.mul(par.process_sink.loc['inertia_constant'][process_online_inertia]) )
|
|
409
|
+
|
|
410
|
+
# Flow processes
|
|
411
|
+
pss_sink_flow_inertia = pss_sink_inertia[~pss_sink_inertia.get_level_values('process').isin(s.process_online)]
|
|
412
|
+
flow_inertia_cols = pss_sink_flow_inertia.intersection(r.flow_dt.columns)
|
|
413
|
+
process_flow = flow_inertia_cols.droplevel('source').unique()
|
|
414
|
+
par_process_sink_inertia = par.process_sink[process_flow].loc['inertia_constant']
|
|
415
|
+
par_process_sink_inertia.index = flow_inertia_cols.join(par_process_sink_inertia.index)
|
|
416
|
+
flows_weighted_sink = (
|
|
417
|
+
r.flow_dt[flow_inertia_cols]
|
|
418
|
+
.mul(par_process_sink_inertia) )
|
|
419
|
+
|
|
420
|
+
# Sum across sources for each (process, sink)
|
|
421
|
+
unit_inertia_sink_flow = flows_weighted_sink.T.groupby(level=['process', 'sink']).sum().T
|
|
422
|
+
unit_inertia_sink_flow.columns.names = ['process', 'node']
|
|
423
|
+
unit_inertia[unit_inertia_sink_flow.columns] = unit_inertia_sink_flow
|
|
424
|
+
|
|
425
|
+
# 2. Add group dimension by joining with group_node
|
|
426
|
+
group_unit_inertia = pd.DataFrame(index=s.dt_realize_dispatch, columns=pd.MultiIndex.from_tuples([], names=['group', 'process', 'node']), dtype=float)
|
|
427
|
+
|
|
428
|
+
for g in s.groupInertia:
|
|
429
|
+
# Get (process, node) pairs for this group
|
|
430
|
+
group_pn = s.group_node[s.group_node.get_level_values('group') == g].droplevel('group')
|
|
431
|
+
# Filter unit_inertia to these columns
|
|
432
|
+
cols = unit_inertia.columns.intersection(group_pn)
|
|
433
|
+
# Add group level to columns
|
|
434
|
+
group_cols = pd.MultiIndex.from_arrays(
|
|
435
|
+
[[g] * len(cols), cols.get_level_values(0), cols.get_level_values(1)],
|
|
436
|
+
names=['group', 'process', 'node']
|
|
437
|
+
)
|
|
438
|
+
group_unit_inertia[group_cols] = unit_inertia[cols].values
|
|
439
|
+
|
|
440
|
+
results.append((group_unit_inertia, 'nodeGroup_unit_node_inertia_dt_gee'))
|
|
441
|
+
|
|
442
|
+
# 3. Group inertia - sum by group
|
|
443
|
+
group_inertia = group_unit_inertia.T.groupby(level='group').sum().T
|
|
444
|
+
results.append((group_inertia, 'nodeGroup_inertia_dt_g'))
|
|
445
|
+
|
|
446
|
+
# 4. Largest flow per group
|
|
447
|
+
largest_flow = pd.DataFrame(index=s.dt_realize_dispatch, dtype=float)
|
|
448
|
+
largest_flow.columns.name = 'group'
|
|
449
|
+
|
|
450
|
+
for g in s.groupInertia:
|
|
451
|
+
group_nodes = s.group_node[s.group_node.get_level_values('group') == g].get_level_values('node')
|
|
452
|
+
process_sink_in_group = s.process_sink[s.process_sink.get_level_values('sink').isin(group_nodes)]
|
|
453
|
+
pss_sink = s.process_source_sink_alwaysProcess[
|
|
454
|
+
s.process_source_sink_alwaysProcess.droplevel('source').isin(process_sink_in_group)
|
|
455
|
+
]
|
|
456
|
+
|
|
457
|
+
flow_cols = pss_sink.intersection(r.flow_dt.columns)
|
|
458
|
+
largest_flow[g] = r.flow_dt[flow_cols].max(axis=1)
|
|
459
|
+
|
|
460
|
+
results.append((largest_flow, 'nodeGroup_inertia_largest_flow_dt_g'))
|
|
461
|
+
|
|
462
|
+
return results
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def slack_variables(par, s, v, r, debug):
|
|
466
|
+
"""Slack variables for reserves, non-synchronous, inertia, and capacity margin"""
|
|
467
|
+
|
|
468
|
+
results = []
|
|
469
|
+
|
|
470
|
+
# 1. Reserve slack variables
|
|
471
|
+
results.append((r.q_reserves_dt, 'nodeGroup_slack_reserve_dt_eeg'))
|
|
472
|
+
|
|
473
|
+
# 2. Non-synchronous slack variables
|
|
474
|
+
results.append((r.q_non_synchronous_dt, 'nodeGroup_slack_nonsync_dt_g'))
|
|
475
|
+
|
|
476
|
+
# 3. Inertia slack variables
|
|
477
|
+
results.append((r.q_inertia_dt, 'nodeGroup_slack_inertia_dt_g'))
|
|
478
|
+
|
|
479
|
+
# 4. Capacity margin slack variables (for investment periods only)
|
|
480
|
+
results.append((r.q_capacity_margin_d_not_annualized, 'nodeGroup_slack_capacity_margin_d_g'))
|
|
481
|
+
|
|
482
|
+
# 5. Period-level slack variables (annualized)
|
|
483
|
+
results.append((r.q_reserves_d, 'nodeGroup_slack_reserve_d_eeg'))
|
|
484
|
+
results.append((r.q_inertia_d, 'nodeGroup_slack_inertia_d_g'))
|
|
485
|
+
results.append((r.q_non_synchronous_d, 'nodeGroup_slack_nonsync_d_g'))
|
|
486
|
+
|
|
487
|
+
return results
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def dc_power_flow(par, s, v, r, debug):
|
|
491
|
+
"""DC power flow voltage angles and angle differences per connection"""
|
|
492
|
+
|
|
493
|
+
results = []
|
|
494
|
+
|
|
495
|
+
# Voltage angles per node (may be empty when no DC PF nodes exist)
|
|
496
|
+
if not r.angle_dt.empty:
|
|
497
|
+
results.append((r.angle_dt, 'dc_angle_dt_e'))
|
|
498
|
+
|
|
499
|
+
# Angle difference per connection
|
|
500
|
+
if not r.connection_angle_diff_dt.empty:
|
|
501
|
+
results.append((r.connection_angle_diff_dt, 'dc_angle_diff_dt_e'))
|
|
502
|
+
|
|
503
|
+
# DC PF structural sets (Index objects become parquet mapping files)
|
|
504
|
+
if not s.node_dc_power_flow.empty:
|
|
505
|
+
results.append((s.node_dc_power_flow, 'node_dc_power_flow'))
|
|
506
|
+
if not s.connection_dc_power_flow.empty:
|
|
507
|
+
results.append((s.connection_dc_power_flow, 'connection_dc_power_flow'))
|
|
508
|
+
|
|
509
|
+
return results if results else None
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def input_sets(par, s, v, r, debug):
|
|
513
|
+
"""Input sets needed for scenario results"""
|
|
514
|
+
|
|
515
|
+
results = []
|
|
516
|
+
results.append((s.group_node, 'group_node'))
|
|
517
|
+
results.append((s.group_process, 'group_process'))
|
|
518
|
+
results.append((s.group_process_node, 'group_process_node'))
|
|
519
|
+
results.append((s.nodeGroupIndicators, 'nodeGroupIndicators'))
|
|
520
|
+
results.append((s.flowGroupIndicators, 'flowGroupIndicators'))
|
|
521
|
+
results.append((s.nodeGroupDispatch, 'nodeGroupDispatch'))
|
|
522
|
+
results.append((s.nodeGroupDispatch__connection_Not_in_aggregate, 'nodeGroupDispatch__connection_Not_in_aggregate'))
|
|
523
|
+
results.append((s.nodeGroupDispatch__process__unit__to_node_Not_in_aggregate, 'nodeGroupDispatch__process__unit__to_node_Not_in_aggregate'))
|
|
524
|
+
results.append((s.nodeGroupDispatch__process__node__to_unit_Not_in_aggregate, 'nodeGroupDispatch__process__node__to_unit_Not_in_aggregate'))
|
|
525
|
+
results.append((s.nodeGroupDispatch__process__connection__to_node_Not_in_aggregate, 'nodeGroupDispatch__process__connection__to_node_Not_in_aggregate'))
|
|
526
|
+
results.append((s.nodeGroupDispatch__process__node__to_connection_Not_in_aggregate, 'nodeGroupDispatch__process__node__to_connection_Not_in_aggregate'))
|
|
527
|
+
results.append((s.nodeGroupDispatch__processGroup_Unit_to_group, 'nodeGroupDispatch__processGroup_Unit_to_group'))
|
|
528
|
+
results.append((s.nodeGroupDispatch__processGroup__process__unit__to_node, 'nodeGroupDispatch__processGroup__process__unit__to_node'))
|
|
529
|
+
results.append((s.nodeGroupDispatch__processGroup_Group_to_unit, 'nodeGroupDispatch__processGroup_Group_to_unit'))
|
|
530
|
+
results.append((s.nodeGroupDispatch__processGroup__process__node__to_unit, 'nodeGroupDispatch__processGroup__process__node__to_unit'))
|
|
531
|
+
results.append((s.nodeGroupDispatch__processGroup_Connection, 'nodeGroupDispatch__processGroup_Connection'))
|
|
532
|
+
results.append((s.nodeGroupDispatch__processGroup__process__connection__to_node, 'nodeGroupDispatch__processGroup__process__connection__to_node'))
|
|
533
|
+
results.append((s.nodeGroupDispatch__processGroup__process__node__to_connection, 'nodeGroupDispatch__processGroup__process__node__to_connection'))
|
|
534
|
+
results.append((s.nodeGroupDispatch__process_fully_inside, 'nodeGroupDispatch__process_fully_inside'))
|
|
535
|
+
results.append((par.node_inflow, 'node_inflow__dt'))
|
|
536
|
+
|
|
537
|
+
# Per-period years_represented — used by plot rules 'y' and 'z' to convert
|
|
538
|
+
# annualized outputs to horizon totals or years-weighted averages.
|
|
539
|
+
years_d = par.years_represented_d.rename('years_represented').to_frame()
|
|
540
|
+
years_d.index.name = 'period'
|
|
541
|
+
years_d.columns.name = 'param'
|
|
542
|
+
results.append((years_d, 'years_represented__d'))
|
|
543
|
+
|
|
544
|
+
return results
|