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,335 @@
1
+ """Per-solve sets derived natively from InputSource.
2
+
3
+ Produces — directly from an :class:`InputSource` (and an optional
4
+ active solve name) — the per-solve aggregates that preprocessing
5
+ emits to ``solve_data/*.csv``. Specifically:
6
+
7
+ * ``period_in_use`` — the ``[d]`` set of periods active in the current
8
+ solve (``setof d from dt`` projected from ``steps_in_use.csv``).
9
+ * ``dt_complete`` — the ``[d, t]`` set of complete-time-in-use pairs,
10
+ matching ``steps_complete_solve.csv``.
11
+ * ``period__timeline`` — the ``[d, timeline]`` mapping (timeset →
12
+ timeline lookup).
13
+ * ``p_timeline_duration_in_years`` — the ``[timeline, value]`` per-
14
+ timeline year fraction (``sum_t step_duration / 8760``).
15
+ * ``complete_period_share_of_year`` — the ``[d, value]`` per-period
16
+ year fraction, restricted to dt_complete.
17
+
18
+ The authoritative caller is :func:`derive_per_solve_aggregates`
19
+ which returns a typed :class:`PerSolveAggregates` dataclass. A
20
+ downstream consumer benefits:
21
+
22
+ * :mod:`flextool.engine_polars._derived_params` —
23
+ ``_dt_period_active_steps_from_workdir`` can fall through to native
24
+ derivation when the workdir CSVs are absent.
25
+
26
+ When a fixture has a ``solve.period_timeset`` filter for the active
27
+ solve in the source DB, the helpers no longer need ``solve_data/``
28
+ CSV output to compute their domain.
29
+
30
+ Architecture invariants
31
+ -----------------------
32
+
33
+ * **Lazy throughout** — every internal frame is a
34
+ :class:`polars.LazyFrame`; the public dataclass holds eager
35
+ :class:`polars.DataFrame` for caller convenience but every
36
+ computation funnels through one ``.collect()`` at the rim.
37
+ * **None default → skip entirely** — the public function returns
38
+ ``None`` when the source lacks ``solve.period_timeset`` /
39
+ ``timeline.timestep_duration``; callers fall through to the workdir
40
+ path when present, or the existing legacy CSV fallback.
41
+ * **No defensive gating** — helpers fail loudly when frames have
42
+ unexpected schemas.
43
+ """
44
+ from __future__ import annotations
45
+
46
+ from dataclasses import dataclass
47
+ from typing import TYPE_CHECKING
48
+
49
+ import polars as pl
50
+
51
+ from ._axis_enums import alias_to_axis
52
+
53
+ if TYPE_CHECKING:
54
+ from flextool.engine_polars._input_source import InputSource
55
+
56
+
57
+ __all__ = [
58
+ "PerSolveAggregates",
59
+ "derive_per_solve_aggregates",
60
+ ]
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # Public dataclass
65
+ # ---------------------------------------------------------------------------
66
+
67
+
68
+ @dataclass
69
+ class PerSolveAggregates:
70
+ """Native per-solve aggregates derived from an :class:`InputSource`.
71
+
72
+ All frames are eagerly materialised
73
+ (:class:`polars.DataFrame`) for caller convenience; internally the
74
+ derivation is lazy with one ``.collect()`` per field at the rim.
75
+
76
+ Attributes
77
+ ----------
78
+ period_in_use : pl.DataFrame
79
+ ``[d]`` distinct periods active in the current solve.
80
+ dt_complete : pl.DataFrame
81
+ ``[d, t]`` complete-time-in-use pairs (every (d, t) over the
82
+ period's full timeline range, not just dt).
83
+ period_timeline : pl.DataFrame
84
+ ``[d, timeline]`` mapping — one row per (period, timeline_name).
85
+ p_timeline_duration_in_years : pl.DataFrame
86
+ ``[timeline, value]`` — ``sum_t step_duration[tl, t] / 8760``.
87
+ complete_period_share_of_year : pl.DataFrame
88
+ ``[d, value]`` — period's full-timeline coverage as a year
89
+ fraction (``sum_t step_duration / 8760`` over dt_complete).
90
+ """
91
+
92
+ period_in_use: pl.DataFrame
93
+ dt_complete: pl.DataFrame
94
+ period_timeline: pl.DataFrame
95
+ p_timeline_duration_in_years: pl.DataFrame
96
+ complete_period_share_of_year: pl.DataFrame
97
+
98
+
99
+ # ---------------------------------------------------------------------------
100
+ # Internal source helpers
101
+ # ---------------------------------------------------------------------------
102
+
103
+
104
+ def _try_param(source: "InputSource", entity_class: str,
105
+ parameter_name: str) -> pl.DataFrame | None:
106
+ try:
107
+ df = source.parameter(entity_class, parameter_name)
108
+ except KeyError:
109
+ return None
110
+ if df.height == 0:
111
+ return None
112
+ return df
113
+
114
+
115
+ def _timeline_step_duration_lf(source: "InputSource"
116
+ ) -> pl.LazyFrame | None:
117
+ """Lazy ``[timeline, t, step_duration]`` from
118
+ ``timeline.timestep_duration``.
119
+
120
+ Returns ``None`` when the parameter is absent or has an unexpected
121
+ schema. The Spine source emits the index column under
122
+ ``t`` / ``step`` / ``timestep`` / ``x`` depending on the underlying
123
+ Map index_name; the helper auto-discovers the column.
124
+ """
125
+ tl_dur = _try_param(source, "timeline", "timestep_duration")
126
+ if tl_dur is None:
127
+ return None
128
+ cols = tl_dur.columns
129
+ step_col = next(
130
+ (c for c in ("t", "step", "timestep", "x")
131
+ if c in cols and c not in ("name", "value")),
132
+ None,
133
+ )
134
+ if step_col is None:
135
+ return None
136
+ return (tl_dur.lazy().select(
137
+ pl.col("name").alias("timeline"),
138
+ alias_to_axis(step_col, "t"),
139
+ pl.col("value").cast(pl.Float64).alias("step_duration"),
140
+ ))
141
+
142
+
143
+ def _period_timeset_lf(source: "InputSource", active_solve: str
144
+ ) -> pl.LazyFrame | None:
145
+ """Lazy ``[d, ts]`` for the active solve from ``solve.period_timeset``.
146
+
147
+ Returns ``None`` when the parameter is absent OR the filter for the
148
+ active solve is empty (synthetic rolling-horizon / nested-invest
149
+ sub-solves whose names aren't in Spine — caller handles None).
150
+ """
151
+ p_ts = _try_param(source, "solve", "period_timeset")
152
+ if p_ts is None:
153
+ return None
154
+ period_col = next((c for c in ("period", "x") if c in p_ts.columns),
155
+ None)
156
+ if period_col is None:
157
+ return None
158
+ lf = (p_ts.lazy()
159
+ .filter(pl.col("name") == active_solve)
160
+ .select(alias_to_axis(period_col, "d"),
161
+ pl.col("value").alias("ts")))
162
+ if lf.collect().height == 0:
163
+ return None
164
+ return lf
165
+
166
+
167
+ def _timeset_duration_lf(source: "InputSource") -> pl.LazyFrame | None:
168
+ """Lazy ``[ts, start_step, count]`` from ``timeset.timeset_duration``.
169
+
170
+ Mirrors the (start, count) Map semantics: each ts has zero or more
171
+ contiguous timestep blocks, each starting at ``start_step`` and
172
+ spanning ``count`` consecutive timesteps in the timeline's lex order.
173
+ """
174
+ ts_dur = _try_param(source, "timeset", "timeset_duration")
175
+ if ts_dur is None:
176
+ return None
177
+ cols = ts_dur.columns
178
+ step_col = next(
179
+ (c for c in ("t", "x", "step", "timestep")
180
+ if c in cols and c not in ("name", "value")),
181
+ None,
182
+ )
183
+ if step_col is None:
184
+ return None
185
+ return (ts_dur.lazy()
186
+ .select(pl.col("name").alias("ts"),
187
+ pl.col(step_col).alias("start_step"),
188
+ pl.col("value").cast(pl.Float64).alias("count")))
189
+
190
+
191
+ def _timeset_timeline_lf(source: "InputSource") -> pl.LazyFrame | None:
192
+ """Lazy ``[ts, timeline]`` from ``timeset.timeline``."""
193
+ ts_tl = _try_param(source, "timeset", "timeline")
194
+ if ts_tl is None:
195
+ return None
196
+ return (ts_tl.lazy()
197
+ .select(pl.col("name").alias("ts"),
198
+ pl.col("value").alias("timeline")))
199
+
200
+
201
+ # ---------------------------------------------------------------------------
202
+ # Per-period active-step expansion (mirrors flextool's get_active_time)
203
+ # ---------------------------------------------------------------------------
204
+
205
+
206
+ def _expand_active_steps_lf(
207
+ pt_lf: pl.LazyFrame, # [d, ts]
208
+ ts_tl_lf: pl.LazyFrame, # [ts, timeline]
209
+ ts_dur_lf: pl.LazyFrame, # [ts, start_step, count]
210
+ tl_lf: pl.LazyFrame, # [timeline, t, step_duration]
211
+ ) -> pl.LazyFrame:
212
+ """Expand (d, ts) → (d, t, step_duration) by walking the timeline
213
+ starting at each block's ``start_step`` for ``count`` steps.
214
+
215
+ Mirrors :func:`flextool.engine_polars._timeline.get_active_time`'s
216
+ procedural expansion: for each timeset block, find the rank of
217
+ ``start_step`` within the timeline's lex-ordered timestep list,
218
+ then emit timesteps with ranks in ``[start_rank, start_rank+count)``.
219
+ """
220
+ # Rank timeline timesteps in lex order (matches t0001 < t0002 < ...).
221
+ tl_ranked = (tl_lf.sort("timeline", "t")
222
+ .with_columns(rank=pl.col("t").cum_count()
223
+ .over("timeline")
224
+ .cast(pl.Int64)))
225
+ # Resolve start_rank by joining timeline ranks on (timeline, t=start_step).
226
+ blocks = (pt_lf
227
+ .join(ts_tl_lf, on="ts", how="inner")
228
+ .join(ts_dur_lf, on="ts", how="inner")
229
+ .join(tl_ranked.select(
230
+ pl.col("timeline"),
231
+ pl.col("t").alias("start_step"),
232
+ pl.col("rank").alias("start_rank")),
233
+ on=["timeline", "start_step"], how="left")
234
+ .filter(pl.col("start_rank").is_not_null()))
235
+ # Cross-join with timeline ranks, then filter rank-in-window.
236
+ expanded = (blocks
237
+ .join(tl_ranked, on="timeline", how="inner")
238
+ .filter((pl.col("rank") >= pl.col("start_rank"))
239
+ & (pl.col("rank") < pl.col("start_rank")
240
+ + pl.col("count").cast(pl.Int64))))
241
+ return expanded.select("d", "timeline", "t", "step_duration").unique()
242
+
243
+
244
+ # ---------------------------------------------------------------------------
245
+ # Public driver
246
+ # ---------------------------------------------------------------------------
247
+
248
+
249
+ def derive_per_solve_aggregates(
250
+ source: "InputSource",
251
+ active_solve: str,
252
+ ) -> PerSolveAggregates | None:
253
+ """Build per-solve aggregates natively from *source*.
254
+
255
+ Returns ``None`` when:
256
+ * ``timeline.timestep_duration`` is absent;
257
+ * ``solve.period_timeset`` for *active_solve* is empty (synthetic
258
+ rolling/nested sub-solves whose names aren't in Spine — caller
259
+ falls through to the workdir path);
260
+ * ``timeset.timeset_duration`` or ``timeset.timeline`` is absent.
261
+
262
+ Otherwise returns a fully-populated :class:`PerSolveAggregates`.
263
+
264
+ The derivation is lazy until each field's eager ``.collect()`` —
265
+ five collects total per call. Caller is expected to materialise
266
+ once per per-solve iteration and reuse across the override chain
267
+ (``_dt_period_active_steps``).
268
+ """
269
+ pt_lf = _period_timeset_lf(source, active_solve)
270
+ if pt_lf is None:
271
+ return None
272
+ ts_dur_lf = _timeset_duration_lf(source)
273
+ if ts_dur_lf is None:
274
+ return None
275
+ ts_tl_lf = _timeset_timeline_lf(source)
276
+ if ts_tl_lf is None:
277
+ return None
278
+ tl_lf = _timeline_step_duration_lf(source)
279
+ if tl_lf is None:
280
+ return None
281
+
282
+ # Expand to (d, t, timeline, step_duration) at full lazy.
283
+ expanded_lf = _expand_active_steps_lf(pt_lf, ts_tl_lf, ts_dur_lf, tl_lf)
284
+
285
+ # ── period_in_use[d] ─────────────────────────────────────────────
286
+ period_in_use = (expanded_lf.select("d").unique().sort("d").collect())
287
+ if period_in_use.height == 0:
288
+ # No active periods for this solve — caller falls through.
289
+ return None
290
+
291
+ # ── dt_complete[d, t] ────────────────────────────────────────────
292
+ # ``complete_active_time_lists = get_active_time(complete_solve_name)``
293
+ # — the ACTIVE expansion of the period's timeset blocks (NOT the
294
+ # full timeline). For non-rolling solves this equals dt; for
295
+ # rolling solves it's the parent solve's full-year expansion. Both
296
+ # are produced by the same expand-blocks-to-timesteps walk.
297
+ period_tl_lf = expanded_lf.select("d", "timeline").unique()
298
+ dt_complete = (expanded_lf.select("d", "t").unique()
299
+ .sort("d", "t").collect())
300
+
301
+ # ── period_timeline[d, timeline] ─────────────────────────────────
302
+ period_timeline = (period_tl_lf.sort("d", "timeline").collect())
303
+
304
+ # ── p_timeline_duration_in_years[timeline] ───────────────────────
305
+ # = sum_t step_duration[timeline, t] / 8760
306
+ p_tdy = (tl_lf.group_by("timeline")
307
+ .agg((pl.col("step_duration").sum() / 8760.0)
308
+ .alias("value"))
309
+ .sort("timeline")
310
+ .collect())
311
+
312
+ # ── complete_period_share_of_year[d] ─────────────────────────────
313
+ # Mirrors ``period_calculated_params.py`` lines 186-204:
314
+ # complete_hours_in_period[d] = sum_{(d2, t) ∈ dt_complete,
315
+ # (d2, d) ∈ period__branch}
316
+ # complete_step_duration[d2, t]
317
+ # For deterministic (non-stochastic) solves the period__branch is
318
+ # the diagonal — (d, d) for every d — so the sum collapses to
319
+ # ``sum_t complete_step_duration[d, t]``. Multi-branch stochastic
320
+ # extension is out of scope for the current dispatch; the
321
+ # PerSolveAggregates dataclass already exposes the per-(d, d) frame
322
+ # which downstream branch-aware helpers can fold themselves.
323
+ cpsoy = (expanded_lf
324
+ .group_by("d").agg((pl.col("step_duration").sum() / 8760.0)
325
+ .alias("value"))
326
+ .sort("d")
327
+ .collect())
328
+
329
+ return PerSolveAggregates(
330
+ period_in_use=period_in_use,
331
+ dt_complete=dt_complete,
332
+ period_timeline=period_timeline,
333
+ p_timeline_duration_in_years=p_tdy,
334
+ complete_period_share_of_year=cpsoy,
335
+ )