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,179 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def unit_capacity(par, s, v, r, debug):
|
|
5
|
+
"""Unit capacity by period"""
|
|
6
|
+
|
|
7
|
+
# Get all periods and filter to process_unit entities. Sort
|
|
8
|
+
# periods deterministically: the nested cascade unions
|
|
9
|
+
# ``d_realize_dispatch_or_invest`` across rolls and the invest
|
|
10
|
+
# parent step's invest_periods come from a Polars ``.unique()``
|
|
11
|
+
# call whose row order is hash-partitioned and unstable across
|
|
12
|
+
# processes (see read_sets.py invest_periods_list build). An
|
|
13
|
+
# explicit sort here is the row-ordering guarantee the period-
|
|
14
|
+
# indexed CSV writers depend on.
|
|
15
|
+
if hasattr(s.d_realize_dispatch_or_invest, 'get_level_values') \
|
|
16
|
+
and 'period' in (s.d_realize_dispatch_or_invest.names or []):
|
|
17
|
+
periods = sorted(set(
|
|
18
|
+
s.d_realize_dispatch_or_invest.get_level_values('period')
|
|
19
|
+
))
|
|
20
|
+
else:
|
|
21
|
+
periods = sorted(set(s.d_realize_dispatch_or_invest))
|
|
22
|
+
processes = list(s.process_unit)
|
|
23
|
+
|
|
24
|
+
# Create base dataframe with all combinations (unit, period order)
|
|
25
|
+
index = pd.MultiIndex.from_product([processes, periods], names=['unit', 'period'])
|
|
26
|
+
results = pd.DataFrame(index=index)
|
|
27
|
+
results.columns.name = 'parameter'
|
|
28
|
+
|
|
29
|
+
# Existing capacity - filter to process_unit only. Uses
|
|
30
|
+
# ``par.entity_pre_existing`` (the static pre-invest baseline), NOT
|
|
31
|
+
# ``entity_all_existing`` (the cumulative chain-sum). On a multi-
|
|
32
|
+
# solve handoff (e.g. an invest solve feeding a dispatch solve) the
|
|
33
|
+
# handoff folds the prior solve's ``v_invest`` into
|
|
34
|
+
# ``entity_all_existing``; reading that here would misreport newly
|
|
35
|
+
# invested capacity under "existing" in the dispatch solve. The
|
|
36
|
+
# invested capacity is reported in the ``invested`` column (this
|
|
37
|
+
# solve's / roll's ``v_invest``) and the ``total`` column carries
|
|
38
|
+
# the cumulative sum; ``existing`` stays at the original baseline.
|
|
39
|
+
existing = par.entity_pre_existing[processes].unstack()
|
|
40
|
+
results['existing'] = existing
|
|
41
|
+
|
|
42
|
+
# Invested capacity - default to None, overwrite if data exists
|
|
43
|
+
results['invested'] = pd.Series(dtype=float)
|
|
44
|
+
if not v.invest.empty and len(v.invest.columns) > 0:
|
|
45
|
+
ed_unit_invest = s.ed_invest[s.ed_invest.get_level_values('entity').isin(s.process_unit)]
|
|
46
|
+
# Filter to (entity, period) pairs that actually have a v_invest value
|
|
47
|
+
# in this solve. Without the per-solve solve__ed_invest filter that
|
|
48
|
+
# v3.32.0 used, s.ed_invest is the full possible-pairs union; restrict
|
|
49
|
+
# here to avoid KeyErrors on never-realized pairs (e.g. (e, p2025)
|
|
50
|
+
# when only p2020 is realized in the current solve).
|
|
51
|
+
unstacked_invest = v.invest.unstack()
|
|
52
|
+
ed_unit_invest = ed_unit_invest.intersection(unstacked_invest.index)
|
|
53
|
+
unit_invest = ed_unit_invest.get_level_values('entity').unique()
|
|
54
|
+
results['invested'] = unstacked_invest[ed_unit_invest] * par.entity_unitsize[unit_invest]
|
|
55
|
+
|
|
56
|
+
# Divested capacity - default to None, overwrite if data exists
|
|
57
|
+
results['divested'] = pd.Series(dtype=float)
|
|
58
|
+
if not v.divest.empty and len(v.divest.columns) > 0:
|
|
59
|
+
ed_unit_divest = s.ed_divest[s.ed_divest.get_level_values('entity').isin(s.process_unit)]
|
|
60
|
+
unstacked_divest = v.divest.unstack()
|
|
61
|
+
ed_unit_divest = ed_unit_divest.intersection(unstacked_divest.index)
|
|
62
|
+
unit_divest = ed_unit_divest.get_level_values('entity').unique()
|
|
63
|
+
results['divested'] = unstacked_divest[ed_unit_divest] * par.entity_unitsize[unit_divest]
|
|
64
|
+
|
|
65
|
+
# Total capacity - filter to process_unit only
|
|
66
|
+
total = r.entity_all_capacity[processes].unstack()
|
|
67
|
+
results['total'] = total
|
|
68
|
+
results = results[['existing', 'invested', 'divested', 'total']]
|
|
69
|
+
return results, 'unit_capacity_ed_p'
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def connection_capacity(par, s, v, r, debug):
|
|
73
|
+
"""Connection capacity by period"""
|
|
74
|
+
|
|
75
|
+
# Get all periods and filter to process_connection entities.
|
|
76
|
+
# See unit_capacity above for the deterministic-period-order rationale.
|
|
77
|
+
if hasattr(s.d_realize_dispatch_or_invest, 'get_level_values') \
|
|
78
|
+
and 'period' in (s.d_realize_dispatch_or_invest.names or []):
|
|
79
|
+
periods = sorted(set(
|
|
80
|
+
s.d_realize_dispatch_or_invest.get_level_values('period')
|
|
81
|
+
))
|
|
82
|
+
else:
|
|
83
|
+
periods = sorted(set(s.d_realize_dispatch_or_invest))
|
|
84
|
+
connections = list(s.process_connection)
|
|
85
|
+
|
|
86
|
+
# Create base dataframe with all combinations (connection, period order)
|
|
87
|
+
index = pd.MultiIndex.from_product([connections, periods], names=['connection', 'period'])
|
|
88
|
+
results = pd.DataFrame(index=index)
|
|
89
|
+
results.columns.name = 'parameter'
|
|
90
|
+
|
|
91
|
+
# Existing capacity - filter to process_connection only. Uses
|
|
92
|
+
# ``entity_pre_existing`` (pre-invest baseline); see unit_capacity
|
|
93
|
+
# above for why carried-forward invest must not land here.
|
|
94
|
+
existing = par.entity_pre_existing[connections].unstack()
|
|
95
|
+
results['existing'] = existing
|
|
96
|
+
|
|
97
|
+
# Invested capacity - default to empty, overwrite if data exists
|
|
98
|
+
results['invested'] = pd.Series(dtype=float)
|
|
99
|
+
if not v.invest.empty and len(v.invest.columns) > 0:
|
|
100
|
+
ed_conn_invest = s.ed_invest[s.ed_invest.get_level_values('entity').isin(s.process_connection)]
|
|
101
|
+
# See unit_capacity above for why we intersect with v.invest's columns.
|
|
102
|
+
unstacked_invest = v.invest.unstack()
|
|
103
|
+
ed_conn_invest = ed_conn_invest.intersection(unstacked_invest.index)
|
|
104
|
+
conn_invest = ed_conn_invest.get_level_values('entity').unique()
|
|
105
|
+
results['invested'] = unstacked_invest[ed_conn_invest] * par.entity_unitsize[conn_invest]
|
|
106
|
+
|
|
107
|
+
# Divested capacity - default to empty, overwrite if data exists
|
|
108
|
+
results['divested'] = pd.Series(dtype=float)
|
|
109
|
+
if not v.divest.empty and len(v.divest.columns) > 0:
|
|
110
|
+
ed_conn_divest = s.ed_divest[s.ed_divest.get_level_values('entity').isin(s.process_connection)]
|
|
111
|
+
unstacked_divest = v.divest.unstack()
|
|
112
|
+
ed_conn_divest = ed_conn_divest.intersection(unstacked_divest.index)
|
|
113
|
+
conn_divest = ed_conn_divest.get_level_values('entity').unique()
|
|
114
|
+
results['divested'] = unstacked_divest[ed_conn_divest] * par.entity_unitsize[conn_divest] # was: results['invested']
|
|
115
|
+
|
|
116
|
+
# Total capacity - filter to process_connection only
|
|
117
|
+
results['total'] = r.entity_all_capacity[connections].unstack()
|
|
118
|
+
|
|
119
|
+
# Reorder columns
|
|
120
|
+
results = results[['existing', 'invested', 'divested', 'total']]
|
|
121
|
+
|
|
122
|
+
return results, 'connection_capacity_ed_p'
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def node_capacity(par, s, v, r, debug):
|
|
126
|
+
"""Node capacity by period"""
|
|
127
|
+
|
|
128
|
+
# Get all periods and filter to node_state entities.
|
|
129
|
+
# See unit_capacity above for the deterministic-period-order rationale.
|
|
130
|
+
if hasattr(s.d_realize_dispatch_or_invest, 'get_level_values') \
|
|
131
|
+
and 'period' in (s.d_realize_dispatch_or_invest.names or []):
|
|
132
|
+
periods = sorted(set(
|
|
133
|
+
s.d_realize_dispatch_or_invest.get_level_values('period')
|
|
134
|
+
))
|
|
135
|
+
else:
|
|
136
|
+
periods = sorted(set(s.d_realize_dispatch_or_invest))
|
|
137
|
+
nodes = list(s.node_state)
|
|
138
|
+
|
|
139
|
+
# Create base dataframe with all combinations (node, period order)
|
|
140
|
+
index = pd.MultiIndex.from_product([nodes, periods], names=['node', 'period'])
|
|
141
|
+
results = pd.DataFrame(index=index)
|
|
142
|
+
results.columns.name = 'parameter'
|
|
143
|
+
|
|
144
|
+
# Existing capacity - filter to node_state only. Uses
|
|
145
|
+
# ``entity_pre_existing`` (pre-invest baseline); see unit_capacity
|
|
146
|
+
# above for why carried-forward invest must not land here.
|
|
147
|
+
if nodes:
|
|
148
|
+
existing = par.entity_pre_existing[nodes].unstack()
|
|
149
|
+
results['existing'] = existing
|
|
150
|
+
else:
|
|
151
|
+
results['existing'] = pd.Series(dtype=float)
|
|
152
|
+
|
|
153
|
+
# Invested capacity - default to empty, overwrite if data exists
|
|
154
|
+
results['invested'] = pd.Series(dtype=float)
|
|
155
|
+
if not v.invest.empty and len(v.invest.columns) > 0:
|
|
156
|
+
ed_node_invest = s.ed_invest[s.ed_invest.get_level_values('entity').isin(s.node)]
|
|
157
|
+
# See unit_capacity above for why we intersect with v.invest's columns.
|
|
158
|
+
unstacked_invest = v.invest.unstack()
|
|
159
|
+
ed_node_invest = ed_node_invest.intersection(unstacked_invest.index)
|
|
160
|
+
node_invest = ed_node_invest.get_level_values('entity').unique()
|
|
161
|
+
results['invested'] = unstacked_invest[ed_node_invest] * par.entity_unitsize[node_invest]
|
|
162
|
+
|
|
163
|
+
# Divested capacity - default to empty, overwrite if data exists
|
|
164
|
+
results['divested'] = pd.Series(dtype=float)
|
|
165
|
+
if not v.divest.empty and len(v.divest.columns) > 0:
|
|
166
|
+
ed_node_divest = s.ed_divest[s.ed_divest.get_level_values('entity').isin(s.node)]
|
|
167
|
+
unstacked_divest = v.divest.unstack()
|
|
168
|
+
ed_node_divest = ed_node_divest.intersection(unstacked_divest.index)
|
|
169
|
+
node_divest = ed_node_divest.get_level_values('entity').unique()
|
|
170
|
+
results['divested'] = unstacked_divest[ed_node_divest] * par.entity_unitsize[node_divest] # was: v.invest, results['invested']
|
|
171
|
+
|
|
172
|
+
# Total capacity - filter to node_state only
|
|
173
|
+
if nodes:
|
|
174
|
+
results['total'] = r.entity_all_capacity[nodes].unstack()
|
|
175
|
+
else:
|
|
176
|
+
results['total'] = pd.Series(dtype=float)
|
|
177
|
+
|
|
178
|
+
results = results[['existing', 'invested', 'divested', 'total']]
|
|
179
|
+
return results, 'node_capacity_ed_p'
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def generic(par, s, v, r, debug):
|
|
5
|
+
if debug:
|
|
6
|
+
results = []
|
|
7
|
+
df = pd.concat([par.inflation_factor_operations_yearly, par.inflation_factor_investment_yearly], axis=1)
|
|
8
|
+
df.columns = ["operations discount factor","investments discount factor"]
|
|
9
|
+
df.columns.name = "param"
|
|
10
|
+
results.append((df, 'discountFactors_d_p'))
|
|
11
|
+
|
|
12
|
+
df = par.entity_annuity
|
|
13
|
+
results.append((df, 'entity_annuity_d_p'))
|
|
14
|
+
|
|
15
|
+
return results
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def cost_summaries(par, s, v, r, debug):
|
|
19
|
+
"""Cost summaries for periods and timesteps"""
|
|
20
|
+
|
|
21
|
+
results = []
|
|
22
|
+
|
|
23
|
+
# Common calculations
|
|
24
|
+
discount_ops = par.inflation_factor_operations_yearly
|
|
25
|
+
discount_invs = par.inflation_factor_investment_yearly
|
|
26
|
+
period_share = par.complete_period_share_of_year
|
|
27
|
+
to_millions = 1000000
|
|
28
|
+
|
|
29
|
+
# 1. Costs at timestep level (non-annualized)
|
|
30
|
+
costs_dt = pd.DataFrame(index=s.dt_realize_dispatch, dtype=float)
|
|
31
|
+
costs_dt.columns.name = 'category'
|
|
32
|
+
costs_dt['commodity_cost'] = r.cost_commodity_dt.sum(axis=1)
|
|
33
|
+
# Sign convention: store the LP obj contribution. flextool.mod's
|
|
34
|
+
# commodity term is `+ price × BUY - price × SELL` (lines ~1985-2000),
|
|
35
|
+
# so the obj contribution from sales is `-r.sales_commodity_dt`
|
|
36
|
+
# — revenue shows as a negative value, matching costOper_dt's
|
|
37
|
+
# `- r.sales_commodity_dt` (calc_costs.py:158-163). Without the
|
|
38
|
+
# negation, sum(costs_dt categories) ≠ costOper_dt + costPenalty_dt
|
|
39
|
+
# by 2×|sales|, which propagates into costs_discounted_p_.
|
|
40
|
+
costs_dt['commodity_sales'] = -r.sales_commodity_dt.sum(axis=1)
|
|
41
|
+
costs_dt['co2'] = r.cost_co2_dt
|
|
42
|
+
costs_dt['other operational'] = r.cost_process_other_operational_cost_dt.sum(axis=1)
|
|
43
|
+
costs_dt['starts'] = r.cost_startup_dt.sum(axis=1)
|
|
44
|
+
costs_dt['upward slack penalty'] = r.costPenalty_node_state_upDown_dt.xs('up', level='upDown', axis=1).sum(axis=1)
|
|
45
|
+
costs_dt['downward slack penalty'] = r.costPenalty_node_state_upDown_dt.xs('down', level='upDown', axis=1).sum(axis=1)
|
|
46
|
+
costs_dt['inertia slack penalty'] = r.costPenalty_inertia_dt.sum(axis=1)
|
|
47
|
+
costs_dt['non-synchronous slack penalty'] = r.costPenalty_non_synchronous_dt.sum(axis=1)
|
|
48
|
+
try:
|
|
49
|
+
costs_dt['upward reserve slack penalty'] = r.costPenalty_reserve_upDown_dt.xs('up', level='updown', axis=1).sum(axis=1)
|
|
50
|
+
except KeyError:
|
|
51
|
+
costs_dt['upward reserve slack penalty'] = 0
|
|
52
|
+
try:
|
|
53
|
+
costs_dt['downward reserve slack penalty'] = r.costPenalty_reserve_upDown_dt.xs('down', level='updown', axis=1).sum(axis=1)
|
|
54
|
+
except KeyError:
|
|
55
|
+
costs_dt['downward reserve slack penalty'] = 0
|
|
56
|
+
|
|
57
|
+
results.append((costs_dt, 'costs_dt_p'))
|
|
58
|
+
|
|
59
|
+
# 2. Annualized, inflation adjusted and years represented (derived from costs_dt)
|
|
60
|
+
dispatch_costs_pure_period = costs_dt.groupby(level='period').sum()
|
|
61
|
+
dispatch_costs_annualized_period = dispatch_costs_pure_period.div(period_share, axis=0) / to_millions
|
|
62
|
+
# discount_ops = inflation_factor_operations_yearly already sums
|
|
63
|
+
# (1+r)^y × p_years_represented[d,y] over the represented years of d
|
|
64
|
+
# (preprocessing/period_calculated_params.py:287-301), so multiplying
|
|
65
|
+
# by it produces the horizon-weighted NPV directly. Multiplying by
|
|
66
|
+
# par.years_represented_d on top of that double-counts the year
|
|
67
|
+
# weighting and shows up as sum(costs_discounted) = obj × years_rep.
|
|
68
|
+
dispatch_costs_inflation_adjusted = (
|
|
69
|
+
dispatch_costs_annualized_period
|
|
70
|
+
.mul(discount_ops, axis=0)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# 3. Discounted and inflation adjusted (with years represented) investment costs.
|
|
74
|
+
# Indexed by d_realize_invest ∪ d_realized_period ∪ period_in_use:
|
|
75
|
+
# invest/divest and fixed_invested/fixed_divested only have rows in
|
|
76
|
+
# d_realize_invest, but fixed-cost-pre-existing applies in every period
|
|
77
|
+
# that the LP objective sees — which is ``period_in_use`` (every period
|
|
78
|
+
# carrying a non-zero ``complete_period_share_of_year``). Excluding
|
|
79
|
+
# ``period_in_use`` periods that are NOT in d_realized_period dropped
|
|
80
|
+
# fixed_pre_existing rows that the LP DID charge for (e.g. the ``all``
|
|
81
|
+
# scenario realizes p2020 dispatch but charges fixed_pre_existing for
|
|
82
|
+
# p2020+p2025 via ``years_represented``; bare
|
|
83
|
+
# ``d_realize_invest ∪ d_realized_period`` discarded p2025's row on
|
|
84
|
+
# assignment to ``investment_costs``, leaving the per-category sum 1000
|
|
85
|
+
# M-CUR below summary's full-horizon total).
|
|
86
|
+
period_in_use = par.complete_period_share_of_year.index
|
|
87
|
+
investment_index = s.d_realize_invest.union(s.d_realized_period).union(period_in_use)
|
|
88
|
+
investment_costs = pd.DataFrame(index=investment_index, dtype=float)
|
|
89
|
+
investment_costs.columns.name = 'category'
|
|
90
|
+
# Invest + divest are independent per-period series: a scenario may
|
|
91
|
+
# realize investment but no divestment anywhere in the cascade (and
|
|
92
|
+
# vice-versa). When ``v_divest`` is empty across every sub-solve the
|
|
93
|
+
# divest-cost series is row-less, so a bare ``invest + divest`` would
|
|
94
|
+
# align on the index union and yield NaN at EVERY period the divest
|
|
95
|
+
# series lacks — which ``fillna(0.0)`` below then silently zeroes,
|
|
96
|
+
# discarding the valid investment cost (the ``unit investment &
|
|
97
|
+
# retirement`` cost-path collapse for nested-invest scenarios with no
|
|
98
|
+
# divestment). ``add(fill_value=0)`` is the correct additive-identity
|
|
99
|
+
# union: a period present in only one operand keeps that operand's
|
|
100
|
+
# value instead of becoming NaN. Byte-identical when both series carry
|
|
101
|
+
# the same full period index (the divest-active scenarios).
|
|
102
|
+
investment_costs['unit investment & retirement'] = r.costInvestUnit_d.add(r.costDivestUnit_d, fill_value=0) / to_millions
|
|
103
|
+
investment_costs['connection investment & retirement'] = r.costInvestConnection_d.add(r.costDivestConnection_d, fill_value=0) / to_millions
|
|
104
|
+
investment_costs['storage investment & retirement'] = r.costInvestState_d.add(r.costDivestState_d, fill_value=0) / to_millions
|
|
105
|
+
investment_costs['fixed cost pre-existing'] = r.costFixedPreExisting_d / to_millions
|
|
106
|
+
investment_costs['fixed cost invested'] = r.costFixedInvested_d / to_millions
|
|
107
|
+
investment_costs['fixed cost reduction of divestments'] = r.costFixedDivested_d / to_millions
|
|
108
|
+
investment_costs['capacity margin penalty'] = r.costPenalty_capacity_margin_d / to_millions
|
|
109
|
+
investment_costs = investment_costs.fillna(0.0)
|
|
110
|
+
|
|
111
|
+
# Annualize back: Remove inflation adjustment and years represented
|
|
112
|
+
annual_invest_costs = investment_costs.div(discount_invs, axis=0)
|
|
113
|
+
annual_invest_costs['fixed cost pre-existing'] = investment_costs['fixed cost pre-existing'].div(discount_ops, axis=0)
|
|
114
|
+
annual_invest_costs['fixed cost invested'] = investment_costs['fixed cost invested'].div(discount_ops, axis=0)
|
|
115
|
+
annual_invest_costs['fixed cost reduction of divestments'] = investment_costs['fixed cost reduction of divestments'].div(discount_ops, axis=0)
|
|
116
|
+
annual_invest_costs['capacity margin penalty'] = investment_costs['capacity margin penalty'].div(discount_ops, axis=0)
|
|
117
|
+
|
|
118
|
+
# 4. Combined summary (investment + dispatch aggregated to period)
|
|
119
|
+
all_periods = s.d_realized_period.union(s.d_realize_invest)
|
|
120
|
+
summary = pd.DataFrame(index=all_periods, dtype=float)
|
|
121
|
+
summary.columns.name = 'parameter'
|
|
122
|
+
|
|
123
|
+
# Without inflation and years (so, pure annual results)
|
|
124
|
+
summary_annualized = annual_invest_costs.join(dispatch_costs_annualized_period)
|
|
125
|
+
results.append((summary_annualized, 'annualized_costs_d_p'))
|
|
126
|
+
|
|
127
|
+
# With years_represented adjusted with inflation (same as model).
|
|
128
|
+
# Outer join: investment_costs is indexed by d_realize_invest (only
|
|
129
|
+
# periods where invest/divest is realized), but dispatch_costs covers
|
|
130
|
+
# d_realized_period (every realized period). A left join on
|
|
131
|
+
# investment_costs would silently drop dispatch costs for periods
|
|
132
|
+
# without investment activity (e.g. y2020_2029_2x5y dispatches in
|
|
133
|
+
# both p2020 and p2025 but only realizes invest in p2020).
|
|
134
|
+
summary_inflation_years = investment_costs.join(
|
|
135
|
+
dispatch_costs_inflation_adjusted, how='outer'
|
|
136
|
+
).fillna(0.0)
|
|
137
|
+
results.append((summary_inflation_years, 'costs_discounted_d_p'))
|
|
138
|
+
|
|
139
|
+
# With years_represented adjusted with inflation (same as model).
|
|
140
|
+
# Outer join: investment_costs is indexed by d_realize_invest (only
|
|
141
|
+
# periods where invest/divest is realized), but dispatch_costs covers
|
|
142
|
+
# d_realized_period (every realized period). A left join on
|
|
143
|
+
# investment_costs would silently drop dispatch costs for periods
|
|
144
|
+
# without investment activity (e.g. y2020_2029_2x5y dispatches in
|
|
145
|
+
# both p2020 and p2025 but only realizes invest in p2020).
|
|
146
|
+
summary_inflation_years = investment_costs.join(
|
|
147
|
+
dispatch_costs_inflation_adjusted, how='outer'
|
|
148
|
+
).fillna(0.0)
|
|
149
|
+
results.append((summary_inflation_years.sum(axis=0), 'costs_discounted_p_'))
|
|
150
|
+
|
|
151
|
+
return results
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def cost_breakdown_by_entity(par, s, v, r, debug):
|
|
155
|
+
"""Per-entity cost break-down at the PERIOD level (unit / connection / node).
|
|
156
|
+
|
|
157
|
+
Purely additive over the existing per-entity intermediates in
|
|
158
|
+
``calc_costs.py``. Each table mirrors ``cost_summaries`` arithmetic
|
|
159
|
+
EXACTLY, only collapsed per-entity instead of system-wide, so summing a
|
|
160
|
+
per-entity table over its entities reconciles against the system summary
|
|
161
|
+
(``annualized_costs_d_p`` / ``costs_discounted_d_p``). Six tables: unit,
|
|
162
|
+
connection, node — in annualized (M CUR/a) and discounted (M CUR) flavours.
|
|
163
|
+
|
|
164
|
+
Index = (period, entity) MultiIndex with the entity level named
|
|
165
|
+
'unit'/'connection'/'node'; columns = single level named 'category'.
|
|
166
|
+
PERIOD-LEVEL ONLY — no per-timestep tables (this deliberately avoids the
|
|
167
|
+
CSV writer's positional solve-injection on dt-indexed frames).
|
|
168
|
+
|
|
169
|
+
Fuel/commodity cost is attributed to the consuming process; multi-group
|
|
170
|
+
CO2 cost is the sum over every priced group a process touches (matching the
|
|
171
|
+
LP objective).
|
|
172
|
+
"""
|
|
173
|
+
results = []
|
|
174
|
+
|
|
175
|
+
discount_ops = par.inflation_factor_operations_yearly
|
|
176
|
+
discount_invs = par.inflation_factor_investment_yearly
|
|
177
|
+
period_share = par.complete_period_share_of_year
|
|
178
|
+
to_millions = 1000000
|
|
179
|
+
|
|
180
|
+
# Entity sets for the three entity types. Operational/startup/fuel/co2
|
|
181
|
+
# process frames are sliced to the processes of the relevant type; the
|
|
182
|
+
# invest/fixed frames (already per-entity) are sliced the same way.
|
|
183
|
+
entity_specs = (
|
|
184
|
+
('unit', list(s.process_unit), 'unit'),
|
|
185
|
+
('connection', list(s.process_connection), 'connection'),
|
|
186
|
+
('node', list(s.node_state), 'node'),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def _slice_cols(frame, entities):
|
|
190
|
+
"""Columns of ``frame`` restricted to ``entities`` (present only)."""
|
|
191
|
+
return [e for e in entities if e in frame.columns]
|
|
192
|
+
|
|
193
|
+
def _op_annualized(frame, entities):
|
|
194
|
+
"""Operational dt frame → per-(period, entity) annualized Series
|
|
195
|
+
(M CUR/a). Matches dispatch_costs_annualized_period."""
|
|
196
|
+
cols = _slice_cols(frame, entities)
|
|
197
|
+
if not cols:
|
|
198
|
+
return None
|
|
199
|
+
per_d = frame[cols].groupby('period').sum().div(period_share, axis=0) / to_millions
|
|
200
|
+
return per_d.stack(future_stack=True)
|
|
201
|
+
|
|
202
|
+
def _op_discounted(frame, entities):
|
|
203
|
+
"""Operational dt frame → per-(period, entity) discounted Series
|
|
204
|
+
(M CUR). Matches dispatch_costs_inflation_adjusted."""
|
|
205
|
+
cols = _slice_cols(frame, entities)
|
|
206
|
+
if not cols:
|
|
207
|
+
return None
|
|
208
|
+
per_d = (
|
|
209
|
+
frame[cols].groupby('period').sum()
|
|
210
|
+
.div(period_share, axis=0)
|
|
211
|
+
.mul(discount_ops, axis=0)
|
|
212
|
+
/ to_millions
|
|
213
|
+
)
|
|
214
|
+
return per_d.stack(future_stack=True)
|
|
215
|
+
|
|
216
|
+
def _inv_discounted(frame, entities):
|
|
217
|
+
"""Already-discounted M-path per-entity frame → per-(period, entity)
|
|
218
|
+
Series (M CUR). Matches investment_costs (/1e6 only)."""
|
|
219
|
+
cols = _slice_cols(frame, entities)
|
|
220
|
+
if not cols:
|
|
221
|
+
return None
|
|
222
|
+
return (frame[cols] / to_millions).stack(future_stack=True)
|
|
223
|
+
|
|
224
|
+
def _inv_annualized(frame, entities, *, discount):
|
|
225
|
+
"""Already-discounted M-path per-entity frame → per-(period, entity)
|
|
226
|
+
annualized Series (M CUR/a): remove the supplied discount factor.
|
|
227
|
+
Matches annual_invest_costs."""
|
|
228
|
+
cols = _slice_cols(frame, entities)
|
|
229
|
+
if not cols:
|
|
230
|
+
return None
|
|
231
|
+
return (frame[cols].div(discount, axis=0) / to_millions).stack(future_stack=True)
|
|
232
|
+
|
|
233
|
+
for kind, entities, level_name in entity_specs:
|
|
234
|
+
# Build (annualized_pieces, discounted_pieces) dicts keyed by category.
|
|
235
|
+
ann_pieces: dict = {}
|
|
236
|
+
dis_pieces: dict = {}
|
|
237
|
+
|
|
238
|
+
# --- Operational (process / node-state) ---
|
|
239
|
+
# commodity_sales is NEGATED to match out_costs.py:40 sign convention.
|
|
240
|
+
op_specs = [
|
|
241
|
+
('commodity_cost', r.cost_commodity_process_dt, 1.0),
|
|
242
|
+
('commodity_sales', r.sales_commodity_process_dt, -1.0),
|
|
243
|
+
('co2', r.cost_process_co2_dt, 1.0),
|
|
244
|
+
('other operational', r.cost_process_other_operational_cost_dt, 1.0),
|
|
245
|
+
('starts', r.cost_startup_dt, 1.0),
|
|
246
|
+
]
|
|
247
|
+
for cat, frame, sign in op_specs:
|
|
248
|
+
src = frame if sign == 1.0 else frame.mul(sign)
|
|
249
|
+
a = _op_annualized(src, entities)
|
|
250
|
+
d = _op_discounted(src, entities)
|
|
251
|
+
if a is not None:
|
|
252
|
+
ann_pieces[cat] = a
|
|
253
|
+
if d is not None:
|
|
254
|
+
dis_pieces[cat] = d
|
|
255
|
+
|
|
256
|
+
# --- Node-state slack penalties (node table only) ---
|
|
257
|
+
# Same operational annualized/discounted treatment (they flow through
|
|
258
|
+
# costs_dt_p → annualized/discounted in the system path,
|
|
259
|
+
# out_costs.py:44-45). The slack penalty applies to every balance
|
|
260
|
+
# node carrying a state-slack variable (the frame's own columns) —
|
|
261
|
+
# which is a SUPERSET of s.node_state (storage-state nodes). Slicing
|
|
262
|
+
# to s.node_state here would drop the penalties of balance-only nodes
|
|
263
|
+
# and break the system reconciliation, so use the frame's own node
|
|
264
|
+
# columns as the entity set instead of ``entities``.
|
|
265
|
+
if kind == 'node':
|
|
266
|
+
slack = r.costPenalty_node_state_upDown_dt
|
|
267
|
+
for cat, ud in (('upward slack penalty', 'up'),
|
|
268
|
+
('downward slack penalty', 'down')):
|
|
269
|
+
try:
|
|
270
|
+
sub = slack.xs(ud, level='upDown', axis=1)
|
|
271
|
+
except KeyError:
|
|
272
|
+
continue
|
|
273
|
+
slack_nodes = list(sub.columns)
|
|
274
|
+
a = _op_annualized(sub, slack_nodes)
|
|
275
|
+
d = _op_discounted(sub, slack_nodes)
|
|
276
|
+
if a is not None:
|
|
277
|
+
ann_pieces[cat] = a
|
|
278
|
+
if d is not None:
|
|
279
|
+
dis_pieces[cat] = d
|
|
280
|
+
|
|
281
|
+
# --- Investment / fixed (already discounted M-path) ---
|
|
282
|
+
# Skip categories whose sliced frame is empty (dispatch-only models
|
|
283
|
+
# have empty v.invest / v.divest).
|
|
284
|
+
invest_label = 'storage investment' if kind == 'node' else 'investment'
|
|
285
|
+
divest_label = 'storage retirement' if kind == 'node' else 'retirement'
|
|
286
|
+
inv_specs = [
|
|
287
|
+
(invest_label, r.cost_entity_invest_d, discount_invs),
|
|
288
|
+
(divest_label, r.cost_entity_divest_d, discount_invs),
|
|
289
|
+
('fixed cost pre-existing', r.cost_entity_fixed_pre_existing, discount_ops),
|
|
290
|
+
('fixed cost invested', r.cost_entity_fixed_invested, discount_ops),
|
|
291
|
+
('fixed cost reduction of divestments', r.cost_entity_fixed_divested, discount_ops),
|
|
292
|
+
]
|
|
293
|
+
for cat, frame, discount in inv_specs:
|
|
294
|
+
d = _inv_discounted(frame, entities)
|
|
295
|
+
a = _inv_annualized(frame, entities, discount=discount)
|
|
296
|
+
if a is not None:
|
|
297
|
+
ann_pieces[cat] = a
|
|
298
|
+
if d is not None:
|
|
299
|
+
dis_pieces[cat] = d
|
|
300
|
+
|
|
301
|
+
for pieces, flavour in ((ann_pieces, 'annualized'), (dis_pieces, 'discounted')):
|
|
302
|
+
if not pieces:
|
|
303
|
+
# Empty guard: no category pieces — skip the append.
|
|
304
|
+
continue
|
|
305
|
+
df = pd.concat(pieces, axis=1)
|
|
306
|
+
df.columns.name = 'category'
|
|
307
|
+
# After future_stack the entity level is the last index level;
|
|
308
|
+
# name the (period, entity) levels explicitly.
|
|
309
|
+
df.index = df.index.set_names(['period', level_name])
|
|
310
|
+
df = df.fillna(0.0).sort_index()
|
|
311
|
+
key = f'cost_{kind}_{flavour}_d_ec'
|
|
312
|
+
results.append((df, key))
|
|
313
|
+
|
|
314
|
+
return results
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def CO2(par, s, v, r, debug):
|
|
318
|
+
"""Annualized CO2 Mt for groups by period"""
|
|
319
|
+
results = []
|
|
320
|
+
|
|
321
|
+
# Calculate CO2 emissions in Mt
|
|
322
|
+
total_co2 = ((r.emissions_co2_d * par.years_represented_d) / 1000000).sum(axis=0)
|
|
323
|
+
co2_summary = pd.DataFrame(index=["CO2 [Mt]"], columns=["model_wide"], data=total_co2)
|
|
324
|
+
co2_summary.index.name = 'param_CO2'
|
|
325
|
+
co2_summary.columns.name = 'scope'
|
|
326
|
+
results.append((co2_summary, 'CO2__'))
|
|
327
|
+
|
|
328
|
+
# Process co2 emissions (annualized — plot rule 'y' weights by years_represented for horizon totals)
|
|
329
|
+
process_co2 = r.process_emissions_co2_d.groupby(['period']).sum()
|
|
330
|
+
results.append((process_co2, 'process_co2_d_eee'))
|
|
331
|
+
|
|
332
|
+
# Group co2 emissions (annualized)
|
|
333
|
+
results.append((r.group_co2_d, 'CO2_d_g'))
|
|
334
|
+
return results
|