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,504 @@
1
+ """Per-period calculated params.
2
+
3
+ Called per-solve via ``write_period_calculated_params`` and
4
+ ``write_branch_weights``.
5
+
6
+ Output CSVs:
7
+
8
+ * ``write_period_calculated_params`` (11 CSVs):
9
+
10
+ - ``p_timeline_duration_in_years.csv`` — sum_t dur / 8760
11
+ - ``hours_in_period.csv`` — sum_t step_duration
12
+ - ``period_share_of_year.csv`` — hours / 8760
13
+ - ``p_years_d.csv`` — period_with_history scan
14
+ - ``complete_hours_in_period.csv`` — branch-summed complete hours
15
+ - ``complete_period_share_of_year_calc.csv`` — complete hours / 8760
16
+ - ``p_years_until_invest.csv`` — cumulative years + invest offset
17
+ - ``p_years_until_dispatch.csv`` — cumulative years + ops offset
18
+ - ``p_inflation_factor_investment_yearly.csv``— inflation discount factor
19
+ - ``p_inflation_factor_operations_yearly.csv``— inflation discount factor
20
+ - ``f_d_k.csv`` — ladder fraction
21
+
22
+ * ``write_branch_weights`` (2 CSVs):
23
+
24
+ - ``pd_branch_weight.csv`` — per-(period) sibling-normalised weight
25
+ - ``pdt_branch_weight.csv`` — per-(period, time) sibling-normalised weight
26
+
27
+ Reuse note (Phase 2 sub-dispatch 3 brief):
28
+
29
+ ``flextool.engine_polars._per_solve_sets.PerSolveAggregates`` already
30
+ derives ``complete_period_share_of_year`` natively from
31
+ :class:`InputSource` (cluster-D / NPV path) — but that helper is
32
+ upstream-bound: it works on solve-config objects rather than the
33
+ per-solve ``solve_data/*.csv`` files that this writer reads. Sharing
34
+ its lazy-LF pipeline would force a second data path through this
35
+ writer; instead we mirror the legacy CSV-reading shape (which is the
36
+ authoritative byte-for-byte parity surface) and keep the formula
37
+ expression aligned with ``PerSolveAggregates``.
38
+
39
+ Float values formatted with ``repr(float(v))`` for byte-identical
40
+ parity with the legacy emitter.
41
+ """
42
+ from __future__ import annotations
43
+
44
+ from pathlib import Path
45
+
46
+ import polars as pl
47
+
48
+ from flextool.engine_polars._emit_provider_io import _emit
49
+
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # CSV I/O helpers — same conventions as ``_emit_per_solve`` /
53
+ # ``_emit_lp_scaling``.
54
+ # ---------------------------------------------------------------------------
55
+
56
+
57
+ def _read_csv(path: Path, columns: list[str],
58
+ *, provider: "object | None" = None) -> pl.DataFrame:
59
+ """Eager-read a tiny flextool CSV via the Provider.
60
+
61
+ Returns the Provider's frame sliced to *columns* with positional
62
+ rename; returns an empty all-``Utf8`` frame when the Provider
63
+ misses the key. Step 2.5 Phase C dropped the disk-fallback arm —
64
+ cascade modules read the Provider only.
65
+ """
66
+ from flextool.engine_polars._emit_provider_io import (
67
+ _provider_key,
68
+ _provider_lookup_positional,
69
+ )
70
+ seeded = _provider_lookup_positional(
71
+ provider, _provider_key(path), path, columns,
72
+ )
73
+ if seeded is not None:
74
+ return seeded
75
+ return pl.DataFrame(
76
+ {c: [] for c in columns},
77
+ schema={c: pl.Utf8 for c in columns},
78
+ )
79
+
80
+
81
+ def _read_singles(path: Path,
82
+ *, provider: "object | None" = None) -> list[str]:
83
+ """List the first column of a header+rows CSV, dropping blanks."""
84
+ df = _read_csv(path, ["v"], provider=provider)
85
+ return [v for v in df["v"].to_list() if v]
86
+
87
+
88
+ def _to_utf8_frame(
89
+ headers: tuple[str, ...],
90
+ rows: list[tuple],
91
+ ) -> pl.DataFrame:
92
+ """Build an all-``Utf8`` polars frame from a header tuple + row list.
93
+
94
+ Each cell is taken verbatim when already a string, otherwise via
95
+ ``str(v)``. Float cells are pre-rendered with ``repr(float(v))``
96
+ by the ``derive_*`` builders so polars' ``write_csv`` does not
97
+ re-format numbers (preserves byte parity with the legacy emitter).
98
+ """
99
+ cols: dict[str, list[str]] = {h: [] for h in headers}
100
+ for row in rows:
101
+ for h, v in zip(headers, row):
102
+ cols[h].append(v if isinstance(v, str) else str(v))
103
+ return pl.DataFrame(cols, schema={h: pl.Utf8 for h in headers})
104
+
105
+
106
+ def _keyed_frame(header: tuple[str, str],
107
+ rows: list[tuple[str, float]]) -> pl.DataFrame:
108
+ """Build a 2-col Utf8 frame with ``repr(v)`` on the value cell."""
109
+ return _to_utf8_frame(
110
+ header, [(k, repr(v)) for k, v in rows],
111
+ )
112
+
113
+
114
+ def _keyed_frame_2(header: tuple[str, str, str],
115
+ rows: list[tuple[str, str, float]]) -> pl.DataFrame:
116
+ """Build a 3-col Utf8 frame with ``repr(v)`` on the value cell."""
117
+ return _to_utf8_frame(
118
+ header, [(a, b, repr(v)) for a, b, v in rows],
119
+ )
120
+
121
+
122
+ def _emit_keyed(provider, key: str, header: tuple[str, str],
123
+ rows: list[tuple[str, float]]) -> None:
124
+ _emit(provider, key, _keyed_frame(header, rows))
125
+
126
+
127
+ def _emit_keyed_2(provider, key: str, header: tuple[str, str, str],
128
+ rows: list[tuple[str, str, float]]) -> None:
129
+ _emit(provider, key, _keyed_frame_2(header, rows))
130
+
131
+
132
+ def _read_step_duration(
133
+ path: Path,
134
+ *, provider: "object | None" = None,
135
+ ) -> tuple[dict[tuple[str, str], float], dict[str, None]]:
136
+ """Read (period, step, duration) CSV → (dict, ordered-set of periods)."""
137
+ df = _read_csv(path, ["period", "step", "duration"], provider=provider)
138
+ out: dict[tuple[str, str], float] = {}
139
+ periods: dict[str, None] = {}
140
+ for d, t, v in zip(df["period"].to_list(),
141
+ df["step"].to_list(),
142
+ df["duration"].to_list()):
143
+ if not d or not t:
144
+ continue
145
+ try:
146
+ out[(d, t)] = float(v)
147
+ periods.setdefault(d, None)
148
+ except (ValueError, TypeError):
149
+ continue
150
+ return out, periods
151
+
152
+
153
+ def _read_pb_pairs(path: Path,
154
+ *, provider: "object | None" = None) -> list[tuple[str, str]]:
155
+ """Read period__branch.csv → list of (period, branch) tuples."""
156
+ df = _read_csv(path, ["period", "branch"], provider=provider)
157
+ return [(d, b) for d, b in zip(df["period"].to_list(),
158
+ df["branch"].to_list())
159
+ if d and b]
160
+
161
+
162
+ # ---------------------------------------------------------------------------
163
+ # Family A — write_period_calculated_params
164
+ # ---------------------------------------------------------------------------
165
+
166
+
167
+ def emit_period_calculated_params(
168
+ input_dir: Path, solve_data_dir: Path,
169
+ *, provider,
170
+ ) -> None:
171
+ """Emit ``period_calculated_params`` to the Provider.
172
+ Emits the same 12 frames under ``solve_data/<basename>`` keys via
173
+ :func:`_emit` (dual-key registration).
174
+ """
175
+ # ── Sources ────────────────────────────────────────────────────────
176
+ step_duration, _period_set = _read_step_duration(
177
+ solve_data_dir / "steps_in_use.csv", provider=provider,
178
+ )
179
+ complete_step_duration, _ = _read_step_duration(
180
+ solve_data_dir / "steps_complete_solve.csv", provider=provider,
181
+ )
182
+
183
+ pb_rows = _read_pb_pairs(solve_data_dir / "period__branch.csv",
184
+ provider=provider)
185
+ branches_for_d: dict[str, list[str]] = {}
186
+ for d, b in pb_rows:
187
+ branches_for_d.setdefault(d, []).append(b)
188
+
189
+ timeline_step_duration, timelines = _read_step_duration(
190
+ input_dir / "timeline.csv", provider=provider,
191
+ )
192
+
193
+ pwh_df = _read_csv(solve_data_dir / "period_with_history.csv",
194
+ ["period", "value"], provider=provider)
195
+ p_period_from_solve: dict[str, float] = {}
196
+ period_with_history: list[str] = []
197
+ for d, v in zip(pwh_df["period"].to_list(),
198
+ pwh_df["value"].to_list()):
199
+ if not d:
200
+ continue
201
+ period_with_history.append(d)
202
+ try:
203
+ p_period_from_solve[d] = float(v)
204
+ except (ValueError, TypeError):
205
+ continue
206
+
207
+ pyr_df = _read_csv(
208
+ solve_data_dir / "p_years_represented.csv",
209
+ ["period", "year", "p_years_from_solve", "value"],
210
+ provider=provider,
211
+ )
212
+ p_years_represented: dict[tuple[str, str], float] = {}
213
+ years_for_period: dict[str, list[str]] = {}
214
+ for d, y, v in zip(pyr_df["period"].to_list(),
215
+ pyr_df["year"].to_list(),
216
+ pyr_df["value"].to_list()):
217
+ if not d or not y:
218
+ continue
219
+ try:
220
+ p_years_represented[(d, y)] = float(v)
221
+ years_for_period.setdefault(d, []).append(y)
222
+ except (ValueError, TypeError):
223
+ continue
224
+
225
+ period_in_use = _read_singles(solve_data_dir / "period_in_use_set.csv",
226
+ provider=provider)
227
+ periodAll = _read_singles(solve_data_dir / "periodAll_set.csv",
228
+ provider=provider)
229
+
230
+ sum_step_dur_by_period: dict[str, float] = {}
231
+ for (d, _t), v in step_duration.items():
232
+ sum_step_dur_by_period[d] = sum_step_dur_by_period.get(d, 0.0) + v
233
+
234
+ sum_timeline_dur: dict[str, float] = {}
235
+ for (tl, _t), v in timeline_step_duration.items():
236
+ sum_timeline_dur[tl] = sum_timeline_dur.get(tl, 0.0) + v
237
+
238
+ pb_pairs_set = frozenset(pb_rows)
239
+
240
+ sum_complete_by_d2: dict[str, float] = {}
241
+ for (d2, _t), v in complete_step_duration.items():
242
+ sum_complete_by_d2[d2] = sum_complete_by_d2.get(d2, 0.0) + v
243
+
244
+ p_tdy = [(tl, sum_timeline_dur.get(tl, 0.0) / 8760.0) for tl in timelines]
245
+ _emit_keyed(provider, "solve_data/p_timeline_duration_in_years.csv",
246
+ ("timeline", "value"), p_tdy)
247
+
248
+ hours_in_period = [
249
+ (d, sum_step_dur_by_period.get(d, 0.0)) for d in period_in_use
250
+ ]
251
+ _emit_keyed(provider, "solve_data/hours_in_period.csv",
252
+ ("period", "value"), hours_in_period)
253
+
254
+ period_share = [(d, h / 8760.0) for d, h in hours_in_period]
255
+ _emit_keyed(provider, "solve_data/period_share_of_year.csv",
256
+ ("period", "value"), period_share)
257
+
258
+ p_years_d_rows = [
259
+ (d, p_period_from_solve.get(d, 0.0)) for d in period_with_history
260
+ ]
261
+ _emit_keyed(provider, "solve_data/p_years_d.csv",
262
+ ("period", "value"), p_years_d_rows)
263
+
264
+ branches_for_period: dict[str, list[str]] = {}
265
+ for d2, d in pb_pairs_set:
266
+ branches_for_period.setdefault(d, []).append(d2)
267
+ complete_hours: list[tuple[str, float]] = []
268
+ for d in period_in_use:
269
+ total = sum(
270
+ sum_complete_by_d2.get(d2, 0.0)
271
+ for d2 in branches_for_period.get(d, ())
272
+ )
273
+ complete_hours.append((d, total))
274
+ _emit_keyed(provider, "solve_data/complete_hours_in_period.csv",
275
+ ("period", "value"), complete_hours)
276
+
277
+ complete_share = [(d, h / 8760.0) for d, h in complete_hours]
278
+ _emit_keyed(provider, "solve_data/complete_period_share_of_year_calc.csv",
279
+ ("period", "value"), complete_share)
280
+
281
+ def _scalar_max(csv_path: Path, default: float) -> float:
282
+ rows_df = _read_csv(csv_path, ["key", "value"], provider=provider)
283
+ vals: list[float] = []
284
+ for v in rows_df["value"].to_list():
285
+ if v is None or v == "":
286
+ continue
287
+ try:
288
+ vals.append(float(v))
289
+ except ValueError:
290
+ continue
291
+ return max(vals) if vals else default
292
+
293
+ p_inflation = _scalar_max(input_dir / "p_inflation_rate.csv", 0.0)
294
+ p_infl_offset_investment = _scalar_max(
295
+ input_dir / "p_inflation_offset_investment.csv", 0.0,
296
+ )
297
+ p_infl_offset_operations = _scalar_max(
298
+ input_dir / "p_inflation_offset_operations.csv", 0.5,
299
+ )
300
+
301
+ global_years_set: dict[str, None] = {}
302
+ for years_list in years_for_period.values():
303
+ for y in years_list:
304
+ global_years_set.setdefault(y, None)
305
+ try:
306
+ sorted_global_years = sorted(
307
+ global_years_set.keys(), key=lambda y: float(y),
308
+ )
309
+ except ValueError:
310
+ sorted_global_years = sorted(global_years_set.keys())
311
+
312
+ pyy_invest: list[tuple[str, str, float]] = []
313
+ pyy_dispatch: list[tuple[str, str, float]] = []
314
+ inflation_invest: dict[str, float] = {}
315
+ inflation_ops: dict[str, float] = {}
316
+ one_plus_inflation_inv = (
317
+ 1.0 / (1.0 + p_inflation) if p_inflation != -1.0 else 1.0
318
+ )
319
+
320
+ for d in periodAll:
321
+ years_for_d = years_for_period.get(d, ())
322
+ try:
323
+ sorted_d_years = sorted(years_for_d, key=lambda y: float(y))
324
+ except ValueError:
325
+ sorted_d_years = sorted(years_for_d)
326
+
327
+ cumulative = 0.0
328
+ global_pos: dict[str, float] = {}
329
+ for y2 in sorted_global_years:
330
+ global_pos[y2] = cumulative
331
+ cumulative += p_years_represented.get((d, y2), 1.0)
332
+
333
+ per_year: list[tuple[str, float, float]] = []
334
+ for y in sorted_d_years:
335
+ pyr = p_years_represented.get((d, y), 1.0)
336
+ base = global_pos.get(y, 0.0)
337
+ until_invest = base + pyr * p_infl_offset_investment
338
+ until_dispatch = base + pyr * p_infl_offset_operations
339
+ pyy_invest.append((d, y, until_invest))
340
+ pyy_dispatch.append((d, y, until_dispatch))
341
+ per_year.append((y, until_invest, until_dispatch))
342
+
343
+ sum_p_years_for_d = sum(
344
+ p_years_represented.get((d, y), 1.0) for y in sorted_d_years
345
+ )
346
+ if sum_p_years_for_d > 0:
347
+ inv_factor = 0.0
348
+ ops_factor = 0.0
349
+ for y, until_inv, until_op in per_year:
350
+ pyr = p_years_represented.get((d, y), 1.0)
351
+ inv_factor += pyr * (one_plus_inflation_inv ** until_inv)
352
+ ops_factor += pyr * (one_plus_inflation_inv ** until_op)
353
+ inflation_invest[d] = inv_factor
354
+ inflation_ops[d] = ops_factor
355
+ else:
356
+ inflation_invest[d] = 1.0
357
+ inflation_ops[d] = 1.0
358
+
359
+ _emit_keyed_2(provider, "solve_data/p_years_until_invest.csv",
360
+ ("period", "year", "value"), pyy_invest)
361
+ _emit_keyed_2(provider, "solve_data/p_years_until_dispatch.csv",
362
+ ("period", "year", "value"), pyy_dispatch)
363
+
364
+ period_universe = _read_singles(solve_data_dir / "period_set.csv",
365
+ provider=provider)
366
+ inv_yearly = [
367
+ (d, inflation_invest.get(d, 1.0)) for d in period_universe
368
+ ]
369
+ ops_yearly = [
370
+ (d, inflation_ops.get(d, 1.0)) for d in period_in_use
371
+ ]
372
+ _emit_keyed(provider, "solve_data/p_inflation_factor_investment_yearly.csv",
373
+ ("period", "value"), inv_yearly)
374
+ _emit_keyed(provider, "solve_data/p_inflation_factor_operations_yearly.csv",
375
+ ("period", "value"), ops_yearly)
376
+
377
+ # Phase 4.1a / 4.2-1g — read the cross-roll sim-hour accumulator from
378
+ # the handoff translator's ``handoff/cum_sim_hours`` Provider key
379
+ # (canonical schema ``[period, p_ladder_cum_sim_hours]``). Empty /
380
+ # missing handoff (first solve) collapses ``p_ladder_cum`` to {}
381
+ # so ``f_d_k`` reduces to ``sum_step_dur / (share * 8760)``. The
382
+ # legacy ``solve_data/ladder_cum_sim_hours`` CSV fallback was dropped
383
+ # in 4.2-1g — the translator pipeline always populates the handoff
384
+ # key now, so the fallback was dead in production.
385
+ from flextool.engine_polars import _provider_keys as K
386
+ from flextool.engine_polars._provider_translators import read_handoff_frame
387
+ p_ladder_cum: dict[str, float] = {}
388
+ handoff_df = read_handoff_frame(provider, K.HANDOFF_CUM_SIM_HOURS)
389
+ if handoff_df is not None and "p_ladder_cum_sim_hours" in handoff_df.columns:
390
+ for d, v in zip(handoff_df["period"].to_list(),
391
+ handoff_df["p_ladder_cum_sim_hours"].to_list()):
392
+ if not d:
393
+ continue
394
+ try:
395
+ p_ladder_cum[d] = float(v)
396
+ except (ValueError, TypeError):
397
+ continue
398
+ complete_share_lookup = dict(complete_share)
399
+ f_d_k_rows: list[tuple[str, float]] = []
400
+ for d in period_in_use:
401
+ num = p_ladder_cum.get(d, 0.0) + sum_step_dur_by_period.get(d, 0.0)
402
+ denom = complete_share_lookup.get(d, 0.0) * 8760.0
403
+ f_d_k_rows.append((d, num / denom))
404
+ _emit_keyed(provider, "solve_data/f_d_k.csv",
405
+ ("period", "value"), f_d_k_rows)
406
+
407
+
408
+ # ---------------------------------------------------------------------------
409
+ # Family B — write_branch_weights
410
+ # ---------------------------------------------------------------------------
411
+
412
+
413
+ def emit_branch_weights(input_dir: Path, solve_data_dir: Path,
414
+ *, provider) -> None:
415
+ """Emit ``branch_weights`` to the Provider.
416
+ Emits ``pd_branch_weight.csv`` and ``pdt_branch_weight.csv`` under
417
+ ``solve_data/<basename>`` keys via :func:`_emit`.
418
+ """
419
+ del input_dir # legacy signature parity; no input/ reads here.
420
+
421
+ pb_rows = _read_pb_pairs(solve_data_dir / "period__branch.csv",
422
+ provider=provider)
423
+ pb_set = frozenset(pb_rows)
424
+
425
+ sbw_df = _read_csv(
426
+ solve_data_dir / "solve_branch_weight.csv",
427
+ ["branch", "value"],
428
+ provider=provider,
429
+ )
430
+ branch_weight: dict[str, float] = {}
431
+ for b, v in zip(sbw_df["branch"].to_list(), sbw_df["value"].to_list()):
432
+ if not b:
433
+ continue
434
+ try:
435
+ branch_weight[b] = float(v)
436
+ except (ValueError, TypeError):
437
+ continue
438
+
439
+ def w(b: str) -> float:
440
+ return branch_weight.get(b, 1.0)
441
+
442
+ ft_df = _read_csv(
443
+ solve_data_dir / "first_timesteps.csv",
444
+ ["period", "step"],
445
+ provider=provider,
446
+ )
447
+ times_with_first_set: dict[str, set[str]] = {}
448
+ first_time_for_d: dict[str, str] = {}
449
+ for d, t in zip(ft_df["period"].to_list(), ft_df["step"].to_list()):
450
+ if not d or not t:
451
+ continue
452
+ first_time_for_d[d] = t
453
+ times_with_first_set.setdefault(t, set()).add(d)
454
+
455
+ su_df = _read_csv(
456
+ solve_data_dir / "steps_in_use.csv",
457
+ ["period", "step"],
458
+ provider=provider,
459
+ )
460
+ dt_pairs: list[tuple[str, str]] = []
461
+ branches_for_t: dict[str, set[str]] = {}
462
+ for d, t in zip(su_df["period"].to_list(), su_df["step"].to_list()):
463
+ if not d or not t:
464
+ continue
465
+ dt_pairs.append((d, t))
466
+ branches_for_t.setdefault(t, set()).add(d)
467
+
468
+ period_in_use = _read_singles(solve_data_dir / "period_in_use_set.csv",
469
+ provider=provider)
470
+
471
+ pd_rows: list[tuple[str, float]] = []
472
+ for d in period_in_use:
473
+ ts = first_time_for_d.get(d)
474
+ if ts is None:
475
+ continue
476
+ branches_at_ts = times_with_first_set.get(ts, set())
477
+ denom = 0.0
478
+ for d2, b in pb_rows:
479
+ if b not in branches_at_ts:
480
+ continue
481
+ if (d2, d) not in pb_set:
482
+ continue
483
+ denom += w(b)
484
+ if denom == 0.0:
485
+ continue
486
+ pd_rows.append((d, w(d) / denom))
487
+ _emit_keyed(provider, "solve_data/pd_branch_weight.csv",
488
+ ("period", "value"), pd_rows)
489
+
490
+ pdt_rows: list[tuple[str, str, float]] = []
491
+ for d, t in dt_pairs:
492
+ branches_with_t = branches_for_t.get(t, set())
493
+ denom = 0.0
494
+ for d2, b in pb_rows:
495
+ if b not in branches_with_t:
496
+ continue
497
+ if (d2, d) not in pb_set:
498
+ continue
499
+ denom += w(b)
500
+ if denom == 0.0:
501
+ continue
502
+ pdt_rows.append((d, t, w(d) / denom))
503
+ _emit_keyed_2(provider, "solve_data/pdt_branch_weight.csv",
504
+ ("period", "time", "value"), pdt_rows)