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,107 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def compute_capacity_and_flows(par, s, v, r) -> None:
|
|
5
|
+
"""Compute capacity, process online state, and flow-related quantities."""
|
|
6
|
+
step_duration = par.step_duration
|
|
7
|
+
unitsize = par.entity_unitsize
|
|
8
|
+
|
|
9
|
+
# hours_in_realized_period
|
|
10
|
+
hours_in_realized_period = step_duration.groupby(level='period').sum()
|
|
11
|
+
hours_in_realized_period = hours_in_realized_period.reindex(s.d_realized_period)
|
|
12
|
+
r.hours_in_realized_period = hours_in_realized_period
|
|
13
|
+
r.realized_period_share_of_year = hours_in_realized_period / 8760
|
|
14
|
+
|
|
15
|
+
s.nb = s.node_balance.union(s.node_balance_period)
|
|
16
|
+
|
|
17
|
+
# entity_all_capacity: read directly from model output (existing + cumulative invest - divest)
|
|
18
|
+
# The model uses edd_invest set to correctly track which investments apply to which periods,
|
|
19
|
+
# handling both single-solve and multi-solve scenarios.
|
|
20
|
+
r.entity_all_capacity = par.entity_all_capacity.copy()
|
|
21
|
+
# Drop solve level from index — downstream code expects (period,) only.
|
|
22
|
+
# For rolling scenarios, keep only the last solve per period.
|
|
23
|
+
if r.entity_all_capacity.index.nlevels > 1:
|
|
24
|
+
r.entity_all_capacity = r.entity_all_capacity.droplevel('solve')
|
|
25
|
+
r.entity_all_capacity = r.entity_all_capacity[~r.entity_all_capacity.index.duplicated(keep='last')]
|
|
26
|
+
r.entity_all_capacity.columns.name = 'process' # Required for level=0 matching in VRE calculations
|
|
27
|
+
|
|
28
|
+
# process_online_dt
|
|
29
|
+
r.process_online_dt = v.online_linear.add(v.online_integer, fill_value=0)
|
|
30
|
+
|
|
31
|
+
# flow_dt: applies unit-size scaling and slope/section transformations
|
|
32
|
+
s.process_source_sink_alwaysProcess = s.process_method_sources_sinks.droplevel(['method', 'orig_source', 'orig_sink'])
|
|
33
|
+
s.process_source_sink_alwaysProcess.names = ['process', 'source', 'sink']
|
|
34
|
+
|
|
35
|
+
r.flow_dt = pd.DataFrame(
|
|
36
|
+
index=s.dt_realize_dispatch,
|
|
37
|
+
columns=s.process_source_sink_alwaysProcess,
|
|
38
|
+
dtype=float
|
|
39
|
+
)
|
|
40
|
+
slope = par.process_slope
|
|
41
|
+
section = par.process_section
|
|
42
|
+
|
|
43
|
+
for row in s.process_method_sources_sinks:
|
|
44
|
+
p = row[0]
|
|
45
|
+
method = row[1]
|
|
46
|
+
orig_source, orig_sink = row[2], row[3]
|
|
47
|
+
always_source, always_sink = row[4], row[5]
|
|
48
|
+
|
|
49
|
+
flow_val = v.flow[[(p, orig_source, orig_sink)]] * unitsize[p]
|
|
50
|
+
|
|
51
|
+
# method_2way_1var_off arcs carry a single signed flow
|
|
52
|
+
# ``v_flow - v_flow_back``: the forward variable ``v_flow >= 0``
|
|
53
|
+
# plus a non-negative reverse auxiliary ``v_flow_back``. Subtract
|
|
54
|
+
# the back auxiliary so the reported flow is the true signed net
|
|
55
|
+
# (negative = sink->source), matching the engine's nodeBalance.
|
|
56
|
+
flow_back = getattr(v, "flow_back", None)
|
|
57
|
+
if (flow_back is not None and not flow_back.empty
|
|
58
|
+
and (p, orig_source, orig_sink) in flow_back.columns):
|
|
59
|
+
# ``flow_back`` shares ``v.flow``'s (solve, period, time) row
|
|
60
|
+
# index, so its column lines up element-wise with ``flow_val``
|
|
61
|
+
# (which is ``v.flow[[col]] * unitsize``). Subtract by raw
|
|
62
|
+
# values to stay agnostic to row-index level differences.
|
|
63
|
+
back_vals = (flow_back[(p, orig_source, orig_sink)] * unitsize[p]).values
|
|
64
|
+
flow_val = flow_val.sub(back_vals, axis=0)
|
|
65
|
+
|
|
66
|
+
if (method in s.method_1var_per_way and
|
|
67
|
+
p not in s.process_profile and
|
|
68
|
+
orig_source == always_source and orig_sink != always_sink):
|
|
69
|
+
flow_val = flow_val.mul(slope[p], axis=0)
|
|
70
|
+
if p in s.process_unit:
|
|
71
|
+
# FlexData only populates
|
|
72
|
+
# ``p_process_{sink,source}_conversion_flow_coeff`` when a
|
|
73
|
+
# non-default value is present (default = 1.0). Use a safe
|
|
74
|
+
# lookup so direct-method units without explicit coefficients
|
|
75
|
+
# (the common case) divide by 1.0 instead of raising
|
|
76
|
+
# KeyError.
|
|
77
|
+
sink_coef = par.process_sink_conversion_flow_coeff.get(
|
|
78
|
+
(p, orig_sink), 1.0,
|
|
79
|
+
)
|
|
80
|
+
src_coef = par.process_source_conversion_flow_coeff.get(
|
|
81
|
+
(p, orig_source), 1.0,
|
|
82
|
+
)
|
|
83
|
+
flow_val /= (sink_coef * src_coef)
|
|
84
|
+
if (p, 'min_load_efficiency') in s.process__ct_method:
|
|
85
|
+
flow_val = flow_val.add(r.process_online_dt[p] * section[p] * unitsize[p], axis=0)
|
|
86
|
+
r.flow_dt[p, always_source, always_sink] = flow_val
|
|
87
|
+
|
|
88
|
+
# ramp_dtt
|
|
89
|
+
current_idx = s.dtt.droplevel('t_previous')
|
|
90
|
+
previous_idx = s.dtt.droplevel('time')
|
|
91
|
+
r.ramp_dtt = pd.DataFrame(
|
|
92
|
+
r.flow_dt.reindex(current_idx).values - r.flow_dt.reindex(previous_idx).values,
|
|
93
|
+
index=s.dtt,
|
|
94
|
+
columns=r.flow_dt.columns,
|
|
95
|
+
dtype=float
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# flow_d (with step_duration), process_source_flow_d, process_sink_flow_d
|
|
99
|
+
r.flow_d = r.flow_dt.mul(step_duration, axis=0).groupby('period').sum()
|
|
100
|
+
r.process_source_flow_d = (
|
|
101
|
+
r.flow_d.T.groupby(level=['process', 'source']).sum().T
|
|
102
|
+
.reindex(columns=s.process_source, fill_value=0.0)
|
|
103
|
+
)
|
|
104
|
+
r.process_sink_flow_d = (
|
|
105
|
+
r.flow_d.T.groupby(level=['process', 'sink']).sum().T
|
|
106
|
+
.reindex(columns=s.process_sink, fill_value=0.0)
|
|
107
|
+
)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
from flextool.process_outputs._annualize import annualize_dt_to_d
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def compute_connection_flows(par, s, v, r) -> None:
|
|
7
|
+
"""Compute connection flow quantities and expose from_conn/to_conn for group aggregations."""
|
|
8
|
+
step_duration = par.step_duration
|
|
9
|
+
|
|
10
|
+
# Identify method_2way_1var connections (single signed flow variable).
|
|
11
|
+
# These include DC power flow connections (no_losses_no_variable_cost).
|
|
12
|
+
conn_1var: set[str] = set()
|
|
13
|
+
for proc, method in s.process_method:
|
|
14
|
+
if proc in s.process_connection and method == 'method_2way_1var_off':
|
|
15
|
+
conn_1var.add(proc)
|
|
16
|
+
|
|
17
|
+
# Filter to connection processes.
|
|
18
|
+
# For 2var connections: clip to non-negative (each direction is a separate variable).
|
|
19
|
+
# For 1var connections: keep sign (positive = left→right, negative = right→left).
|
|
20
|
+
all_conn_flows = r.flow_dt[
|
|
21
|
+
r.flow_dt.columns[r.flow_dt.columns.get_level_values('process').isin(s.process_connection)]
|
|
22
|
+
]
|
|
23
|
+
cols_1var = [c for c in all_conn_flows.columns if c[0] in conn_1var]
|
|
24
|
+
cols_2var = [c for c in all_conn_flows.columns if c[0] not in conn_1var]
|
|
25
|
+
parts = []
|
|
26
|
+
if cols_1var:
|
|
27
|
+
parts.append(all_conn_flows[cols_1var]) # keep sign
|
|
28
|
+
if cols_2var:
|
|
29
|
+
parts.append(all_conn_flows[cols_2var].clip(lower=0.0)) # clip
|
|
30
|
+
if len(parts) > 1:
|
|
31
|
+
conn_flows = pd.concat(parts, axis=1)
|
|
32
|
+
elif parts:
|
|
33
|
+
conn_flows = parts[0]
|
|
34
|
+
else:
|
|
35
|
+
# No connections: preserve the 3-level MultiIndex so droplevel() works downstream
|
|
36
|
+
conn_flows = all_conn_flows # already empty with correct column names
|
|
37
|
+
|
|
38
|
+
# Split into the four directional flows present in one connection.
|
|
39
|
+
# For 2var: both forward and reverse entries exist in process_source_sink.
|
|
40
|
+
# For 1var: only one entry exists — left_to_conn and conn_to_right match,
|
|
41
|
+
# conn_to_left and right_to_conn are empty. The signed flow value
|
|
42
|
+
# naturally gives correct net flow and directional results.
|
|
43
|
+
conn_to_left = conn_flows[conn_flows.columns[conn_flows.columns.droplevel('source').isin(s.process_source)]]
|
|
44
|
+
left_to_conn = conn_flows[conn_flows.columns[conn_flows.columns.droplevel('sink').isin(s.process_source)]]
|
|
45
|
+
conn_to_right = conn_flows[conn_flows.columns[conn_flows.columns.droplevel('source').isin(s.process_sink)]]
|
|
46
|
+
right_to_conn = conn_flows[conn_flows.columns[conn_flows.columns.droplevel('sink').isin(s.process_sink)]]
|
|
47
|
+
|
|
48
|
+
conn_to_left.columns = conn_to_left.columns.droplevel('source')
|
|
49
|
+
left_to_conn.columns = left_to_conn.columns.droplevel('sink')
|
|
50
|
+
conn_to_right.columns = conn_to_right.columns.droplevel('source')
|
|
51
|
+
right_to_conn.columns = right_to_conn.columns.droplevel('sink')
|
|
52
|
+
|
|
53
|
+
conn_to_left.columns.names = ['process', 'node']
|
|
54
|
+
left_to_conn.columns.names = ['process', 'node']
|
|
55
|
+
conn_to_right.columns.names = ['process', 'node']
|
|
56
|
+
right_to_conn.columns.names = ['process', 'node']
|
|
57
|
+
|
|
58
|
+
r.connection_dt = conn_to_right.droplevel('node', axis=1).sub(
|
|
59
|
+
conn_to_left.droplevel('node', axis=1), fill_value=0
|
|
60
|
+
)
|
|
61
|
+
# Losses = total_out - total_in (negative when energy is lost in connections)
|
|
62
|
+
r.connection_losses_dt = (
|
|
63
|
+
conn_to_left.droplevel('node', axis=1)
|
|
64
|
+
.add(conn_to_right.droplevel('node', axis=1), fill_value=0)
|
|
65
|
+
.sub(left_to_conn.droplevel('node', axis=1), fill_value=0)
|
|
66
|
+
.sub(right_to_conn.droplevel('node', axis=1), fill_value=0)
|
|
67
|
+
)
|
|
68
|
+
r.connection_to_left_node__dt = conn_to_left.sub(left_to_conn, fill_value=0)
|
|
69
|
+
r.connection_to_right_node__dt = conn_to_right.sub(right_to_conn, fill_value=0)
|
|
70
|
+
# v_flow is MW — multiply by step_duration for MWh, then weight each
|
|
71
|
+
# (d, t) by par.timestep_weight (=1.0 with no/uniform timeset_weights →
|
|
72
|
+
# byte-identical) before the period sum and annualisation.
|
|
73
|
+
r.connection_to_left_node__d = annualize_dt_to_d(
|
|
74
|
+
r.connection_to_left_node__dt, par.timestep_weight,
|
|
75
|
+
par.complete_period_share_of_year, step_duration,
|
|
76
|
+
)
|
|
77
|
+
r.connection_to_right_node__d = annualize_dt_to_d(
|
|
78
|
+
r.connection_to_right_node__dt, par.timestep_weight,
|
|
79
|
+
par.complete_period_share_of_year, step_duration,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Expose combined directional flows for use by calc_group_flows
|
|
83
|
+
r.from_conn = pd.concat([conn_to_left, conn_to_right], axis=1) # columns: ['process', 'node']
|
|
84
|
+
r.to_conn = pd.concat([left_to_conn, right_to_conn], axis=1) # columns: ['process', 'node']
|
|
85
|
+
|
|
86
|
+
# connection_d — weight each (d, t) by par.timestep_weight (=1.0 with
|
|
87
|
+
# no/uniform timeset_weights → byte-identical) here at the period sum;
|
|
88
|
+
# the cpsoy annualisation is applied downstream in out_ancillary.
|
|
89
|
+
r_conn_weighted = r.connection_dt.mul(step_duration, axis=0).mul(par.timestep_weight, axis=0)
|
|
90
|
+
if not r_conn_weighted.empty:
|
|
91
|
+
r.connection_d = r_conn_weighted[
|
|
92
|
+
r_conn_weighted.index.get_level_values('period').isin(s.d_realized_period)
|
|
93
|
+
].groupby(level='period').sum()
|
|
94
|
+
else:
|
|
95
|
+
r.connection_d = pd.DataFrame(
|
|
96
|
+
index=s.d_realized_period,
|
|
97
|
+
columns=pd.Index([], name='process'),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# connection_losses_d — weight each (d, t) by par.timestep_weight as above
|
|
101
|
+
# (cpsoy annualisation applied downstream in out_ancillary).
|
|
102
|
+
r_conn_losses_weighted = r.connection_losses_dt.mul(step_duration, axis=0).mul(par.timestep_weight, axis=0)
|
|
103
|
+
if not r_conn_losses_weighted.empty:
|
|
104
|
+
r.connection_losses_d = r_conn_losses_weighted[
|
|
105
|
+
r_conn_losses_weighted.index.get_level_values('period').isin(s.d_realized_period)
|
|
106
|
+
].groupby(level='period').sum()
|
|
107
|
+
else:
|
|
108
|
+
r.connection_losses_d = pd.DataFrame(
|
|
109
|
+
index=s.d_realized_period,
|
|
110
|
+
columns=pd.Index([], name='process'),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# DC power flow: angle differences per connection
|
|
114
|
+
if not v.angle.empty and not s.connection_dc_power_flow.empty:
|
|
115
|
+
angle_diff_cols: dict[str, pd.Series] = {}
|
|
116
|
+
for conn in s.connection_dc_power_flow:
|
|
117
|
+
# Find source and sink nodes for this connection from process_source_sink
|
|
118
|
+
pss_for_conn = s.process_source_sink[
|
|
119
|
+
s.process_source_sink.get_level_values('process') == conn
|
|
120
|
+
]
|
|
121
|
+
if pss_for_conn.empty:
|
|
122
|
+
continue
|
|
123
|
+
source_node = pss_for_conn.get_level_values('source')[0]
|
|
124
|
+
sink_node = pss_for_conn.get_level_values('sink')[0]
|
|
125
|
+
if source_node in v.angle.columns and sink_node in v.angle.columns:
|
|
126
|
+
angle_diff_cols[conn] = v.angle[source_node] - v.angle[sink_node]
|
|
127
|
+
if angle_diff_cols:
|
|
128
|
+
r.connection_angle_diff_dt = pd.DataFrame(angle_diff_cols)
|
|
129
|
+
r.connection_angle_diff_dt.columns.name = 'connection'
|
|
130
|
+
else:
|
|
131
|
+
r.connection_angle_diff_dt = pd.DataFrame()
|
|
132
|
+
else:
|
|
133
|
+
r.connection_angle_diff_dt = pd.DataFrame()
|
|
134
|
+
|
|
135
|
+
# Store angle_dt for output (may be empty)
|
|
136
|
+
r.angle_dt = v.angle
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
from flextool.process_outputs._annualize import annualize_dt_to_d
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _agg_period_or_empty(df: pd.DataFrame, realized_periods, period_level: str = 'period') -> pd.DataFrame:
|
|
7
|
+
"""Group-by period sum, or an all-zeros frame when df is empty."""
|
|
8
|
+
if df.empty:
|
|
9
|
+
cols = df.columns if hasattr(df, 'columns') else []
|
|
10
|
+
return pd.DataFrame(0.0, index=realized_periods, columns=cols)
|
|
11
|
+
return df[df.index.get_level_values(period_level).isin(realized_periods)].groupby(period_level).sum()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def compute_costs(par, s, v, r) -> None:
|
|
15
|
+
"""Compute all cost quantities (depends on r.costPenalty_* from compute_slacks).
|
|
16
|
+
|
|
17
|
+
Per-timestep cost terms must carry the same step-level scaling set as
|
|
18
|
+
the LP objective in flextool.mod: ``step_duration * p_rp_cost_weight``.
|
|
19
|
+
Period-level scaling (``p_inflation_factor_operations_yearly /
|
|
20
|
+
complete_period_share_of_year``) is applied when aggregating *_dt →
|
|
21
|
+
*_d below.
|
|
22
|
+
"""
|
|
23
|
+
# --- Commodity costs ---
|
|
24
|
+
# In-memory ``commodity_node`` is derived from the FlexData flow_*
|
|
25
|
+
# frames (covers every (c, n) referenced by an arc, regardless of
|
|
26
|
+
# whether a price was authored). ``par.commodity_price`` only
|
|
27
|
+
# carries columns for commodities with a populated ``p_commodity_price``
|
|
28
|
+
# entry — so intersect here to avoid KeyError when an arc references
|
|
29
|
+
# a price-less commodity (e.g. coal_chp where coal has only a
|
|
30
|
+
# co2_content row). Legacy behaviour: cost contribution is 0.
|
|
31
|
+
available_commodities = pd.Index(par.commodity_price.columns).unique()
|
|
32
|
+
commodity_node = s.commodity_node[
|
|
33
|
+
s.commodity_node.get_level_values('commodity').isin(available_commodities)
|
|
34
|
+
]
|
|
35
|
+
flow_from_commodity_node = r.flow_dt[r.flow_dt.columns[r.flow_dt.columns.get_level_values(level=1).isin(commodity_node.get_level_values(level=1))]]
|
|
36
|
+
flow_to_commodity_node = r.flow_dt[r.flow_dt.columns[r.flow_dt.columns.get_level_values(level=2).isin(commodity_node.get_level_values(level=1))]]
|
|
37
|
+
|
|
38
|
+
# Per-step scaling factor: step_duration × timestep_weight. Used by all
|
|
39
|
+
# per-(d, t) cost terms below to match the objective in flextool.mod.
|
|
40
|
+
step_x_rp = par.step_duration.mul(par.timestep_weight, axis=0)
|
|
41
|
+
|
|
42
|
+
commodity_price = par.commodity_price[commodity_node.get_level_values('commodity').unique()]
|
|
43
|
+
commodity_price.columns = commodity_price.columns.join(commodity_node)
|
|
44
|
+
flow_from_commodity_node.columns.names = ['process', 'node', 'sink']
|
|
45
|
+
flow_from_commodity_node.columns = flow_from_commodity_node.columns.join(commodity_price.columns)
|
|
46
|
+
flow_from_commodity = flow_from_commodity_node.T.groupby('commodity').sum().T
|
|
47
|
+
r.cost_commodity_dt = flow_from_commodity.mul(commodity_price).mul(step_x_rp, axis=0)
|
|
48
|
+
flow_to_commodity_node.columns.names = ['process', 'source', 'node']
|
|
49
|
+
flow_to_commodity_node.columns = flow_to_commodity_node.columns.join(commodity_price.columns)
|
|
50
|
+
flow_to_commodity = flow_to_commodity_node.T.groupby('commodity').sum().T
|
|
51
|
+
r.sales_commodity_dt = flow_to_commodity.mul(commodity_price).mul(step_x_rp, axis=0)
|
|
52
|
+
|
|
53
|
+
r.cost_commodity_d = r.cost_commodity_dt.groupby('period').sum()
|
|
54
|
+
r.sales_commodity_d = r.sales_commodity_dt.groupby('period').sum()
|
|
55
|
+
|
|
56
|
+
# --- Per-process commodity (fuel) buy & sales (additive output layer) ---
|
|
57
|
+
# Attribute fuel cost to the consuming process. Same dt-price, step_x_rp
|
|
58
|
+
# scaling and sign convention as the per-commodity totals above; only the
|
|
59
|
+
# collapse axis differs (process, not commodity). Build a price frame
|
|
60
|
+
# whose columns mirror the flow's full column MultiIndex (price selected
|
|
61
|
+
# by each column's commodity level), then a plain element-wise mul — a
|
|
62
|
+
# level= broadcast raises NotImplementedError on the non-unique commodity
|
|
63
|
+
# key. Summing these over 'process' reproduces r.cost_commodity_dt /
|
|
64
|
+
# r.sales_commodity_dt exactly (reconciliation invariant).
|
|
65
|
+
_price_by_commodity = commodity_price.droplevel('node', axis=1)
|
|
66
|
+
_price_by_commodity = _price_by_commodity.loc[:, ~_price_by_commodity.columns.duplicated()]
|
|
67
|
+
|
|
68
|
+
def _per_process_commodity_cost(flow_frame):
|
|
69
|
+
pf = _price_by_commodity.reindex(
|
|
70
|
+
columns=flow_frame.columns.get_level_values('commodity'))
|
|
71
|
+
pf.columns = flow_frame.columns
|
|
72
|
+
return flow_frame.mul(step_x_rp, axis=0).mul(pf).T.groupby('process').sum().T
|
|
73
|
+
|
|
74
|
+
r.cost_commodity_process_dt = _per_process_commodity_cost(flow_from_commodity_node)
|
|
75
|
+
r.sales_commodity_process_dt = _per_process_commodity_cost(flow_to_commodity_node)
|
|
76
|
+
|
|
77
|
+
# --- CO2 emissions ---
|
|
78
|
+
flow_outof_cols = r.flow_dt.columns.copy()
|
|
79
|
+
flow_outof_cols.names = ['process', 'node', 'sink']
|
|
80
|
+
flow_outof_cols = s.process__commodity__node_co2.join(flow_outof_cols)
|
|
81
|
+
flow_outof_cols = flow_outof_cols[~flow_outof_cols.get_level_values('sink').isna()]
|
|
82
|
+
|
|
83
|
+
flow_into_cols = r.flow_dt.columns.copy()
|
|
84
|
+
flow_into_cols.names = ['process', 'source', 'node']
|
|
85
|
+
flow_into_cols = s.process__commodity__node_co2.join(flow_into_cols)
|
|
86
|
+
flow_into_cols = flow_into_cols[~flow_into_cols.get_level_values('source').isna()]
|
|
87
|
+
flow_into_cols = flow_into_cols.reorder_levels(order=['process', 'commodity', 'source', 'node'])
|
|
88
|
+
|
|
89
|
+
flow_outof_node = r.flow_dt[flow_outof_cols.droplevel('commodity')]
|
|
90
|
+
flow_outof_node.columns.names = ['process', 'node', 'sink']
|
|
91
|
+
flow_outof_node.columns = flow_outof_node.columns.join(flow_outof_cols)
|
|
92
|
+
|
|
93
|
+
flow_into_node = r.flow_dt[flow_into_cols.droplevel('commodity')]
|
|
94
|
+
flow_into_node.columns.names = ['process', 'source', 'node']
|
|
95
|
+
flow_into_node.columns = flow_into_node.columns.join(flow_into_cols)
|
|
96
|
+
|
|
97
|
+
flow_into_node_grouped = flow_into_node.T.groupby(level=[0, 2, 3]).sum().T
|
|
98
|
+
flow_outof_node_grouped = flow_outof_node.T.groupby(level=[0, 1, 3]).sum().T
|
|
99
|
+
|
|
100
|
+
net_flow = flow_outof_node_grouped.sub(flow_into_node_grouped, fill_value=0)
|
|
101
|
+
# Emissions are physical MWh, so only step_duration applies here (no
|
|
102
|
+
# timestep_weight). timestep_weight is added downstream on the monetary
|
|
103
|
+
# group_cost_co2_dt to match the objective's CO2-price term
|
|
104
|
+
# (flextool.mod ~line 2312-2336).
|
|
105
|
+
net_flow_with_duration = net_flow.mul(par.step_duration, axis=0)
|
|
106
|
+
r.process_emissions_co2_dt = net_flow_with_duration.mul(par.commodity_co2_content, axis=1, level='commodity')
|
|
107
|
+
|
|
108
|
+
cols_df = r.process_emissions_co2_dt.columns.to_frame(index=False)
|
|
109
|
+
cols_df['type'] = 'unit'
|
|
110
|
+
cols_df.loc[cols_df['process'].isin(s.process_connection), 'type'] = 'connection'
|
|
111
|
+
r.process_emissions_co2_dt.columns = pd.MultiIndex.from_frame(
|
|
112
|
+
cols_df[['type', 'process', 'commodity', 'node']]
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Reported annual emissions (tonnes) must match the already-weighted CO2
|
|
116
|
+
# cap and CO2 cost: weight each (d, t) by par.timestep_weight (=1.0 with
|
|
117
|
+
# no/uniform timeset_weights → byte-identical) before the period sum.
|
|
118
|
+
# process_emissions_co2_dt carries step_duration but NOT timestep_weight
|
|
119
|
+
# (see net_flow_with_duration above), so this is the only weight applied.
|
|
120
|
+
r.process_emissions_co2_d = annualize_dt_to_d(r.process_emissions_co2_dt, par.timestep_weight, par.complete_period_share_of_year)
|
|
121
|
+
|
|
122
|
+
r.emissions_co2_d = r.process_emissions_co2_d.sum(axis=1)
|
|
123
|
+
r.emissions_co2_dt = r.process_emissions_co2_dt.sum(axis=1)
|
|
124
|
+
|
|
125
|
+
# Group CO2
|
|
126
|
+
s.group_node_co2 = s.group_node[s.group_node.get_level_values('group').isin(s.group_co2_limit.union(s.group_co2_price))]
|
|
127
|
+
group_process_co2_columns = r.process_emissions_co2_dt.columns.join(s.group_node_co2)
|
|
128
|
+
r.group_process_emissions_co2_dt = pd.DataFrame(index=r.process_emissions_co2_dt.index, columns=group_process_co2_columns)
|
|
129
|
+
for col in group_process_co2_columns:
|
|
130
|
+
r.group_process_emissions_co2_dt[col] = r.process_emissions_co2_dt[col[:4]]
|
|
131
|
+
r.group_co2_dt = r.group_process_emissions_co2_dt.T.groupby('group').sum().T
|
|
132
|
+
# Reported annual group emissions (tonnes): weight each (d, t) by
|
|
133
|
+
# par.timestep_weight (=1.0 with no/uniform timeset_weights →
|
|
134
|
+
# byte-identical) before the period sum so it matches the CO2 cap/cost.
|
|
135
|
+
# group_co2_dt derives from process_emissions_co2_dt (step_duration only,
|
|
136
|
+
# no timestep_weight), so this is the only weight applied.
|
|
137
|
+
r.group_co2_d = annualize_dt_to_d(r.group_co2_dt, par.timestep_weight, par.complete_period_share_of_year)
|
|
138
|
+
# Monetary CO2 cost = emissions × price. Emissions already carry
|
|
139
|
+
# step_duration; add timestep_weight to match the objective's CO2-price
|
|
140
|
+
# term (flextool.mod ~line 2312-2336 where it has
|
|
141
|
+
# step_duration × timestep_weight). Per-period cost_co2 is then
|
|
142
|
+
# aggregated from the rp-weighted cost_dt directly (instead of
|
|
143
|
+
# multiplying the annualized physical emissions by price, which would
|
|
144
|
+
# drop the per-step rp weighting).
|
|
145
|
+
r.group_cost_co2_dt = r.group_co2_dt.mul(par.group_co2_price).mul(par.timestep_weight, axis=0)
|
|
146
|
+
r.group_cost_co2_d = r.group_cost_co2_dt.groupby('period').sum()
|
|
147
|
+
r.cost_co2_dt = r.group_cost_co2_dt.sum(axis=1)
|
|
148
|
+
r.cost_co2_d = r.group_cost_co2_d.sum(axis=1)
|
|
149
|
+
|
|
150
|
+
# --- Per-process CO2 cost (additive output layer) ---
|
|
151
|
+
# group_process_emissions_co2_dt replicates per-process emissions once per
|
|
152
|
+
# priced group the process's node belongs to. Multiply by the dt group
|
|
153
|
+
# price (column-aligned to the 'group' level) and timestep_weight, then
|
|
154
|
+
# groupby('process').sum() — this SUMS the per-group charges over every
|
|
155
|
+
# priced group the process touches, matching the LP objective and
|
|
156
|
+
# calc_costs.py:104-127. Summing over 'process' reproduces r.cost_co2_dt.
|
|
157
|
+
_co2_cols = r.group_process_emissions_co2_dt.columns
|
|
158
|
+
_co2_price = par.group_co2_price.reindex(columns=_co2_cols.get_level_values('group'))
|
|
159
|
+
_co2_price.columns = _co2_cols
|
|
160
|
+
r.cost_process_co2_dt = (
|
|
161
|
+
r.group_process_emissions_co2_dt
|
|
162
|
+
.mul(_co2_price)
|
|
163
|
+
.mul(par.timestep_weight, axis=0)
|
|
164
|
+
.T.groupby('process').sum().T
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# --- Operational costs ---
|
|
168
|
+
# Mod objective (~line 2347-2376): flow × unitsize × varCost ×
|
|
169
|
+
# step_duration × timestep_weight × inflation / period_share ×
|
|
170
|
+
# pdt_branch_weight.
|
|
171
|
+
#
|
|
172
|
+
# The mod splits the ``other_operational_cost`` contribution into
|
|
173
|
+
# three sums keyed by ``pssdt_varCost_noEff``,
|
|
174
|
+
# ``pssdt_varCost_eff_unit_source``/``_sink``, and
|
|
175
|
+
# ``_eff_connection``. For ``min_load_efficiency`` processes the
|
|
176
|
+
# source-side sum also contains a ``section × online × unitsize``
|
|
177
|
+
# term (flextool.mod line 2358-2364). In Python this term is
|
|
178
|
+
# folded into ``r.flow_dt`` on the source-side (p, source, p)
|
|
179
|
+
# tuple by ``calc_capacity_flows.compute_capacity_and_flows``
|
|
180
|
+
# (line 58-59): ``flow_val.add(online × section × unitsize)`` when
|
|
181
|
+
# the process uses ``min_load_efficiency``. Multiplying that
|
|
182
|
+
# pre-aggregated source-side flow by the per-(p, source, sink)
|
|
183
|
+
# varCost here therefore reproduces both the slope and the section
|
|
184
|
+
# contributions in a single matmul, matching the objective. See
|
|
185
|
+
# ``test_cost_aggregation_semantics.TestMinLoadEfficiencySectionTerm``
|
|
186
|
+
# for the regression guard.
|
|
187
|
+
relevant_flows = r.flow_dt.loc[:, r.flow_dt.columns.intersection(par.process_source_sink_varCost.columns)]
|
|
188
|
+
cost_flows = relevant_flows.mul(step_x_rp, axis=0).mul(par.process_source_sink_varCost, axis=1)
|
|
189
|
+
r.cost_process_other_operational_cost_dt = cost_flows.T.groupby(level=0).sum().T.reindex(columns=s.process, fill_value=0.0)
|
|
190
|
+
|
|
191
|
+
# --- Startup costs ---
|
|
192
|
+
# Mod objective (~line 2337-2346): v_startup × startup_cost × unitsize
|
|
193
|
+
# × timestep_weight × inflation / period_share × pdt_branch_weight.
|
|
194
|
+
# No step_duration: startup is a per-event (not per-hour) cost.
|
|
195
|
+
r.process_startup_dt = v.startup_linear.add(v.startup_integer, fill_value=0)
|
|
196
|
+
r.cost_startup_dt = pd.DataFrame(0.0, index=r.process_startup_dt.index, columns=s.process_online, dtype=float)
|
|
197
|
+
valid_processes = s.process_online.intersection(r.process_startup_dt.columns).intersection(par.process_startup_cost.columns)
|
|
198
|
+
if len(valid_processes) > 0:
|
|
199
|
+
cost = r.process_startup_dt[valid_processes].mul(par.entity_unitsize[valid_processes], axis=1)
|
|
200
|
+
periods = cost.index.get_level_values('period')
|
|
201
|
+
period_costs = par.process_startup_cost.loc[periods, valid_processes]
|
|
202
|
+
period_costs.index = cost.index
|
|
203
|
+
r.cost_startup_dt[valid_processes] = cost.mul(period_costs).mul(par.timestep_weight, axis=0)
|
|
204
|
+
|
|
205
|
+
# --- Investment costs ---
|
|
206
|
+
r.cost_entity_invest_d = v.invest.mul(par.entity_unitsize[v.invest.columns]).mul(par.entity_annual_discounted)
|
|
207
|
+
r.cost_entity_divest_d = -v.divest.mul(par.entity_unitsize[v.divest.columns]).mul(par.entity_annual_divest_discounted)
|
|
208
|
+
# Use p_entity_all_existing (= pre_existing + carry-forward from earlier
|
|
209
|
+
# solves − divested) to match the mod objective term at
|
|
210
|
+
# flextool.mod:2395-2398. Differs from par.entity_pre_existing only for
|
|
211
|
+
# rolling / nested solves where prior-solve v_invest has aged into
|
|
212
|
+
# current-solve existing capacity.
|
|
213
|
+
r.cost_entity_fixed_pre_existing = (par.entity_all_existing * par.entity_fixed_cost).mul(par.inflation_factor_operations_yearly, axis=0)
|
|
214
|
+
# entity_lifetime_fixed_cost* are densified over _entity_universe and
|
|
215
|
+
# solve-keyed (concat-unioned), so they cover every v.invest/v.divest
|
|
216
|
+
# column — direct indexing is safe.
|
|
217
|
+
r.cost_entity_fixed_invested = (v.invest.mul(par.entity_unitsize[v.invest.columns] * par.entity_lifetime_fixed_cost[v.invest.columns]))
|
|
218
|
+
r.cost_entity_fixed_divested = -(v.divest.mul(par.entity_unitsize[v.divest.columns] * par.entity_lifetime_fixed_cost_divest[v.divest.columns]))
|
|
219
|
+
|
|
220
|
+
# --- Aggregate operational costs (depends on r.costPenalty_* from compute_slacks) ---
|
|
221
|
+
r.costOper_dt = (r.cost_commodity_dt.sum(axis=1) -
|
|
222
|
+
r.sales_commodity_dt.sum(axis=1) +
|
|
223
|
+
r.cost_process_other_operational_cost_dt.sum(axis=1) +
|
|
224
|
+
r.cost_startup_dt.sum(axis=1) +
|
|
225
|
+
r.cost_co2_dt
|
|
226
|
+
)
|
|
227
|
+
r.costPenalty_dt = (r.costPenalty_node_state_upDown_dt.sum(axis=1) +
|
|
228
|
+
r.costPenalty_inertia_dt.sum(axis=1) +
|
|
229
|
+
r.costPenalty_non_synchronous_dt.sum(axis=1) +
|
|
230
|
+
r.costPenalty_reserve_upDown_dt.sum(axis=1))
|
|
231
|
+
|
|
232
|
+
r.cost_process_other_operational_cost_d = r.cost_process_other_operational_cost_dt.groupby('period').sum()
|
|
233
|
+
r.cost_variable_d = r.cost_process_other_operational_cost_d.sum(axis=1)
|
|
234
|
+
|
|
235
|
+
r.cost_startup_d = _agg_period_or_empty(r.cost_startup_dt, s.d_realized_period)
|
|
236
|
+
r.costPenalty_inertia_d = _agg_period_or_empty(r.costPenalty_inertia_dt, s.d_realized_period)
|
|
237
|
+
r.costPenalty_non_synchronous_d = _agg_period_or_empty(r.costPenalty_non_synchronous_dt, s.d_realized_period)
|
|
238
|
+
r.costPenalty_reserve_upDown_d = _agg_period_or_empty(r.costPenalty_reserve_upDown_dt, s.d_realized_period)
|
|
239
|
+
|
|
240
|
+
r.costOper_d = r.costOper_dt.groupby('period').sum() \
|
|
241
|
+
.mul(par.inflation_factor_operations_yearly, axis=0) \
|
|
242
|
+
.div(par.complete_period_share_of_year, axis=0)
|
|
243
|
+
r.costPenalty_d = r.costPenalty_dt.groupby('period').sum() \
|
|
244
|
+
.mul(par.inflation_factor_operations_yearly, axis=0) \
|
|
245
|
+
.div(par.complete_period_share_of_year, axis=0)
|
|
246
|
+
r.costPenalty_d = r.costPenalty_d.add(r.costPenalty_capacity_margin_d, fill_value=0)
|
|
247
|
+
r.costOper_and_penalty_d = r.costOper_d.add(r.costPenalty_d, fill_value=0)
|
|
248
|
+
|
|
249
|
+
r.costInvestUnit_d = r.cost_entity_invest_d[[e for e in s.process_unit if e in r.cost_entity_invest_d.columns]].sum(axis=1)
|
|
250
|
+
r.costDivestUnit_d = r.cost_entity_divest_d[[e for e in s.process_unit if e in r.cost_entity_divest_d.columns]].sum(axis=1)
|
|
251
|
+
r.costInvestConnection_d = r.cost_entity_invest_d[[e for e in s.process_connection if e in r.cost_entity_invest_d.columns]].sum(axis=1)
|
|
252
|
+
r.costDivestConnection_d = r.cost_entity_divest_d[[e for e in s.process_connection if e in r.cost_entity_divest_d.columns]].sum(axis=1)
|
|
253
|
+
r.costInvestState_d = r.cost_entity_invest_d[[e for e in s.node_state if e in r.cost_entity_invest_d.columns]].sum(axis=1)
|
|
254
|
+
r.costDivestState_d = r.cost_entity_divest_d[[e for e in s.node_state if e in r.cost_entity_divest_d.columns]].sum(axis=1)
|
|
255
|
+
|
|
256
|
+
r.costInvest_d = r.costInvestUnit_d + r.costInvestConnection_d + r.costInvestState_d
|
|
257
|
+
r.costDivest_d = r.costDivestUnit_d + r.costDivestConnection_d + r.costDivestState_d
|
|
258
|
+
r.costFixedPreExisting_d = r.cost_entity_fixed_pre_existing.sum(axis=1)
|
|
259
|
+
r.costFixedInvested_d = r.cost_entity_fixed_invested.sum(axis=1)
|
|
260
|
+
r.costFixedDivested_d = r.cost_entity_fixed_divested.sum(axis=1)
|