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,375 @@
1
+ """Commodity ladder input-derivation — cumulative + annual tabular emission.
2
+
3
+ Two ladder writers:
4
+
5
+ * :func:`derive_commodity_ladder_cumulative`
6
+ * :func:`derive_commodity_ladder_annual`
7
+
8
+ Both emit one canonical frame to the :class:`FlexDataProvider`:
9
+
10
+ * ``input/commodity_ladder_cumulative`` ``(commodity, tier, price, quantity)``
11
+ * ``input/commodity_ladder_annual`` ``(commodity, period, tier, price, quantity)``
12
+
13
+ The annual derivation auto-detects 2d vs 3d Spine maps and expands 2d
14
+ maps across the model period list (sourced from the Provider's
15
+ ``input/periods_available`` key — populated by the
16
+ ``_PARAMETER_SPECS`` loop — with DB fallbacks for setups that supply
17
+ periods only through ``solve.period_timeset``).
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+
23
+ import polars as pl
24
+
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Helpers
28
+ # ---------------------------------------------------------------------------
29
+
30
+
31
+ def _tier_sort_key(t: str) -> tuple[int, str]:
32
+ """Stable sort by integer tier when possible, else by string."""
33
+ try:
34
+ return (0, f"{int(t):020d}")
35
+ except ValueError:
36
+ return (1, t)
37
+
38
+
39
+ def _quantity_sentinel(quantity: str) -> str:
40
+ """Convert user-facing infinite quantities into the 1e30 sentinel
41
+ interpreted as the unbounded tail tier
42
+ (see ``ladder_tier_cap_infinite_cum`` / ``_ann``).
43
+ """
44
+ try:
45
+ q_float = float(quantity)
46
+ except ValueError:
47
+ q_float = float("inf")
48
+ if q_float == float("inf") or q_float >= 1e30:
49
+ return "1e30"
50
+ return quantity
51
+
52
+
53
+ def _iter_flat_ladder_rows(api, value, commodity: str,
54
+ logger: logging.Logger) -> list[list]:
55
+ """Flatten a Spine map ladder value. Returns the raw list-of-lists
56
+ from ``convert_map_to_table`` or an empty list on failure.
57
+ """
58
+ try:
59
+ return api.convert_map_to_table(value)
60
+ except Exception as exc:
61
+ logger.warning(
62
+ "Could not flatten ladder for commodity '%s': %s",
63
+ commodity, exc,
64
+ )
65
+ return []
66
+
67
+
68
+ def _collect_periods(backend, provider, api) -> list[str]:
69
+ """Return the model's period list (for 1d-map → per-period expansion
70
+ of ``price_ladder_annual``).
71
+
72
+ Step 2.5-F Phase E
73
+ ------------------
74
+
75
+ Provider-first: the ``input/periods_available`` key is populated by
76
+ :meth:`SpineDBBackend.parameter_values` during the
77
+ ``_PARAMETER_SPECS`` pass for ``model.periods_available``. DB
78
+ fallbacks (in order) cover the case where the parameter is set as a
79
+ raw map or where periods come exclusively from
80
+ ``solve.period_timeset``.
81
+ """
82
+ periods: list[str] = []
83
+ seen: set[str] = set()
84
+
85
+ # Provider arm — frame layout: "model,period_from_model" (the legacy
86
+ # input/periods_available.csv schema). Take the rightmost column.
87
+ if provider is not None and provider.has("input/periods_available"):
88
+ df = provider.get("input/periods_available")
89
+ if df is not None and df.height > 0:
90
+ col = df.columns[-1]
91
+ for value in df.get_column(col).to_list():
92
+ v = str(value).strip()
93
+ if v and v not in seen:
94
+ periods.append(v)
95
+ seen.add(v)
96
+ if periods:
97
+ return periods
98
+
99
+ # Fallback 1: raw map on model.periods_available.
100
+ for pv in backend.find_parameter_values(
101
+ entity_class_name="model",
102
+ parameter_definition_name="periods_available",
103
+ ):
104
+ if pv["type"] is None:
105
+ continue
106
+ val = pv["parsed_value"]
107
+ try:
108
+ flat = api.convert_map_to_table(val)
109
+ except Exception:
110
+ flat = []
111
+ for entry in flat:
112
+ for c in (str(x) for x in entry):
113
+ if c and c not in seen:
114
+ periods.append(c)
115
+ seen.add(c)
116
+ if periods:
117
+ return periods
118
+
119
+ # Fallback 2: scan solve.period_timeset map indexes.
120
+ for pv in backend.find_parameter_values(
121
+ entity_class_name="solve",
122
+ parameter_definition_name="period_timeset",
123
+ ):
124
+ if pv["type"] is None:
125
+ continue
126
+ val = pv["parsed_value"]
127
+ try:
128
+ flat = api.convert_map_to_table(val)
129
+ except Exception:
130
+ flat = []
131
+ for entry in flat:
132
+ if len(entry) >= 1:
133
+ p = str(entry[0])
134
+ if p and p not in seen:
135
+ periods.append(p)
136
+ seen.add(p)
137
+ return periods
138
+
139
+
140
+ # ---------------------------------------------------------------------------
141
+ # Phase D — commodity_ladder_cumulative
142
+ # ---------------------------------------------------------------------------
143
+
144
+
145
+ def derive_commodity_ladder_cumulative(
146
+ backend, provider, logger: logging.Logger,
147
+ ) -> None:
148
+ """Run the cumulative-ladder derivation.
149
+
150
+ Emits the Provider frame ``input/commodity_ladder_cumulative`` with
151
+ columns ``(commodity, tier, price, quantity)`` — one row per
152
+ (commodity, tier).
153
+
154
+ Only the ``commodity.price_ladder_cumulative`` parameter is
155
+ consulted (always a 2d map: ``Map(tier -> {price, quantity})`` —
156
+ 2d in Spine's counting because ``{price, quantity}`` is a second
157
+ index layer). The ``price_method`` filter happens mod-side via
158
+ the ``commodity_with_ladder_cumulative`` set.
159
+ """
160
+ api = backend.api
161
+ rows: list[tuple[str, int, str, str]] = []
162
+
163
+ for pv in backend.find_parameter_values(
164
+ entity_class_name="commodity",
165
+ parameter_definition_name="price_ladder_cumulative",
166
+ ):
167
+ if pv["type"] is None:
168
+ continue
169
+ if pv["type"] != "map":
170
+ logger.warning(
171
+ "commodity.price_ladder_cumulative on '%s' has type %s "
172
+ "(expected nested 1d map); skipping.",
173
+ pv["entity_byname"][0], pv["type"],
174
+ )
175
+ continue
176
+ commodity = pv["entity_byname"][0]
177
+ flat = _iter_flat_ladder_rows(api, pv["parsed_value"], commodity, logger)
178
+ per_tier: dict[str, dict[str, str]] = {}
179
+ for entry in flat:
180
+ # Expected layout: [tier_idx, facet, value] (length 3).
181
+ if len(entry) < 3:
182
+ continue
183
+ tier_str = str(entry[0])
184
+ facet = str(entry[1])
185
+ val = entry[-1]
186
+ per_tier.setdefault(tier_str, {})[facet] = str(val)
187
+
188
+ for tier_str in sorted(per_tier.keys(), key=_tier_sort_key):
189
+ facets = per_tier[tier_str]
190
+ price = facets.get("price", "0")
191
+ quantity = _quantity_sentinel(facets.get("quantity", "inf"))
192
+ try:
193
+ tier_int = int(tier_str)
194
+ except ValueError:
195
+ logger.warning(
196
+ "commodity.price_ladder_cumulative tier on '%s' is not "
197
+ "an integer ('%s'); skipping tier.",
198
+ commodity, tier_str,
199
+ )
200
+ continue
201
+ rows.append((commodity, tier_int, price, quantity))
202
+
203
+ # All-Utf8 frame to match the legacy CSV's textual schema (consumers
204
+ # read these through the writer-port ``_read_csv`` helper which
205
+ # expects string columns).
206
+ frame = pl.DataFrame(
207
+ {
208
+ "commodity": [r[0] for r in rows],
209
+ "tier": [str(r[1]) for r in rows],
210
+ "price": [r[2] for r in rows],
211
+ "quantity": [r[3] for r in rows],
212
+ },
213
+ schema={
214
+ "commodity": pl.Utf8,
215
+ "tier": pl.Utf8,
216
+ "price": pl.Utf8,
217
+ "quantity": pl.Utf8,
218
+ },
219
+ )
220
+ provider.put("input/commodity_ladder_cumulative", frame)
221
+
222
+
223
+ # ---------------------------------------------------------------------------
224
+ # Phase E — commodity_ladder_annual (2d / 3d auto-detect + period expand)
225
+ # ---------------------------------------------------------------------------
226
+
227
+
228
+ def derive_commodity_ladder_annual(
229
+ backend, provider, logger: logging.Logger,
230
+ ) -> None:
231
+ """Run the annual-ladder derivation.
232
+
233
+ Emits the Provider frame ``input/commodity_ladder_annual`` with
234
+ columns ``(commodity, period, tier, price, quantity)`` — one row
235
+ per (commodity, period, tier).
236
+
237
+ Reads ``commodity.price_ladder_annual``. Auto-detects the map depth:
238
+
239
+ * 2d form (Spine 2d_map): ``Map(tier -> {price, quantity})`` — the
240
+ same (price, quantity) is expanded across every model period.
241
+ * 3d form (Spine 3d_map): ``Map(period -> Map(tier -> {price,
242
+ quantity}))`` — per-period rows are kept as-is.
243
+ """
244
+ api = backend.api
245
+ rows: list[tuple[str, str, int, str, str]] = []
246
+ periods_cache: list[str] | None = None
247
+
248
+ for pv in backend.find_parameter_values(
249
+ entity_class_name="commodity",
250
+ parameter_definition_name="price_ladder_annual",
251
+ ):
252
+ if pv["type"] is None:
253
+ continue
254
+ if pv["type"] != "map":
255
+ logger.warning(
256
+ "commodity.price_ladder_annual on '%s' has type %s "
257
+ "(expected nested map); skipping.",
258
+ pv["entity_byname"][0], pv["type"],
259
+ )
260
+ continue
261
+ commodity = pv["entity_byname"][0]
262
+ flat = _iter_flat_ladder_rows(api, pv["parsed_value"], commodity, logger)
263
+ if not flat:
264
+ continue
265
+
266
+ # Depth detection via the flat table row length. Spine's Map
267
+ # dimension count matches the flat row length: 2d_map yields
268
+ # length-3 rows, 3d_map yields length-4 rows.
269
+ # 2d_map: [tier, facet, value] → len 3
270
+ # 3d_map: [period, tier, facet, value] → len 4
271
+ max_len = max((len(row) for row in flat), default=0)
272
+ if max_len == 3:
273
+ # 2d_map → expand across all model periods.
274
+ per_tier: dict[str, dict[str, str]] = {}
275
+ for entry in flat:
276
+ if len(entry) < 3:
277
+ continue
278
+ tier_str = str(entry[0])
279
+ facet = str(entry[1])
280
+ val = entry[-1]
281
+ per_tier.setdefault(tier_str, {})[facet] = str(val)
282
+ if periods_cache is None:
283
+ periods_cache = _collect_periods(backend, provider, api)
284
+ if not periods_cache:
285
+ logger.warning(
286
+ "commodity.price_ladder_annual on '%s' is 2d_map but "
287
+ "no model periods were available for expansion; "
288
+ "skipping.", commodity,
289
+ )
290
+ continue
291
+ for period in periods_cache:
292
+ for tier_str in sorted(per_tier.keys(), key=_tier_sort_key):
293
+ facets = per_tier[tier_str]
294
+ price = facets.get("price", "0")
295
+ quantity = _quantity_sentinel(facets.get("quantity", "inf"))
296
+ try:
297
+ tier_int = int(tier_str)
298
+ except ValueError:
299
+ logger.warning(
300
+ "commodity.price_ladder_annual tier on '%s' is "
301
+ "not an integer ('%s'); skipping tier.",
302
+ commodity, tier_str,
303
+ )
304
+ continue
305
+ rows.append(
306
+ (commodity, period, tier_int, price, quantity)
307
+ )
308
+ elif max_len >= 4:
309
+ # 3d_map → per-period. Flat row layout [period, tier,
310
+ # facet, value] — Spine nests Map(period -> Map(tier ->
311
+ # {price, quantity})).
312
+ per_period_tier: dict[tuple[str, str], dict[str, str]] = {}
313
+ for entry in flat:
314
+ if len(entry) < 4:
315
+ continue
316
+ period = str(entry[0])
317
+ tier_str = str(entry[1])
318
+ facet = str(entry[2])
319
+ val = entry[-1]
320
+ per_period_tier.setdefault(
321
+ (period, tier_str), {}
322
+ )[facet] = str(val)
323
+
324
+ def _sort_key(k: tuple[str, str]) -> tuple:
325
+ return (k[0], _tier_sort_key(k[1]))
326
+
327
+ for (period, tier_str) in sorted(
328
+ per_period_tier.keys(), key=_sort_key,
329
+ ):
330
+ facets = per_period_tier[(period, tier_str)]
331
+ price = facets.get("price", "0")
332
+ quantity = _quantity_sentinel(facets.get("quantity", "inf"))
333
+ try:
334
+ tier_int = int(tier_str)
335
+ except ValueError:
336
+ logger.warning(
337
+ "commodity.price_ladder_annual tier on '%s' is not "
338
+ "an integer ('%s'); skipping tier.",
339
+ commodity, tier_str,
340
+ )
341
+ continue
342
+ rows.append(
343
+ (commodity, period, tier_int, price, quantity)
344
+ )
345
+ else:
346
+ logger.warning(
347
+ "commodity.price_ladder_annual on '%s' has unexpected "
348
+ "flattened shape (max row length %d); skipping.",
349
+ commodity, max_len,
350
+ )
351
+ continue
352
+
353
+ frame = pl.DataFrame(
354
+ {
355
+ "commodity": [r[0] for r in rows],
356
+ "period": [r[1] for r in rows],
357
+ "tier": [str(r[2]) for r in rows],
358
+ "price": [r[3] for r in rows],
359
+ "quantity": [r[4] for r in rows],
360
+ },
361
+ schema={
362
+ "commodity": pl.Utf8,
363
+ "period": pl.Utf8,
364
+ "tier": pl.Utf8,
365
+ "price": pl.Utf8,
366
+ "quantity": pl.Utf8,
367
+ },
368
+ )
369
+ provider.put("input/commodity_ladder_annual", frame)
370
+
371
+
372
+ __all__ = [
373
+ "derive_commodity_ladder_cumulative",
374
+ "derive_commodity_ladder_annual",
375
+ ]
@@ -0,0 +1,70 @@
1
+ """Commodity-ladder filtered subsets of ``commodity``.
2
+
3
+ Three single-dimension sets selected by ``p_commodity_price_method``:
4
+
5
+ set commodity_with_ladder := {c : price_method[c] != 'price'};
6
+ set commodity_with_ladder_annual := {c : price_method[c] == 'price_ladder_annual'};
7
+ set commodity_with_ladder_cumulative := {c : price_method[c] == 'price_ladder_cumulative'};
8
+
9
+ ``p_commodity_price_method`` has ``default 'price'``, so commodities
10
+ not present in the price-method CSV are treated as ``'price'`` and
11
+ therefore excluded from all three sets.
12
+
13
+ Output frames (canonical CSV column = ``commodity``):
14
+
15
+ * ``solve_data/commodity_with_ladder``
16
+ * ``solve_data/commodity_with_ladder_annual``
17
+ * ``solve_data/commodity_with_ladder_cumulative``
18
+
19
+ The frames land under the ``solve_data/<name>`` Provider key, matching
20
+ the ``solve_data/<file>.csv`` paths that consumers
21
+ (``_emit_mid_sets``, ``_emit_per_solve``, ``model.py``, the
22
+ ``_commodity_ladder`` cascade loader) resolve through the
23
+ Provider-aware ``_read_csv`` helper.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import polars as pl
28
+
29
+
30
+ def derive_commodity_ladder_sets(backend, provider) -> None:
31
+ """Run the commodity-ladder-sets derivation.
32
+
33
+ Reads ``commodity.price_method`` via the
34
+ :class:`SpineDBBackend` and emits three Provider frames.
35
+ """
36
+ # Read price_method per commodity directly from the Backend.
37
+ price_methods: dict[str, str] = {}
38
+ for pv in backend.find_parameter_values(
39
+ entity_class_name="commodity",
40
+ parameter_definition_name="price_method",
41
+ ):
42
+ if pv["type"] is None:
43
+ continue
44
+ price_methods[pv["entity_byname"][0]] = str(pv["parsed_value"])
45
+
46
+ with_ladder = list(dict.fromkeys(
47
+ c for c, m in price_methods.items() if m != "price"
48
+ ))
49
+ with_annual = list(dict.fromkeys(
50
+ c for c, m in price_methods.items() if m == "price_ladder_annual"
51
+ ))
52
+ with_cum = list(dict.fromkeys(
53
+ c for c, m in price_methods.items() if m == "price_ladder_cumulative"
54
+ ))
55
+
56
+ for key, rows in (
57
+ ("solve_data/commodity_with_ladder", with_ladder),
58
+ ("solve_data/commodity_with_ladder_annual", with_annual),
59
+ ("solve_data/commodity_with_ladder_cumulative", with_cum),
60
+ ):
61
+ provider.put(
62
+ key,
63
+ pl.DataFrame(
64
+ {"commodity": rows},
65
+ schema={"commodity": pl.Utf8},
66
+ ),
67
+ )
68
+
69
+
70
+ __all__ = ["derive_commodity_ladder_sets"]