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,208 @@
1
+ from types import SimpleNamespace
2
+
3
+ import pandas as pd
4
+
5
+
6
+ # Variables: droplevel('solve') only
7
+ _V_DROP = [
8
+ 'flow', 'ramp', 'reserve', 'state',
9
+ 'online_linear', 'startup_linear', 'shutdown_linear',
10
+ 'online_integer', 'startup_integer', 'shutdown_integer',
11
+ 'q_state_up', 'q_state_down', 'q_reserve',
12
+ 'q_inertia', 'q_non_synchronous', 'q_state_up_group', 'q_capacity_margin',
13
+ 'invest', 'divest',
14
+ 'dual_invest_connection', 'dual_invest_node', 'dual_invest_unit',
15
+ 'dual_maxInvest_period', 'dual_maxCumulative',
16
+ 'dual_maxInvestGroup_period', 'dual_maxInvestGroup_total', 'dual_maxInvestGroup_cumulative',
17
+ # Investment-floor (min-side) duals — all keep a ('solve', 'period')
18
+ # index (the min invest-total constraint, unlike max-total, retains its
19
+ # period axis), so every family drops the solve level here rather than
20
+ # collapsing to solve-only.
21
+ 'dual_minInvest_period', 'dual_minInvest_total', 'dual_minCumulative',
22
+ 'dual_minInvestGroup_period', 'dual_minInvestGroup_total', 'dual_minInvestGroup_cumulative',
23
+ 'dual_co2_max_period',
24
+ ]
25
+
26
+ # Variables with a solve-only row index — must collapse to a single
27
+ # row (last solve wins) instead of droplevel. Same pattern as
28
+ # ``dual_co2_max_total`` below.
29
+ _V_SOLVE_ONLY = [
30
+ 'dual_maxInvest_total',
31
+ ]
32
+
33
+ # Parameters: droplevel('solve') + deduplicate with keep='last'.
34
+ #
35
+ # Use this bucket for params whose per-roll values represent successive
36
+ # states of the same (entity, period) (e.g. ``entity_all_existing``,
37
+ # which is a chain-sum that gets updated as later rolls realise their
38
+ # investments — the LAST roll covering a given period carries the
39
+ # post-decision value).
40
+ _PAR_DROP = [
41
+ 'step_duration', 'timestep_weight', 'flow_min', 'flow_max',
42
+ 'process_availability', 'process_source_sink_varCost',
43
+ 'process_slope', 'process_section',
44
+ 'node_self_discharge_loss', 'node_penalty_up', 'node_penalty_down',
45
+ 'node_inflow', 'commodity_price', 'group_co2_price',
46
+ 'reserve_upDown_group_reservation', 'profile',
47
+ 'entity_annual_discounted', 'entity_annual_divest_discounted',
48
+ 'inflation_factor_investment_yearly',
49
+ # inflation_factor_operations_yearly evolves across rolls: each step
50
+ # carries period-d's "active" factor (4.54 ≈ Σ(1+r)^y × p_years[d,y])
51
+ # in its FIRST row and a forward-looking discount (3.73) in its
52
+ # SECOND row. ``keep='last'`` therefore picks the value from the
53
+ # step that COMMITS period d (where d is the realized invest/dispatch
54
+ # period), matching what the LP objective uses. ``keep='first'``
55
+ # would lock in the earlier step's forward-looking discount for every
56
+ # period after the first, leaving costs_discounted off by the
57
+ # active/forward ratio (~1.18 for the multi_year fixture).
58
+ 'inflation_factor_operations_yearly',
59
+ 'group_penalty_capacity_margin', 'group_capacity_margin',
60
+ # entity_all_existing carries the cumulative existing-capacity
61
+ # chain (existing + Σ prior invest − Σ prior divest). The last roll
62
+ # covering a given period writes the post-decision value; the
63
+ # earlier rolls' rows for the same period carry the pre-decision
64
+ # value. keep='last' picks the post-decision row to match v3.32.0.
65
+ 'entity_all_existing',
66
+ # entity_pre_existing is the static baseline before any invest
67
+ # decision. In practice constant across rolls for a given
68
+ # (entity, period), so either keep choice yields the same value —
69
+ # moved here for symmetry with entity_all_existing per the
70
+ # 2026-05-14 investigation into the unit_capacity__d.csv lag.
71
+ 'entity_pre_existing',
72
+ # NOTE: complete_period_share_of_year is intentionally NOT in this
73
+ # bucket — it needs an order-INDEPENDENT max-per-period reduction,
74
+ # done explicitly in drop_levels() below. See the comment there.
75
+ ]
76
+
77
+ # Parameters: droplevel('solve') + deduplicate with keep='first'.
78
+ #
79
+ # Use this bucket ONLY for params that are invariant across rolls (so
80
+ # either keep choice would yield the same result). Do NOT use this
81
+ # bucket for params whose value evolves across rolls — see _PAR_DROP
82
+ # above and the comment there.
83
+ _PAR_DEDUP = [
84
+ 'years_from_start_d', 'years_represented_d',
85
+ 'entity_max_units',
86
+ 'process_startup_cost',
87
+ 'entity_fixed_cost', 'entity_lifetime_fixed_cost', 'entity_lifetime_fixed_cost_divest',
88
+ 'node_annual_flow',
89
+ 'group_penalty_inertia', 'group_penalty_non_synchronous',
90
+ 'group_inertia_limit',
91
+ 'node_capacity_for_scaling', 'group_capacity_for_scaling',
92
+ ]
93
+
94
+
95
+ def _reduce_share_per_period(csoy: pd.Series) -> pd.Series:
96
+ """Collapse ``complete_period_share_of_year`` to one value per period.
97
+
98
+ ``complete_period_share_of_year`` is the per-period annualisation
99
+ denominator for DISPATCH outputs (energy/cost = Σ power·dur ÷ share).
100
+ It is NOT invariant across sub-solves: an invest sub-solve whose
101
+ period samples a representative window (e.g. 3×24 h) carries
102
+ ``share = 72/8760 ≈ 0.0082``, while the dispatch sub-solve that
103
+ realises the same period over the full year carries ``share = 1``.
104
+
105
+ BOTH sub-solves persist a row for the realised period, so a plain
106
+ ``keep='first'/'last'`` dedup is **order-dependent** — and the multi-
107
+ solve union order is NOT guaranteed: when the ``_solve_order.txt``
108
+ creation manifest is absent, ``union_realized_slice`` falls back to
109
+ filesystem glob order, which can place the invest row last and pick
110
+ the 0.0082 window share, inflating every period total by
111
+ ``8760/(sampled hours)`` (≈121.7× for a 3-day window). This was the
112
+ cause of an intermittent multi-solve period/aggregate over-count that
113
+ appeared on some scenarios/runs but not others.
114
+
115
+ The dispatch sub-solve covers the full period, so its share is always
116
+ ≥ any sampling sub-solve's for the same period. Reducing by **max per
117
+ period** therefore deterministically selects the realising-dispatch
118
+ share regardless of union order, and is byte-identical to the correct
119
+ (manifest-present) ``keep='last'`` runs. ``sort=False`` preserves the
120
+ first-appearance period order those runs produced.
121
+ """
122
+ csoy = csoy.droplevel('solve')
123
+ return csoy.groupby(level=0, sort=False).max()
124
+
125
+
126
+ def drop_levels(par: SimpleNamespace, s: SimpleNamespace, v: SimpleNamespace):
127
+ """Strip the 'solve' level from all time-indexed variables, parameters and sets.
128
+
129
+ Must be called before any post-processing calculations.
130
+ Returns (par, s, v) with the solve level removed in-place.
131
+ """
132
+ for attr in _V_DROP:
133
+ obj = getattr(v, attr)
134
+ obj = obj.droplevel('solve')
135
+ # For rolling scenarios, keep only last solve's value per (period, time)
136
+ if obj.index.duplicated().any():
137
+ obj = obj[~obj.index.duplicated(keep='last')]
138
+ setattr(v, attr, obj)
139
+
140
+ # Solve-only indexes (``dual_co2_max_total`` historically, plus
141
+ # ``dual_maxInvest_total`` which became solve-only when its reader
142
+ # arity was fixed): collapse to a single row (last solve wins).
143
+ for attr in [*_V_SOLVE_ONLY, 'dual_co2_max_total']:
144
+ obj = getattr(v, attr)
145
+ if not obj.empty:
146
+ last = obj.iloc[[-1]].copy()
147
+ last.index = pd.RangeIndex(1)
148
+ setattr(v, attr, last)
149
+ else:
150
+ setattr(v, attr, obj.reset_index(drop=True))
151
+
152
+ # v_angle: drop solve level only when non-empty (may be empty when no DC PF nodes)
153
+ if not v.angle.empty:
154
+ v.angle = v.angle.droplevel('solve')
155
+ if v.angle.index.duplicated().any():
156
+ v.angle = v.angle[~v.angle.index.duplicated(keep='last')]
157
+
158
+ for attr in _PAR_DROP:
159
+ obj = getattr(par, attr)
160
+ obj = obj.droplevel('solve')
161
+ if obj.index.duplicated().any():
162
+ obj = obj[~obj.index.duplicated(keep='last')]
163
+ setattr(par, attr, obj)
164
+
165
+ for attr in _PAR_DEDUP:
166
+ obj = getattr(par, attr)
167
+ obj = obj.droplevel('solve')
168
+ setattr(par, attr, obj[~obj.index.duplicated(keep='first')])
169
+
170
+ # complete_period_share_of_year — order-independent max-per-period
171
+ # reduction (deterministic across multi-solve union order). See
172
+ # _reduce_share_per_period for the full rationale.
173
+ par.complete_period_share_of_year = _reduce_share_per_period(
174
+ par.complete_period_share_of_year
175
+ )
176
+
177
+ # Sets have varied special handling so are done individually
178
+ s.solve_period = s.d_realized_period
179
+ # Save per-timestep solve mapping before dropping (for correct re-join in CSV output)
180
+ # For rolling scenarios, keep only the last solve's mapping per (period, time)
181
+ s.solve_period_time = s.dt_realize_dispatch
182
+ if s.solve_period_time.droplevel('solve').duplicated().any():
183
+ s.solve_period_time = s.solve_period_time[~s.solve_period_time.droplevel('solve').duplicated(keep='last')]
184
+ s.period = s.period.droplevel('solve')
185
+ s.period__time_first = s.period__time_first.droplevel('solve')
186
+ s.period_first_of_solve = s.period_first_of_solve.droplevel('solve')
187
+ s.period_in_use = s.period_in_use.droplevel('solve').unique()
188
+ s.d_realize_dispatch_or_invest = s.d_realize_dispatch_or_invest.droplevel('solve').unique()
189
+ s.d_realize_invest = s.d_realize_invest.droplevel('solve').unique()
190
+ s.d_realized_period = s.d_realized_period.droplevel('solve').unique()
191
+ s.dt_realize_dispatch = s.dt_realize_dispatch.droplevel('solve').unique()
192
+ s.dt = s.dt.droplevel('solve').unique()
193
+ s.dt_fix_storage_timesteps = s.dt_fix_storage_timesteps.droplevel('solve')
194
+ # For rolling scenarios, dtt/dtttdt have overlapping timesteps from different solves.
195
+ # Filter to only realized (period, time) pairs, keeping the last solve's mapping.
196
+ s.dtt = s.dtt.droplevel('solve')
197
+ dtt_pt = s.dtt.droplevel('t_previous')
198
+ s.dtt = s.dtt[dtt_pt.isin(s.dt_realize_dispatch) & ~dtt_pt.duplicated(keep='last')]
199
+ s.dtttdt = s.dtttdt.droplevel('solve')
200
+ dtttdt_pt = s.dtttdt.droplevel(['t_previous', 't_previous_within_timeset', 'd_previous', 't_previous_within_solve'])
201
+ s.dtttdt = s.dtttdt[dtttdt_pt.isin(s.dt_realize_dispatch) & ~dtttdt_pt.duplicated(keep='last')]
202
+ s.ed_invest = s.ed_invest.droplevel('solve').unique().join(s.d_realize_invest, how='inner')
203
+ s.edd_invest = s.edd_invest.droplevel('solve').unique()
204
+ s.edd_invest.names = ['entity', 'period_invest', 'period']
205
+ s.edd_invest = s.edd_invest.join(s.d_realize_invest, how='inner')
206
+ s.ed_divest = s.ed_divest.droplevel('solve').unique().join(s.d_realize_invest, how='inner')
207
+
208
+ return par, s, v