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,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)