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.
Files changed (322) hide show
  1. flextool/__init__.py +41 -0
  2. flextool/_mem_sampler.py +193 -0
  3. flextool/_resources.py +43 -0
  4. flextool/calibrate/__init__.py +51 -0
  5. flextool/calibrate/__main__.py +11 -0
  6. flextool/calibrate/_cli.py +316 -0
  7. flextool/calibrate/_db_alt.py +166 -0
  8. flextool/calibrate/_final_outputs.py +110 -0
  9. flextool/calibrate/_guard.py +151 -0
  10. flextool/calibrate/_loop.py +558 -0
  11. flextool/calibrate/_readers.py +223 -0
  12. flextool/calibrate/_report.py +263 -0
  13. flextool/calibrate/_sizing.py +699 -0
  14. flextool/calibrate/_solve.py +134 -0
  15. flextool/calibrate/_solve_status.py +495 -0
  16. flextool/cli/__init__.py +9 -0
  17. flextool/cli/_console.py +51 -0
  18. flextool/cli/_timing.py +147 -0
  19. flextool/cli/cmd_execute_flextool_workflow.py +187 -0
  20. flextool/cli/cmd_export_to_tabular.py +56 -0
  21. flextool/cli/cmd_import_sensitivities.py +75 -0
  22. flextool/cli/cmd_migrate_database.py +13 -0
  23. flextool/cli/cmd_open_results_db.py +269 -0
  24. flextool/cli/cmd_read_matpower.py +66 -0
  25. flextool/cli/cmd_read_old_flextool.py +63 -0
  26. flextool/cli/cmd_read_self_describing_tabular_input.py +50 -0
  27. flextool/cli/cmd_read_tabular_input.py +81 -0
  28. flextool/cli/cmd_run_flextool.py +1095 -0
  29. flextool/cli/cmd_scenario_results.py +284 -0
  30. flextool/cli/cmd_solve_mps.py +169 -0
  31. flextool/cli/cmd_update_flextool.py +17 -0
  32. flextool/cli/cmd_write_outputs.py +125 -0
  33. flextool/common_utils/__init__.py +1 -0
  34. flextool/common_utils/plot_mem_shape.py +77 -0
  35. flextool/common_utils/precision.py +451 -0
  36. flextool/decomposition/__init__.py +0 -0
  37. flextool/decomposition/region_decomposition.py +128 -0
  38. flextool/decomposition/region_filter.py +1261 -0
  39. flextool/engine_polars/__init__.py +110 -0
  40. flextool/engine_polars/_axis_enums.py +742 -0
  41. flextool/engine_polars/_benders.py +3462 -0
  42. flextool/engine_polars/_block_layout.py +1479 -0
  43. flextool/engine_polars/_blocks.py +1515 -0
  44. flextool/engine_polars/_commodity_ladder.py +660 -0
  45. flextool/engine_polars/_cumulative_invest.py +1165 -0
  46. flextool/engine_polars/_db_loader.py +153 -0
  47. flextool/engine_polars/_db_reader.py +127 -0
  48. flextool/engine_polars/_dc_power_flow.py +445 -0
  49. flextool/engine_polars/_delay.py +442 -0
  50. flextool/engine_polars/_derived_arithmetic.py +432 -0
  51. flextool/engine_polars/_derived_block.py +990 -0
  52. flextool/engine_polars/_derived_branch.py +769 -0
  53. flextool/engine_polars/_derived_existing.py +1353 -0
  54. flextool/engine_polars/_derived_npv.py +1297 -0
  55. flextool/engine_polars/_derived_params.py +9850 -0
  56. flextool/engine_polars/_derived_profile.py +881 -0
  57. flextool/engine_polars/_derived_walks.py +276 -0
  58. flextool/engine_polars/_determinism.py +70 -0
  59. flextool/engine_polars/_direct_params.py +2186 -0
  60. flextool/engine_polars/_dump_csvs.py +1009 -0
  61. flextool/engine_polars/_emit_arc_unions.py +1631 -0
  62. flextool/engine_polars/_emit_calc_params.py +729 -0
  63. flextool/engine_polars/_emit_chain_params.py +709 -0
  64. flextool/engine_polars/_emit_co2_accumulators.py +400 -0
  65. flextool/engine_polars/_emit_dispatchers.py +690 -0
  66. flextool/engine_polars/_emit_energy_margin.py +125 -0
  67. flextool/engine_polars/_emit_energy_margin_adder.py +290 -0
  68. flextool/engine_polars/_emit_entity_annual.py +428 -0
  69. flextool/engine_polars/_emit_inflow_scaling.py +1420 -0
  70. flextool/engine_polars/_emit_leaf_sets.py +550 -0
  71. flextool/engine_polars/_emit_lp_scaling.py +665 -0
  72. flextool/engine_polars/_emit_mid_sets.py +859 -0
  73. flextool/engine_polars/_emit_pdt_params.py +759 -0
  74. flextool/engine_polars/_emit_per_solve.py +774 -0
  75. flextool/engine_polars/_emit_period_calc.py +504 -0
  76. flextool/engine_polars/_emit_period_params.py +2398 -0
  77. flextool/engine_polars/_emit_provider_io.py +141 -0
  78. flextool/engine_polars/_emit_reserve.py +574 -0
  79. flextool/engine_polars/_emit_solve_time.py +311 -0
  80. flextool/engine_polars/_emit_solve_writers.py +1249 -0
  81. flextool/engine_polars/_flex_data_accumulator.py +388 -0
  82. flextool/engine_polars/_flex_data_provider.py +478 -0
  83. flextool/engine_polars/_group_slack.py +1253 -0
  84. flextool/engine_polars/_inmemory_reader.py +140 -0
  85. flextool/engine_polars/_input_source.py +336 -0
  86. flextool/engine_polars/_invest_seeds.py +191 -0
  87. flextool/engine_polars/_native_input_writer.py +100 -0
  88. flextool/engine_polars/_native_run_model.py +1348 -0
  89. flextool/engine_polars/_orchestration.py +4314 -0
  90. flextool/engine_polars/_output_writer.py +439 -0
  91. flextool/engine_polars/_param_shapes.py +1595 -0
  92. flextool/engine_polars/_parquet_bundle.py +723 -0
  93. flextool/engine_polars/_pdt_join.py +167 -0
  94. flextool/engine_polars/_pdt_lookup.py +547 -0
  95. flextool/engine_polars/_per_solve_sets.py +335 -0
  96. flextool/engine_polars/_projection_params.py +2056 -0
  97. flextool/engine_polars/_provider_keys.py +173 -0
  98. flextool/engine_polars/_provider_translators.py +225 -0
  99. flextool/engine_polars/_recursive_solve.py +703 -0
  100. flextool/engine_polars/_region_filter.py +2508 -0
  101. flextool/engine_polars/_reserve.py +649 -0
  102. flextool/engine_polars/_solve_acceptance.py +331 -0
  103. flextool/engine_polars/_solve_config.py +1001 -0
  104. flextool/engine_polars/_solve_context.py +885 -0
  105. flextool/engine_polars/_solve_handoff.py +164 -0
  106. flextool/engine_polars/_solve_state.py +232 -0
  107. flextool/engine_polars/_solver_base.py +36 -0
  108. flextool/engine_polars/_solver_dispatch.py +511 -0
  109. flextool/engine_polars/_spinedb_reader.py +1165 -0
  110. flextool/engine_polars/_stochastic.py +593 -0
  111. flextool/engine_polars/_subprocess_solve.py +1838 -0
  112. flextool/engine_polars/_timeline.py +1416 -0
  113. flextool/engine_polars/_vectorize.py +438 -0
  114. flextool/engine_polars/_warm.py +858 -0
  115. flextool/engine_polars/autoscale/__init__.py +107 -0
  116. flextool/engine_polars/autoscale/_config.py +218 -0
  117. flextool/engine_polars/autoscale/_layer2.py +1253 -0
  118. flextool/engine_polars/autoscale/_layer2_types.py +584 -0
  119. flextool/engine_polars/autoscale/_quantity_types.py +621 -0
  120. flextool/engine_polars/autoscale/_report.py +336 -0
  121. flextool/engine_polars/chain.py +259 -0
  122. flextool/engine_polars/input.py +6638 -0
  123. flextool/engine_polars/model.py +4754 -0
  124. flextool/env_check.py +388 -0
  125. flextool/export_to_tabular/__init__.py +5 -0
  126. flextool/export_to_tabular/db_reader.py +224 -0
  127. flextool/export_to_tabular/excel_writer.py +3559 -0
  128. flextool/export_to_tabular/export_settings.yaml +377 -0
  129. flextool/export_to_tabular/export_to_excel.py +227 -0
  130. flextool/export_to_tabular/formatting.py +543 -0
  131. flextool/export_to_tabular/sheet_config.py +876 -0
  132. flextool/gui/__init__.py +0 -0
  133. flextool/gui/__main__.py +118 -0
  134. flextool/gui/calibrate_commands.py +184 -0
  135. flextool/gui/calibrate_jobs.py +424 -0
  136. flextool/gui/check_tree.py +142 -0
  137. flextool/gui/cli_format.py +83 -0
  138. flextool/gui/config_parser.py +68 -0
  139. flextool/gui/data_models.py +362 -0
  140. flextool/gui/db_editor_integration.py +202 -0
  141. flextool/gui/db_version_check.py +269 -0
  142. flextool/gui/dialogs/__init__.py +0 -0
  143. flextool/gui/dialogs/add_dialog.py +1098 -0
  144. flextool/gui/dialogs/calibrate_dialog.py +1259 -0
  145. flextool/gui/dialogs/file_picker.py +473 -0
  146. flextool/gui/dialogs/group_picker.py +299 -0
  147. flextool/gui/dialogs/migration_consent_dialog.py +106 -0
  148. flextool/gui/dialogs/migration_progress_dialog.py +237 -0
  149. flextool/gui/dialogs/plot_dialog.py +459 -0
  150. flextool/gui/dialogs/plot_settings_picker.py +2184 -0
  151. flextool/gui/dialogs/project_dialog.py +426 -0
  152. flextool/gui/dialogs/update_dialog.py +212 -0
  153. flextool/gui/downsampling.py +88 -0
  154. flextool/gui/error_handling.py +50 -0
  155. flextool/gui/execution_manager.py +1715 -0
  156. flextool/gui/execution_window.py +1377 -0
  157. flextool/gui/hover_tooltip.py +111 -0
  158. flextool/gui/input_sources.py +730 -0
  159. flextool/gui/main_window.py +6181 -0
  160. flextool/gui/network_graph.py +215 -0
  161. flextool/gui/output_actions.py +393 -0
  162. flextool/gui/output_log_window.py +159 -0
  163. flextool/gui/platform_utils.py +421 -0
  164. flextool/gui/plot_cache.py +88 -0
  165. flextool/gui/plot_canvas.py +543 -0
  166. flextool/gui/plot_config_reader.py +272 -0
  167. flextool/gui/project_utils.py +100 -0
  168. flextool/gui/result_viewer.py +4394 -0
  169. flextool/gui/scenario_key.py +162 -0
  170. flextool/gui/scenario_lists.py +516 -0
  171. flextool/gui/settings_io.py +360 -0
  172. flextool/gui/solve_reader.py +103 -0
  173. flextool/gui/tree_reorder.py +88 -0
  174. flextool/gui/ui_metrics.py +420 -0
  175. flextool/input_derivation/__init__.py +281 -0
  176. flextool/input_derivation/_commodity_ladder.py +375 -0
  177. flextool/input_derivation/_commodity_ladder_sets.py +70 -0
  178. flextool/input_derivation/_dc_power_flow.py +377 -0
  179. flextool/input_derivation/_method_constants.py +77 -0
  180. flextool/input_derivation/_process_method.py +258 -0
  181. flextool/input_derivation/_specs.py +1026 -0
  182. flextool/input_derivation/_validators.py +321 -0
  183. flextool/lean_parquet.py +159 -0
  184. flextool/model_builder/__init__.py +5 -0
  185. flextool/model_builder/build_model.py +589 -0
  186. flextool/model_builder/encoding.py +67 -0
  187. flextool/model_builder/names.py +34 -0
  188. flextool/model_builder/profiles.py +129 -0
  189. flextool/plot_outputs/__init__.py +14 -0
  190. flextool/plot_outputs/axis_helpers.py +355 -0
  191. flextool/plot_outputs/color_template.py +888 -0
  192. flextool/plot_outputs/config.py +171 -0
  193. flextool/plot_outputs/format_helpers.py +345 -0
  194. flextool/plot_outputs/legend_helpers.py +143 -0
  195. flextool/plot_outputs/orchestrator.py +1141 -0
  196. flextool/plot_outputs/perf.py +37 -0
  197. flextool/plot_outputs/plan.py +1787 -0
  198. flextool/plot_outputs/plot_bars.py +1510 -0
  199. flextool/plot_outputs/plot_bars_detail.py +753 -0
  200. flextool/plot_outputs/plot_lines.py +951 -0
  201. flextool/plot_outputs/shared_manifest.py +564 -0
  202. flextool/plot_outputs/subplot_helpers.py +137 -0
  203. flextool/process_inputs/__init__.py +188 -0
  204. flextool/process_inputs/import_old_excel_input.json +4159 -0
  205. flextool/process_inputs/read_matpower.py +451 -0
  206. flextool/process_inputs/read_old_flextool.py +1288 -0
  207. flextool/process_inputs/read_self_describing_excel.py +1423 -0
  208. flextool/process_inputs/read_tabular_with_specification.py +1114 -0
  209. flextool/process_inputs/write_old_flextool_to_db.py +3077 -0
  210. flextool/process_inputs/write_self_describing_to_db.py +977 -0
  211. flextool/process_inputs/write_to_input_db.py +269 -0
  212. flextool/process_outputs/__init__.py +7 -0
  213. flextool/process_outputs/_annualize.py +55 -0
  214. flextool/process_outputs/_inmemory_helpers.py +292 -0
  215. flextool/process_outputs/_output_meta.py +672 -0
  216. flextool/process_outputs/calc_capacity_flows.py +107 -0
  217. flextool/process_outputs/calc_connections.py +136 -0
  218. flextool/process_outputs/calc_costs.py +260 -0
  219. flextool/process_outputs/calc_group_flows.py +192 -0
  220. flextool/process_outputs/calc_slacks.py +103 -0
  221. flextool/process_outputs/calc_storage_vre.py +160 -0
  222. flextool/process_outputs/drop_levels.py +208 -0
  223. flextool/process_outputs/handoff_writers.py +1315 -0
  224. flextool/process_outputs/out_ancillary.py +544 -0
  225. flextool/process_outputs/out_capacity.py +179 -0
  226. flextool/process_outputs/out_costs.py +334 -0
  227. flextool/process_outputs/out_flowgroup.py +189 -0
  228. flextool/process_outputs/out_flows.py +301 -0
  229. flextool/process_outputs/out_group.py +475 -0
  230. flextool/process_outputs/out_node.py +190 -0
  231. flextool/process_outputs/persist_realized_slice.py +601 -0
  232. flextool/process_outputs/process_results.py +24 -0
  233. flextool/process_outputs/read_highs_solution.py +2256 -0
  234. flextool/process_outputs/read_parameters.py +1799 -0
  235. flextool/process_outputs/read_sets.py +1095 -0
  236. flextool/process_outputs/read_variables.py +553 -0
  237. flextool/process_outputs/solve_order.py +81 -0
  238. flextool/process_outputs/spinedb_replay.py +412 -0
  239. flextool/process_outputs/union_realized_slice.py +224 -0
  240. flextool/process_outputs/write_outputs.py +1286 -0
  241. flextool/process_outputs/write_spinedb.py +1267 -0
  242. flextool/representative_periods/__init__.py +5 -0
  243. flextool/representative_periods/clustering.py +165 -0
  244. flextool/representative_periods/force_include.py +563 -0
  245. flextool/representative_periods/netload.py +365 -0
  246. flextool/representative_periods/netload_inputs.py +345 -0
  247. flextool/representative_periods/netload_iterate.py +722 -0
  248. flextool/representative_periods/preprocess.py +948 -0
  249. flextool/representative_periods/scenario_stack.py +195 -0
  250. flextool/representative_periods/weights.py +124 -0
  251. flextool/scenario_comparison/__init__.py +13 -0
  252. flextool/scenario_comparison/config_builder.py +158 -0
  253. flextool/scenario_comparison/constants.py +20 -0
  254. flextool/scenario_comparison/data_models.py +222 -0
  255. flextool/scenario_comparison/db_reader.py +399 -0
  256. flextool/scenario_comparison/dispatch_data.py +1002 -0
  257. flextool/scenario_comparison/dispatch_mappings.py +205 -0
  258. flextool/scenario_comparison/dispatch_plots.py +691 -0
  259. flextool/scenario_comparison/input_entity_colors.py +319 -0
  260. flextool/scenario_comparison/orchestrator.py +453 -0
  261. flextool/scenario_comparison/plan_union.py +244 -0
  262. flextool/scenario_comparison/plot_settings_seed.py +205 -0
  263. flextool/schemas/AXIS_CONTRACT.md +71 -0
  264. flextool/schemas/canonical_databases/howto_aggregate_output.json +6225 -0
  265. flextool/schemas/canonical_databases/howto_connections.json +5606 -0
  266. flextool/schemas/canonical_databases/howto_demand.json +5518 -0
  267. flextool/schemas/canonical_databases/howto_hydro_reservoir.json +6239 -0
  268. flextool/schemas/canonical_databases/howto_hydro_reservoir_with_pump.json +5933 -0
  269. flextool/schemas/canonical_databases/howto_non_sync_and_curtailment.json +5794 -0
  270. flextool/schemas/canonical_databases/howto_ramp_and_start_up.json +5707 -0
  271. flextool/schemas/canonical_databases/howto_stochastics.json +6032 -0
  272. flextool/schemas/canonical_databases/templates_examples.json +13532 -0
  273. flextool/schemas/canonical_databases/templates_time_settings_only.json +5340 -0
  274. flextool/schemas/comparison_settings_template.json +197 -0
  275. flextool/schemas/default_plot_settings.yaml +260 -0
  276. flextool/schemas/default_plots.yaml +2293 -0
  277. flextool/schemas/flextool_axis_contract.json +303 -0
  278. flextool/schemas/flextool_axis_contract.schema.json +247 -0
  279. flextool/schemas/old_flextool_import_template.json +4443 -0
  280. flextool/schemas/output_info_template.json +48 -0
  281. flextool/schemas/output_settings_template.json +256 -0
  282. flextool/schemas/pre_v26/flextool_template_constant_default.json +2105 -0
  283. flextool/schemas/pre_v26/flextool_template_default_optional_output.json +2152 -0
  284. flextool/schemas/pre_v26/flextool_template_default_value.json +2094 -0
  285. flextool/schemas/pre_v26/flextool_template_drop_down.json +2080 -0
  286. flextool/schemas/pre_v26/flextool_template_lifetime_method.json +1990 -0
  287. flextool/schemas/pre_v26/flextool_template_optional_outputs.json +2094 -0
  288. flextool/schemas/pre_v26/flextool_template_output_node_flows.json +2105 -0
  289. flextool/schemas/pre_v26/flextool_template_results_master.json +493 -0
  290. flextool/schemas/pre_v26/flextool_template_rolling_start_remove.json +2087 -0
  291. flextool/schemas/pre_v26/flextool_template_rolling_window.json +2059 -0
  292. flextool/schemas/pre_v26/flextool_template_storage_binding_defaults.json +46 -0
  293. flextool/schemas/pre_v26/flextool_template_v2.json +1990 -0
  294. flextool/schemas/pre_v26/flextool_template_v25.json +3864 -0
  295. flextool/schemas/spinedb_results_schema.json +581 -0
  296. flextool/schemas/spinedb_schema.json +4636 -0
  297. flextool/solver_config/copt.opt.template +18 -0
  298. flextool/solver_config/cplex.opt.template +25 -0
  299. flextool/solver_config/gurobi.opt.template +18 -0
  300. flextool/solver_config/highs.opt.template +18 -0
  301. flextool/solver_config/xpress.opt.template +26 -0
  302. flextool/spinedb_backend/__init__.py +26 -0
  303. flextool/spinedb_backend/_axis_enums.py +1119 -0
  304. flextool/spinedb_backend/_backend.py +1139 -0
  305. flextool/update_flextool/__init__.py +12 -0
  306. flextool/update_flextool/canonical_databases.py +251 -0
  307. flextool/update_flextool/db_migration.py +7108 -0
  308. flextool/update_flextool/ensure_settings_db.py +138 -0
  309. flextool/update_flextool/export_database.py +103 -0
  310. flextool/update_flextool/extend_tests_fixture.py +772 -0
  311. flextool/update_flextool/generate_canonical.py +274 -0
  312. flextool/update_flextool/initialize_database.py +42 -0
  313. flextool/update_flextool/install_info.py +225 -0
  314. flextool/update_flextool/self_update.py +464 -0
  315. flextool/update_flextool/sync_master_json_template.py +125 -0
  316. flextool/update_flextool/test_fixtures.py +187 -0
  317. flextool-4.0.0.dist-info/METADATA +217 -0
  318. flextool-4.0.0.dist-info/RECORD +322 -0
  319. flextool-4.0.0.dist-info/WHEEL +5 -0
  320. flextool-4.0.0.dist-info/entry_points.txt +17 -0
  321. flextool-4.0.0.dist-info/licenses/LICENSE.txt +19 -0
  322. 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