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,550 @@
1
+ """Leaf-level set projections.
2
+
3
+ Four families of trivial "setof" derivations. Each helper reads one
4
+ or two ``input/*.csv`` (or ``solve_data/*.csv``) files and emits a
5
+ small single- or multi-column set CSV under ``solve_data/``:
6
+
7
+ * ``period_param_sets`` — 4 period projections from pd_*.csv.
8
+ * ``invest_method_sets`` — 4 method-filter projections.
9
+ * ``co2_method_sets`` — 3 co2-method projections.
10
+ * ``simple_projections`` — 11 trivial setof projections.
11
+
12
+ Each public ``derive_*`` returns a fresh ``pl.DataFrame`` in-memory.
13
+ ``write_*`` wrappers materialise that frame to the canonical CSV path
14
+ so downstream consumers see the same file layout.
15
+
16
+ Style: read tiny CSVs eagerly with polars, project with native polars
17
+ expressions, deduplicate via ``.unique(maintain_order=True)``. No
18
+ abstraction beyond a per-family ``write_all`` orchestrator. Match
19
+ :mod:`._derived_existing` / :mod:`._projection_params` style.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ from pathlib import Path
24
+
25
+ import polars as pl
26
+
27
+ from . import _provider_keys as K
28
+ from ._axis_enums import alias_to_axis
29
+ from ._emit_provider_io import _emit
30
+
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Method-enum constants — mirror flextool/flextool_base.dat and the legacy
34
+ # constants in preprocessing/{invest_method_sets,co2_method_sets}.py.
35
+ # ---------------------------------------------------------------------------
36
+
37
+ _INVEST_METHOD_NOT_ALLOWED: frozenset[str] = frozenset((
38
+ "not_allowed", "retire_period", "retire_total", "retire_no_limit",
39
+ ))
40
+ _DIVEST_METHOD_NOT_ALLOWED: frozenset[str] = frozenset((
41
+ "not_allowed", "invest_period", "invest_total", "invest_no_limit",
42
+ ))
43
+
44
+ _CO2_PRICE_METHOD: frozenset[str] = frozenset((
45
+ "price", "price_period", "price_total", "price_period_total",
46
+ ))
47
+ _CO2_MAX_PERIOD_METHOD: frozenset[str] = frozenset((
48
+ "period", "price_period", "period_total", "price_period_total",
49
+ ))
50
+ _CO2_MAX_TOTAL_METHOD: frozenset[str] = frozenset((
51
+ "total", "price_total", "period_total", "price_period_total",
52
+ ))
53
+
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # Internal CSV reader. All inputs are tiny set/parameter CSVs (≤ thousands
57
+ # of rows in any realistic fixture) so eager read is fine. Missing source
58
+ # means "empty set" — flextool's legacy code returns ``[]`` in that case.
59
+ # ---------------------------------------------------------------------------
60
+
61
+ def _read_csv(path: Path, columns: list[str],
62
+ *, provider: "object | None" = None) -> pl.DataFrame:
63
+ """Provider-only — returns an empty all-Utf8 frame on Provider miss.
64
+
65
+ Step 2.5 Phase C dropped the disk-fallback arm. The
66
+ canonical-schema empty frame is the documented behaviour when an
67
+ upstream writer hasn't populated *path*'s key (legacy behaviour
68
+ for a missing on-disk CSV).
69
+ """
70
+ from flextool.engine_polars._emit_provider_io import (
71
+ _provider_key,
72
+ _provider_lookup_positional,
73
+ )
74
+ seeded = _provider_lookup_positional(
75
+ provider, _provider_key(path), path, columns,
76
+ )
77
+ if seeded is not None:
78
+ return seeded
79
+ return pl.DataFrame(
80
+ {c: [] for c in columns}, schema={c: pl.Utf8 for c in columns},
81
+ )
82
+
83
+
84
+ # ---------------------------------------------------------------------------
85
+ # Family 1 — period_param_sets (legacy: preprocessing/period_param_sets.py)
86
+ # ---------------------------------------------------------------------------
87
+
88
+ # (source pd_*.csv, target solve_data csv)
89
+ _PERIOD_PARAM_SOURCES: list[tuple[str, str]] = [
90
+ ("pd_group.csv", "period_group.csv"),
91
+ ("pd_node.csv", "period_node.csv"),
92
+ ("pd_commodity.csv", "period_commodity.csv"),
93
+ ("pd_process.csv", "period_process.csv"),
94
+ ]
95
+
96
+
97
+ def derive_period_param_set(input_dir: Path, source_csv: str,
98
+ *, provider: "object | None" = None,
99
+ ) -> pl.DataFrame:
100
+ """Project the ``period`` column out of a ``pd_*.csv`` file.
101
+
102
+ Legacy: setof {(e, param, d, value) in entity__param__period} (d).
103
+ Order = first occurrence in the source CSV.
104
+ """
105
+ df = _read_csv(input_dir / source_csv,
106
+ ["entity", "param", "period", "value"],
107
+ provider=provider)
108
+ return (
109
+ df.filter(pl.col("period") != "")
110
+ .select("period")
111
+ .unique(maintain_order=True)
112
+ )
113
+
114
+
115
+ def emit_period_param_sets(input_dir: Path, solve_data_dir: Path,
116
+ *, provider) -> None:
117
+ """Emit ``period_param_sets`` to the Provider."""
118
+ del solve_data_dir # legacy signature parity; keys are static
119
+ for source_csv, target_name in _PERIOD_PARAM_SOURCES:
120
+ _emit(provider, f"solve_data/{target_name}",
121
+ derive_period_param_set(input_dir, source_csv, provider=provider))
122
+
123
+
124
+ # ---------------------------------------------------------------------------
125
+ # Family 2 — invest_method_sets (legacy: preprocessing/invest_method_sets.py)
126
+ # ---------------------------------------------------------------------------
127
+
128
+ def _project_entity_by_method(
129
+ df: pl.DataFrame,
130
+ disallowed_methods: frozenset[str],
131
+ out_column: str,
132
+ ) -> pl.DataFrame:
133
+ """``setof {(e, m) : m not in disallowed} (e)`` — order preserved."""
134
+ return (
135
+ df.filter(
136
+ (pl.col("entity") != "") & (~pl.col("method").is_in(list(disallowed_methods)))
137
+ )
138
+ .select(alias_to_axis("entity", out_column))
139
+ .unique(maintain_order=True)
140
+ )
141
+
142
+
143
+ def derive_entity_invest(input_dir: Path,
144
+ *, provider: "object | None" = None,
145
+ ) -> pl.DataFrame:
146
+ df = _read_csv(input_dir / "entity__invest_method.csv",
147
+ ["entity", "method"], provider=provider)
148
+ return _project_entity_by_method(df, _INVEST_METHOD_NOT_ALLOWED, "entity")
149
+
150
+
151
+ def derive_entity_divest(input_dir: Path,
152
+ *, provider: "object | None" = None,
153
+ ) -> pl.DataFrame:
154
+ df = _read_csv(input_dir / "entity__invest_method.csv",
155
+ ["entity", "method"], provider=provider)
156
+ return _project_entity_by_method(df, _DIVEST_METHOD_NOT_ALLOWED, "entity")
157
+
158
+
159
+ def derive_group_invest(input_dir: Path,
160
+ *, provider: "object | None" = None,
161
+ ) -> pl.DataFrame:
162
+ df = _read_csv(input_dir / "group__invest_method.csv",
163
+ ["entity", "method"], provider=provider)
164
+ return _project_entity_by_method(df, _INVEST_METHOD_NOT_ALLOWED, "group")
165
+
166
+
167
+ def derive_group_divest(input_dir: Path,
168
+ *, provider: "object | None" = None,
169
+ ) -> pl.DataFrame:
170
+ df = _read_csv(input_dir / "group__invest_method.csv",
171
+ ["entity", "method"], provider=provider)
172
+ return _project_entity_by_method(df, _DIVEST_METHOD_NOT_ALLOWED, "group")
173
+
174
+
175
+ def emit_invest_method_sets(input_dir: Path, solve_data_dir: Path,
176
+ *, provider) -> None:
177
+ """Emit ``invest_method_sets`` to the Provider."""
178
+ del solve_data_dir
179
+ _emit(provider, "solve_data/entityInvest.csv",
180
+ derive_entity_invest(input_dir, provider=provider))
181
+ _emit(provider, "solve_data/entityDivest.csv",
182
+ derive_entity_divest(input_dir, provider=provider))
183
+ _emit(provider, "solve_data/group_invest.csv",
184
+ derive_group_invest(input_dir, provider=provider))
185
+ _emit(provider, "solve_data/group_divest.csv",
186
+ derive_group_divest(input_dir, provider=provider))
187
+
188
+
189
+ # ---------------------------------------------------------------------------
190
+ # Family 3 — co2_method_sets (legacy: preprocessing/co2_method_sets.py)
191
+ # ---------------------------------------------------------------------------
192
+
193
+ def _project_group_by_method_in(
194
+ df: pl.DataFrame, allowed_methods: frozenset[str],
195
+ ) -> pl.DataFrame:
196
+ return (
197
+ df.filter(
198
+ (pl.col("group") != "") & pl.col("method").is_in(list(allowed_methods))
199
+ )
200
+ .select("group")
201
+ .unique(maintain_order=True)
202
+ )
203
+
204
+
205
+ def derive_group_co2(input_dir: Path, kind: str,
206
+ *, provider: "object | None" = None,
207
+ ) -> pl.DataFrame:
208
+ """Project groups whose co2_method ∈ allowed set.
209
+
210
+ ``kind`` is one of ``"price"``, ``"max_period"``, ``"max_total"``.
211
+ """
212
+ allowed = {
213
+ "price": _CO2_PRICE_METHOD,
214
+ "max_period": _CO2_MAX_PERIOD_METHOD,
215
+ "max_total": _CO2_MAX_TOTAL_METHOD,
216
+ }[kind]
217
+ df = _read_csv(input_dir / "group__co2_method.csv",
218
+ ["group", "method"], provider=provider)
219
+ return _project_group_by_method_in(df, allowed)
220
+
221
+
222
+ def emit_co2_method_sets(input_dir: Path, solve_data_dir: Path,
223
+ *, provider) -> None:
224
+ """Emit ``co2_method_sets`` to the Provider."""
225
+ del solve_data_dir
226
+ for kind, target in (
227
+ ("price", "group_co2_price.csv"),
228
+ ("max_period", "group_co2_max_period.csv"),
229
+ ("max_total", "group_co2_max_total.csv"),
230
+ ):
231
+ _emit(provider, f"solve_data/{target}",
232
+ derive_group_co2(input_dir, kind, provider=provider))
233
+
234
+
235
+ # ---------------------------------------------------------------------------
236
+ # Family 4 — simple_projections (legacy: preprocessing/simple_projections.py)
237
+ # ---------------------------------------------------------------------------
238
+
239
+ def derive_optional_yes(input_dir: Path,
240
+ *, provider: "object | None" = None,
241
+ ) -> pl.DataFrame:
242
+ """optional_outputs filtered to value == 'yes'."""
243
+ df = _read_csv(input_dir / "optional_outputs.csv",
244
+ ["output", "value"], provider=provider)
245
+ return (
246
+ df.filter(pl.col("value") == "yes")
247
+ .select("output")
248
+ .unique(maintain_order=True)
249
+ )
250
+
251
+
252
+ def derive_reserve_upDown_group(input_dir: Path,
253
+ *, provider: "object | None" = None,
254
+ ) -> pl.DataFrame:
255
+ """3-tuple (reserve, upDown, group) for method != 'no_reserve'."""
256
+ df = _read_csv(
257
+ input_dir / "reserve__upDown__group__method.csv",
258
+ ["reserve", "upDown", "group", "method"],
259
+ provider=provider,
260
+ )
261
+ return (
262
+ df.filter(pl.col("method") != "no_reserve")
263
+ .select("reserve", "upDown", "group")
264
+ .unique(maintain_order=True)
265
+ )
266
+
267
+
268
+ def derive_group_loss_share(input_dir: Path,
269
+ *, provider: "object | None" = None,
270
+ ) -> pl.DataFrame:
271
+ df = _read_csv(input_dir / "group__loss_share_type.csv",
272
+ ["group", "type"], provider=provider)
273
+ return (
274
+ df.filter(pl.col("group") != "")
275
+ .select("group")
276
+ .unique(maintain_order=True)
277
+ )
278
+
279
+
280
+ def derive_def_optional_yes(input_dir: Path,
281
+ *, provider: "object | None" = None,
282
+ ) -> pl.DataFrame:
283
+ """def_optional_outputs filtered to 'yes' and not overridden 'no'."""
284
+ explicit = _read_csv(input_dir / "optional_outputs.csv",
285
+ ["output", "value"], provider=provider)
286
+ explicit_no = explicit.filter(pl.col("value") == "no").select("output")
287
+ defaults = _read_csv(input_dir / "def_optional_outputs.csv",
288
+ ["output", "value"], provider=provider)
289
+ return (
290
+ defaults.filter(pl.col("value") == "yes")
291
+ .join(explicit_no, on="output", how="anti")
292
+ .select("output")
293
+ .unique(maintain_order=True)
294
+ )
295
+
296
+
297
+ def derive_process_delayed(solve_data_dir: Path,
298
+ *, provider: "object | None" = None,
299
+ ) -> pl.DataFrame:
300
+ """Project ``process`` out of solve_data/process_delayed__duration.csv."""
301
+ df = _read_csv(
302
+ solve_data_dir / "process_delayed__duration.csv",
303
+ ["process", "duration"],
304
+ provider=provider,
305
+ )
306
+ return (
307
+ df.filter(pl.col("process") != "")
308
+ .select("process")
309
+ .unique(maintain_order=True)
310
+ )
311
+
312
+
313
+ def derive_period_solve(solve_data_dir: Path,
314
+ *, provider: "object | None" = None,
315
+ ) -> pl.DataFrame:
316
+ """Project ``period`` out of solve_data/solve_period.csv."""
317
+ df = _read_csv(solve_data_dir / "solve_period.csv",
318
+ ["solve", "period"], provider=provider)
319
+ return (
320
+ df.filter(pl.col("period") != "")
321
+ .select("period")
322
+ .unique(maintain_order=True)
323
+ )
324
+
325
+
326
+ def derive_time_set(input_dir: Path,
327
+ *, provider: "object | None" = None,
328
+ ) -> pl.DataFrame:
329
+ """Project ``time`` out of input/timeline.csv (cols: timeline, step, ...)."""
330
+ df = _read_csv(input_dir / "timeline.csv",
331
+ ["timeline", "step"], provider=provider)
332
+ return (
333
+ df.filter(pl.col("step") != "")
334
+ .select(pl.col("step").alias("time"))
335
+ .unique(maintain_order=True)
336
+ )
337
+
338
+
339
+ def derive_enable_optional_outputs(solve_data_dir: Path,
340
+ *, provider: "object | None" = None,
341
+ ) -> pl.DataFrame:
342
+ """Union of optional_yes and def_optional_yes (order: optional first)."""
343
+ a = _read_csv(solve_data_dir / "optional_yes.csv", ["output"],
344
+ provider=provider)
345
+ b = _read_csv(solve_data_dir / "def_optional_yes.csv", ["output"],
346
+ provider=provider)
347
+ return (
348
+ pl.concat([a, b], how="vertical")
349
+ .filter(pl.col("output") != "")
350
+ .select("output")
351
+ .unique(maintain_order=True)
352
+ )
353
+
354
+
355
+ def derive_node_state_subset(
356
+ solve_data_dir: Path, binding_method: "str | tuple[str, ...]",
357
+ *, provider: "object | None" = None,
358
+ ) -> pl.DataFrame:
359
+ """Filter nodeState by one or more storage_binding_method values.
360
+
361
+ ``binding_method`` is either a single string or a tuple of strings.
362
+ For ``nodeState_rp`` (Phase D onwards) the caller passes the tuple
363
+ ``("bind_within_solve_blended_weights",
364
+ "bind_forward_only_blended_weights")`` so the resulting set is
365
+ the UNION of every node carrying either RP-flavoured method — the
366
+ SHARED RP machinery in ``model.py`` (``rp_inter_period_balance``,
367
+ ``rp_inter_period_max_state``, ``maxState_rp_start``, and the
368
+ intra-period state-change branches in ``nodeBalance_eq``) fires
369
+ over this union; only ``rp_inter_period_cyclic`` further filters
370
+ back to the within_solve subset. For ``nodeStateBlock`` the caller
371
+ still passes the single string ``"bind_intraperiod_blocks"``.
372
+ """
373
+ state = _read_csv(solve_data_dir / "nodeState.csv", ["node"],
374
+ provider=provider)
375
+ binding = _read_csv(
376
+ solve_data_dir / "node__storage_binding_method.csv",
377
+ ["node", "method"], provider=provider,
378
+ )
379
+ if isinstance(binding_method, str):
380
+ methods = (binding_method,)
381
+ else:
382
+ methods = tuple(binding_method)
383
+ matching = (binding
384
+ .filter(pl.col("method").is_in(list(methods)))
385
+ .select("node"))
386
+ return (
387
+ state.join(matching, on="node", how="inner")
388
+ .select("node")
389
+ .unique(maintain_order=True)
390
+ )
391
+
392
+
393
+ # --- simple_setof_projections: 4 trivial single-column projections ---------
394
+
395
+ def derive_solve_period(input_dir: Path,
396
+ *, provider: "object | None" = None,
397
+ ) -> pl.DataFrame:
398
+ """(solve, period) projected from input/timesets_in_use.csv."""
399
+ df = _read_csv(
400
+ input_dir / "timesets_in_use.csv",
401
+ ["solve", "period", "tb"],
402
+ provider=provider,
403
+ )
404
+ return (
405
+ df.filter((pl.col("solve") != "") & (pl.col("period") != ""))
406
+ .select("solve", "period")
407
+ .unique(maintain_order=True)
408
+ )
409
+
410
+
411
+ def derive_timeline(input_dir: Path,
412
+ *, provider: "object | None" = None,
413
+ ) -> pl.DataFrame:
414
+ """``timeline`` projected from input/timesets__timeline.csv (col 1)."""
415
+ df = _read_csv(
416
+ input_dir / "timesets__timeline.csv", ["tb", "timeline"],
417
+ provider=provider,
418
+ )
419
+ return (
420
+ df.filter(pl.col("timeline") != "")
421
+ .select("timeline")
422
+ .unique(maintain_order=True)
423
+ )
424
+
425
+
426
+ def derive_commodity_tier_ann(input_dir: Path,
427
+ *, provider: "object | None" = None,
428
+ ) -> pl.DataFrame:
429
+ """(commodity, tier) projected from input/commodity_ladder_annual.csv.
430
+
431
+ Header order: commodity, period, tier, price, quantity — tier is col 2.
432
+ """
433
+ df = _read_csv(
434
+ input_dir / "commodity_ladder_annual.csv",
435
+ ["commodity", "period", "tier", "price", "quantity"],
436
+ provider=provider,
437
+ )
438
+ return (
439
+ df.filter((pl.col("commodity") != "") & (pl.col("tier") != ""))
440
+ .select("commodity", "tier")
441
+ .unique(maintain_order=True)
442
+ )
443
+
444
+
445
+ # --- orchestrators for simple_projections (preserves legacy call order) ----
446
+
447
+ # --- emit_* twins for the simple_projections orchestrators ---------------
448
+
449
+ def emit_optional_yes(input_dir: Path, solve_data_dir: Path,
450
+ *, provider) -> None:
451
+ """Emit ``optional_yes`` to the Provider."""
452
+ del solve_data_dir
453
+ _emit(provider, "solve_data/optional_yes.csv",
454
+ derive_optional_yes(input_dir, provider=provider))
455
+
456
+
457
+ def emit_reserve_upDown_group(input_dir: Path, solve_data_dir: Path,
458
+ *, provider) -> None:
459
+ """Emit ``reserve_upDown_group`` to the Provider."""
460
+ del solve_data_dir
461
+ _emit(provider, "solve_data/reserve__upDown__group.csv",
462
+ derive_reserve_upDown_group(input_dir, provider=provider))
463
+
464
+
465
+ def emit_group_loss_share(input_dir: Path, solve_data_dir: Path,
466
+ *, provider) -> None:
467
+ """Emit ``group_loss_share`` to the Provider."""
468
+ del solve_data_dir
469
+ _emit(provider, "solve_data/group_loss_share.csv",
470
+ derive_group_loss_share(input_dir, provider=provider))
471
+
472
+
473
+ def emit_def_optional_yes(input_dir: Path, solve_data_dir: Path,
474
+ *, provider) -> None:
475
+ """Emit ``def_optional_yes`` to the Provider."""
476
+ del solve_data_dir
477
+ _emit(provider, "solve_data/def_optional_yes.csv",
478
+ derive_def_optional_yes(input_dir, provider=provider))
479
+
480
+
481
+ def emit_process_delayed(input_dir: Path, solve_data_dir: Path,
482
+ *, provider) -> None:
483
+ """Emit ``process_delayed`` to the Provider."""
484
+ del input_dir
485
+ _emit(provider, "solve_data/process_delayed.csv",
486
+ derive_process_delayed(solve_data_dir, provider=provider))
487
+
488
+
489
+ def emit_period_solve(solve_data_dir: Path, *, provider) -> None:
490
+ """Emit ``period_solve`` to the Provider."""
491
+ _emit(provider, "solve_data/period_solve.csv",
492
+ derive_period_solve(solve_data_dir, provider=provider))
493
+
494
+
495
+ def emit_time_set(input_dir: Path, solve_data_dir: Path,
496
+ *, provider) -> None:
497
+ """Emit ``time_set`` to the Provider."""
498
+ del solve_data_dir
499
+ _emit(provider, "solve_data/time.csv",
500
+ derive_time_set(input_dir, provider=provider))
501
+
502
+
503
+ def emit_enable_optional_outputs(solve_data_dir: Path,
504
+ *, provider) -> None:
505
+ """Emit ``enable_optional_outputs`` to the Provider."""
506
+ _emit(provider, "solve_data/enable_optional_outputs.csv",
507
+ derive_enable_optional_outputs(solve_data_dir, provider=provider))
508
+
509
+
510
+ def emit_node_state_subsets(solve_data_dir: Path,
511
+ *, provider) -> None:
512
+ """Emit ``node_state_subsets`` to the Provider.
513
+
514
+ Phase E: ``nodeState_rp`` is the UNION of every node carrying any
515
+ of the three blended-weights methods —
516
+ ``bind_within_solve_blended_weights`` (across-solve cyclic
517
+ closure), ``bind_within_period_blended_weights`` (per-FlexTool-
518
+ period cyclic closure), and ``bind_forward_only_blended_weights``
519
+ (no closure). The shared RP machinery in ``model.py``
520
+ (intra-period state-change branches in ``nodeBalance_eq``,
521
+ ``rp_inter_period_balance``, ``rp_inter_period_max_state``,
522
+ ``maxState_rp_start``) fires over this union; only the
523
+ ``rp_inter_period_cyclic`` constraint further filters to the
524
+ within_solve ∪ within_period subset, with per-period pairing on
525
+ the optional ``d`` column carried on ``rp_base_first`` /
526
+ ``rp_base_last`` (see ``model.py``'s cyclic-emit block).
527
+ """
528
+ rp = derive_node_state_subset(
529
+ solve_data_dir,
530
+ ("bind_within_solve_blended_weights",
531
+ "bind_within_period_blended_weights",
532
+ "bind_forward_only_blended_weights"),
533
+ provider=provider,
534
+ )
535
+ block = derive_node_state_subset(solve_data_dir, "bind_intraperiod_blocks",
536
+ provider=provider)
537
+ _emit(provider, K.SOLVE_DATA_NODE_STATE_RP, rp)
538
+ _emit(provider, "solve_data/nodeStateBlock.csv", block)
539
+
540
+
541
+ def emit_simple_setof_projections(input_dir: Path, solve_data_dir: Path,
542
+ *, provider) -> None:
543
+ """Emit ``simple_setof_projections`` to the Provider."""
544
+ del solve_data_dir
545
+ _emit(provider, "solve_data/solve_period.csv",
546
+ derive_solve_period(input_dir, provider=provider))
547
+ _emit(provider, "solve_data/timeline.csv",
548
+ derive_timeline(input_dir, provider=provider))
549
+ _emit(provider, "solve_data/commodity__tier_ann.csv",
550
+ derive_commodity_tier_ann(input_dir, provider=provider))