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,442 @@
1
+ """Delayed-process feature (canonical use case: hydro / river chains).
2
+
3
+ This module covers the .mod's ``process_delayed`` family — processes
4
+ whose sink-side delivery at time ``t`` is fed by source-side flows at
5
+ *earlier* times ``t_``, weighted by a per-process delay profile. The
6
+ canonical use case is a **hydro / river network**: water released
7
+ upstream takes a known time to arrive downstream, so the
8
+ ``conversion_indirect`` balance for the downstream unit must aggregate
9
+ upstream inflows over a delay window rather than read instantaneous
10
+ flow. Other plausible uses include thermal-storage charging chains
11
+ and freight-style logistics. ``water_pump`` / ``water_pump_delayed``
12
+ are the test fixtures that currently exercise this.
13
+
14
+ **Note on demand response.** An earlier draft of this module also
15
+ handled DR (``dr_decrease_demand`` / ``dr_increase_demand`` /
16
+ ``dr_shift_demand``). DR was promoted out: those scenarios do not use
17
+ the delay tables — DR is just a regular storage + process pattern with
18
+ ``bind_within_solve`` storage and a sign-bearing inflow at the demand
19
+ node, already handled by the storage and process blocks in
20
+ ``flextool.model.build_flextool``. The DR fixtures' delay CSVs are
21
+ empty, so this module's ``has_feature`` returns False on them.
22
+
23
+ ================================================================
24
+ What this module implements
25
+ ================================================================
26
+
27
+ The .mod's ``conversion_indirect`` constraint (flextool.mod:2343)
28
+ splits the source-side flow term into an *undelayed* part (current-
29
+ time) and a *delayed* part (time-shifted via ``dtt__delay_duration``,
30
+ weighted by ``p_process_delay_weight``)::
31
+
32
+ sum {source : (p, source) in process_source_undelayed}
33
+ + v_flow[p, source, p, d, t] * unitsize * source_coef
34
+ + sum {source : (p, source) in process_source_delayed}
35
+ + sum {(d, t_, t, td) in dtt__delay_duration
36
+ : (p, td) in process_delayed__duration}
37
+ + v_flow[p, source, p, d, t_]
38
+ * unitsize * source_coef * delay_weight[p, td]
39
+
40
+ That is, for delayed (p, source), the inflow to the conversion
41
+ balance at sink-side time ``t`` is the *weighted sum* of source-side
42
+ flows at times ``t_`` paired with ``t`` through ``dtt__delay_duration``.
43
+
44
+ The .mod also has a *commented-out* analogous shift in
45
+ ``nodeBalance_eq`` (flextool.mod:2148-2154) — kept commented in
46
+ upstream .mod, so we do **not** mirror it here. If a future scenario
47
+ needs it, the same machinery applies to ``flow_from_nodeBalance_*``.
48
+
49
+ ================================================================
50
+ Module API
51
+ ================================================================
52
+
53
+ has_feature(d) -> bool
54
+ True iff ``d.process_delayed`` is non-empty.
55
+
56
+ load_data(inp_dir, sd_dir) -> dict[str, ...]
57
+ Reads the delay CSVs. Returns a dict of new FlexData fields:
58
+ process_delayed, process_source_delayed,
59
+ process_source_undelayed,
60
+ dtt__delay_duration, p_process_delay_weight.
61
+ All values are ``None`` (or empty) when the feature is inactive.
62
+
63
+ delayed_input_expr(d, v_flow) -> Expr | None
64
+ Returns the delay-shifted source-side LHS aggregate that the
65
+ ``conversion_indirect`` emission in ``model.py`` threads into
66
+ ``lhs_terms["input_delayed"]`` alongside the undelayed term.
67
+
68
+ add_constraints(m, d, vars) -> None
69
+ Currently a no-op (the delay term is woven into the existing
70
+ ``conversion_indirect`` via ``delayed_input_expr`` rather than
71
+ emitting a separate constraint).
72
+
73
+ add_objective_terms(m, d, vars, op_factor) -> None
74
+ No-op. Delayed processes do not add objective terms; their
75
+ costs propagate through commodity prices and slack penalties
76
+ already wired into ``build_flextool``.
77
+
78
+ ================================================================
79
+ Integration with ``conversion_indirect`` in ``model.py``
80
+ ================================================================
81
+
82
+ ``model.py``'s ``conversion_indirect`` builds the source-side input
83
+ term from ``d.process_input_flows``. When the delay feature is
84
+ active, that block anti-joins ``process_input_flows`` against
85
+ ``d.process_delayed`` (so the undelayed term skips delayed rows) and
86
+ adds the delay-shifted contribution by reading
87
+ ``delayed_input_expr(d, v_flow)`` from this module — woven into the
88
+ same ``add_cstr`` call as the named ``lhs_terms["input_delayed"]``
89
+ entry, so a single constraint per (p, d, t) is preserved.
90
+ """
91
+
92
+ from __future__ import annotations
93
+
94
+ from pathlib import Path
95
+ import polars as pl
96
+
97
+ from polar_high import Sum, Where, Param
98
+ # Engine imports kept light — we don't introduce new variable types.
99
+
100
+ from ._axis_enums import cast_dim
101
+ from ._emit_provider_io import _provider_key
102
+
103
+
104
+ def _provider_get(provider, path: "Path") -> "pl.DataFrame | None":
105
+ """Provider-only fetch. Returns ``None`` when the Provider is
106
+ missing or doesn't carry *path*'s canonical key.
107
+ """
108
+ if provider is None:
109
+ return None
110
+ key = _provider_key(path)
111
+ if not provider.has(key):
112
+ return None
113
+ return provider.get(key)
114
+
115
+
116
+ # ---------------------------------------------------------------------------
117
+ # Feature detection
118
+
119
+ def has_feature(d) -> bool:
120
+ """True iff ``d`` carries non-empty delayed-flow data."""
121
+ pd_set = getattr(d, "process_delayed", None)
122
+ return pd_set is not None and pd_set.height > 0
123
+
124
+
125
+ # ---------------------------------------------------------------------------
126
+ # Data loading
127
+
128
+ def load_data(inp_dir: str | Path, sd_dir: str | Path, *,
129
+ provider=None) -> dict:
130
+ """Read the delay-related solve_data CSVs.
131
+
132
+ Reads from ``solve_data/`` (where flextool.mod's preprocessing emits
133
+ the delay sets). ``inp_dir`` is accepted for API symmetry with the
134
+ other ``_load_*`` helpers in ``input.py`` but is not currently used —
135
+ the canonical sources are all in ``solve_data/`` (see flextool.mod
136
+ lines 525-527).
137
+
138
+ Post-Step-2.5 the loader consumes the live
139
+ :class:`FlexDataProvider`; the disk-fallback arms that previously
140
+ re-read the eight delay CSVs from disk are gone.
141
+
142
+ Returns a dict whose keys correspond to (proposed) ``FlexData`` field
143
+ names. When the feature is inactive (every scenario with no delayed
144
+ processes) every value is ``None``.
145
+
146
+ Proposed FlexData fields::
147
+
148
+ process_delayed pl.DataFrame | None # cols: (p,)
149
+ process_delayed__duration pl.DataFrame | None # cols: (p, td)
150
+ process_source_delayed pl.DataFrame | None # cols: (p, source)
151
+ process_source_undelayed pl.DataFrame | None # cols: (p, source)
152
+ process_source_sink_delayed pl.DataFrame | None # cols: (p, source, sink)
153
+ process_source_sink_undelayed pl.DataFrame | None # cols: (p, source, sink)
154
+ dtt__delay_duration pl.DataFrame | None # cols: (d, t_source, t_sink, td)
155
+ p_process_delay_weight Param | None # dims: (p, td)
156
+ """
157
+ sd = Path(sd_dir)
158
+
159
+ blank = dict(
160
+ process_delayed = None,
161
+ process_delayed__duration = None,
162
+ process_source_delayed = None,
163
+ process_source_undelayed = None,
164
+ process_source_sink_delayed = None,
165
+ process_source_sink_undelayed = None,
166
+ dtt__delay_duration = None,
167
+ p_process_delay_weight = None,
168
+ )
169
+
170
+ pd_path = sd / "process_delayed.csv"
171
+ pd_df = _provider_get(provider, pd_path)
172
+ if pd_df is None or pd_df.height == 0:
173
+ # Header-only / missing — flextool emits these even when no
174
+ # process is delayed (DR scenarios without the delay feature).
175
+ return blank
176
+
177
+ pd_df = pd_df.rename({"process": "p"}) if "process" in pd_df.columns else pd_df
178
+
179
+ # Δ.12-drop: ``process_delayed__duration`` (set frame, not Param)
180
+ # produced authoritatively by
181
+ # ``apply_direct_params.process_delayed__duration_from_source``.
182
+ # Seed dropped.
183
+ pdd_df = None
184
+
185
+ # process_source_(un)delayed: (process, source) frames
186
+ def _read_pse(name: str) -> pl.DataFrame | None:
187
+ p = sd / f"{name}.csv"
188
+ df = _provider_get(provider, p)
189
+ if df is None or df.height == 0:
190
+ return None
191
+ if "process" in df.columns:
192
+ df = df.rename({"process": "p"})
193
+ return df.select("p", "source")
194
+
195
+ pse_delayed = _read_pse("process_source_delayed")
196
+ pse_undelayed = _read_pse("process_source_undelayed")
197
+
198
+ # process_source_sink_(un)delayed: (process, source, sink) frames
199
+ def _read_psse(name: str) -> pl.DataFrame | None:
200
+ p = sd / f"{name}.csv"
201
+ df = _provider_get(provider, p)
202
+ if df is None or df.height == 0:
203
+ return None
204
+ if "process" in df.columns:
205
+ df = df.rename({"process": "p"})
206
+ return df.select("p", "source", "sink")
207
+
208
+ psse_delayed = _read_psse("process_source_sink_delayed")
209
+ psse_undelayed = _read_psse("process_source_sink_undelayed")
210
+
211
+ # Filter out (p, source) pairs where
212
+ # p_process_source_conversion_flow_coeff == 0: the .mod's
213
+ # conversion_indirect LHS multiplies each source-side flow by this
214
+ # coefficient, so a zero coef effectively drops the row from the
215
+ # input balance. Mirror that filter on the delayed side —
216
+ # _load_indirect already does the same on the undelayed side.
217
+ # Without this, water_pump's west→water_pump delayed input is
218
+ # double-counted in conversion_indirect, forcing the LP to dispatch
219
+ # extra battery/coal to compensate (visible as a ~0.099% gap on
220
+ # test_a_lot's multi-period parity).
221
+ inp = Path(inp_dir)
222
+ src_path = inp / "p_process_source_conversion_flow_coeff.csv"
223
+ srcdf = _provider_get(provider, src_path)
224
+ if srcdf is not None and srcdf.height > 0 and "p_process_source_conversion_flow_coeff" in srcdf.columns:
225
+ zero_src = (srcdf
226
+ .rename({"process": "p",
227
+ "p_process_source_conversion_flow_coeff": "coef"})
228
+ .with_columns(pl.col("coef").cast(pl.Float64, strict=False))
229
+ .filter(pl.col("coef") == 0.0)
230
+ .select("p", "source"))
231
+ if zero_src.height > 0:
232
+ if pse_delayed is not None:
233
+ pse_delayed = pse_delayed.join(
234
+ zero_src, on=["p", "source"], how="anti")
235
+ if psse_delayed is not None:
236
+ psse_delayed = psse_delayed.join(
237
+ zero_src, on=["p", "source"], how="anti")
238
+
239
+ # ``dtt__delay_duration`` / ``p_process_delay_weight`` are produced
240
+ # by ``apply_derived_g`` BUT the mismatch fixture
241
+ # ``work_delay_source_coef`` skips auto-resolution and relies on
242
+ # the seed. Keep CSV reads.
243
+ # TODO(Δ.12c+): retire when ``_find_scenario`` covers the mismatch
244
+ # fixtures.
245
+ dtt_path = sd / "dtt__delay_duration.csv"
246
+ dtt_df = None
247
+ raw = _provider_get(provider, dtt_path)
248
+ if raw is not None and raw.height > 0:
249
+ rename_map = {}
250
+ if "period" in raw.columns:
251
+ rename_map["period"] = "d"
252
+ if "time_source" in raw.columns:
253
+ rename_map["time_source"] = "t_source"
254
+ if "time_sink" in raw.columns:
255
+ rename_map["time_sink"] = "t_sink"
256
+ if "delay_duration" in raw.columns:
257
+ rename_map["delay_duration"] = "td"
258
+ dtt_df = raw.rename(rename_map).select("d", "t_source", "t_sink", "td")
259
+
260
+ pw_path = sd / "p_process_delay_weight.csv"
261
+ pw_param = None
262
+ raw = _provider_get(provider, pw_path)
263
+ if raw is not None and raw.height > 0:
264
+ rename_map = {}
265
+ if "process" in raw.columns:
266
+ rename_map["process"] = "p"
267
+ if "delay_duration" in raw.columns:
268
+ rename_map["delay_duration"] = "td"
269
+ pw_long = raw.rename(rename_map).select("p", "td", "value")
270
+ pw_param = Param(("p", "td"), pw_long)
271
+
272
+ return dict(
273
+ process_delayed = pd_df.select("p"),
274
+ process_delayed__duration = pdd_df,
275
+ process_source_delayed = pse_delayed,
276
+ process_source_undelayed = pse_undelayed,
277
+ process_source_sink_delayed = psse_delayed,
278
+ process_source_sink_undelayed = psse_undelayed,
279
+ dtt__delay_duration = dtt_df,
280
+ p_process_delay_weight = pw_param,
281
+ )
282
+
283
+
284
+ # ---------------------------------------------------------------------------
285
+ # Constraint contribution
286
+
287
+ def delayed_input_expr(d, v_flow):
288
+ """Return an Expr representing the delay-shifted source-side input
289
+ contribution to ``conversion_indirect``.
290
+
291
+ Shape: an Expr with open dims ``(p, d, t)`` (where ``t`` is the
292
+ *sink-side* time, matching ``conversion_indirect``'s axes).
293
+
294
+ Definition (mirrors flextool.mod:2348-2353)::
295
+
296
+ Σ_{source : (p, source) ∈ process_source_delayed}
297
+ Σ_{(d, t_source, t_sink, td) ∈ dtt__delay_duration
298
+ : (p, td) ∈ process_delayed__duration}
299
+ v_flow[p, source, p, d, t_source]
300
+ · unitsize[p] · delay_weight[p, td]
301
+
302
+ Implementation:
303
+
304
+ * Rename v_flow's ``t`` → ``t_source`` so the index aligns with
305
+ ``dtt__delay_duration``.
306
+ * Rename v_flow's ``sink`` → ``p_sink`` and inner-join against the
307
+ delayed input flows (where sink = p, the process's own indirect
308
+ balance node).
309
+ * Inner-join with ``dtt__delay_duration`` on (d, t_source) and with
310
+ ``process_delayed__duration`` on (p, td) — this attaches the
311
+ sink-side ``t`` (renamed back to ``t``) and the delay weight.
312
+ * Multiply by ``p_unitsize[p]`` and ``p_process_delay_weight[p, td]``.
313
+ * ``Sum`` over (source, td) leaves dims (p, d, t).
314
+
315
+ Returns ``None`` when there's no delayed-process data (caller should
316
+ use a no-op).
317
+ """
318
+ if not has_feature(d):
319
+ return None
320
+ if d.dtt__delay_duration is None or d.p_process_delay_weight is None:
321
+ return None
322
+ if d.process_source_delayed is None or d.process_delayed__duration is None:
323
+ return None
324
+ if d.p_unitsize is None:
325
+ return None
326
+
327
+ # Delayed input flows: (p, source, sink=p) — restrict process_input_flows
328
+ # (or build directly from process_source_sink_delayed where sink == p).
329
+ psse_delayed = d.process_source_sink_delayed
330
+ if psse_delayed is None or psse_delayed.height == 0:
331
+ return None
332
+ indirect_inputs_delayed = psse_delayed.filter(
333
+ # Cross-axis value compare: "sink" is e-axis, "p" is process
334
+ # axis. Per contract p ⊂ e; up-cast p to e for the equality
335
+ # so the compare runs in Enum without Utf8 materialisation.
336
+ pl.col("sink") == cast_dim(pl.col("p"), None, "e")
337
+ ).select("p", "source", "sink")
338
+ if indirect_inputs_delayed.height == 0:
339
+ return None
340
+
341
+ # Build the delay-mapping table: (p, source, sink, d, t_source, t, td, weight)
342
+ # Step 1: cross-product of (p, source, sink) ∈ delayed inputs with
343
+ # (p, td) ∈ process_delayed__duration → (p, source, sink, td)
344
+ pdd = d.process_delayed__duration
345
+ pst_td = indirect_inputs_delayed.join(pdd, on="p", how="inner")
346
+ if pst_td.height == 0:
347
+ return None
348
+
349
+ # Step 2: cross-join with dtt__delay_duration on (d, t_source, t_sink, td)
350
+ # via td (and the per-period mapping). dtt has columns
351
+ # (d, t_source, t_sink, td); join on td, get all (d, t_source, t_sink).
352
+ dtt = d.dtt__delay_duration
353
+ full_map = pst_td.join(dtt, on="td", how="inner")
354
+ if full_map.height == 0:
355
+ return None
356
+
357
+ # Step 3: rename t_sink → t for the constraint axes; keep t_source
358
+ # as the v_flow time index, td for the weight lookup.
359
+ full_map = full_map.rename({"t_sink": "t"})
360
+ # Cols now: (p, source, sink, td, d, t_source, t)
361
+
362
+ # The Where filter against v_flow needs a frame whose columns are
363
+ # exactly the dims of v_flow (with t renamed to t_source on the
364
+ # variable side). We use Sum with an explicit ``where`` argument
365
+ # in two steps: rename + Where.
366
+ #
367
+ # v_flow has dims (p, source, sink, d, t). We first build a
368
+ # virtual variable with dims (p, source, sink, d, t_source) by
369
+ # renaming the v_flow frame, then Where it against full_map (which
370
+ # has all those plus t and td). The Where adds t and td as new
371
+ # open dims.
372
+ from polar_high.engine import Var as _Var
373
+ v_flow_at_source = _Var(
374
+ name=v_flow.name + "__at_t_source",
375
+ dims=("p", "source", "sink", "d", "t_source"),
376
+ frame=v_flow.frame.rename({"t": "t_source"}),
377
+ lower=v_flow.lower, upper=v_flow.upper, integer=v_flow.integer,
378
+ )
379
+
380
+ # Where(v_flow_at_source, full_map) joins on (p, source, sink, d, t_source)
381
+ # and adds (td, t) as new open dims — matching what we need.
382
+ expr = Where(v_flow_at_source, full_map)
383
+ # Multiply by unitsize[p] and delay weight[p, td].
384
+ expr = expr * d.p_unitsize * d.p_process_delay_weight
385
+ # Per flextool.mod:2573, the delayed source-side term also carries the
386
+ # ``p_process_source_conversion_flow_coeff[p, source]`` multiplier —
387
+ # same factor that the undelayed source-side term in
388
+ # ``conversion_indirect`` applies (model.py:1424-1425). When the
389
+ # Param is None (every coef=1 default) the multiplication is skipped
390
+ # to keep the Expr's open dims unchanged; when present it covers all
391
+ # surviving (p, source) pairs (defaulted to 1.0 by the loader where
392
+ # the CSV is silent). Without this, fixtures combining a delay with
393
+ # a non-default source coefficient diverge from flextool — see
394
+ # ``tests/test_flex_delay_source_coef.py``.
395
+ if getattr(d, "p_process_source_conversion_flow_coeff", None) is not None:
396
+ expr = expr * d.p_process_source_conversion_flow_coeff
397
+
398
+ # Sum over source, sink, td, t_source → leaves (p, d, t).
399
+ # NOTE: original version listed only ("source", "sink", "td") — but
400
+ # t_source is also an open dim of v_flow_at_source after the Where
401
+ # join. Without summing it, the constraint engine refuses the term
402
+ # because t_source is not in the constraint axes (p, d, t). See
403
+ # ``audit/integration_manifest.md`` "## merge step 4 issues".
404
+ expr = Sum(expr, over=("source", "sink", "td", "t_source"))
405
+ return expr
406
+
407
+
408
+ def add_constraints(m, d, vars: dict) -> None:
409
+ """Add the delayed-flow constraint contribution.
410
+
411
+ *PENDING DOWNSTREAM PATCH (see module docstring):* The merge agent
412
+ must wire the Expr returned by :func:`delayed_input_expr` into
413
+ ``model.py``'s ``conversion_indirect`` ``add_cstr`` call as an
414
+ additional ``lhs_terms`` entry, **and** filter
415
+ ``d.process_input_flows`` to exclude delayed processes. Until that
416
+ patch lands, this function is a no-op (delayed processes will not
417
+ solve correctly — their source-side input will be summed at the
418
+ *current* time without the delay shift).
419
+
420
+ ``vars`` is the dict of decision-variable handles passed in by the
421
+ caller; it should contain ``v_flow`` for the Expr construction. When
422
+ the merge wiring is complete, this function will simply call
423
+ :func:`delayed_input_expr` and feed the result into the existing
424
+ ``conversion_indirect`` constraint via the engine's named-term API.
425
+ """
426
+ if not has_feature(d):
427
+ return
428
+ # Self-contained: nothing to emit until model.py is patched.
429
+ # The Expr is exposed via `delayed_input_expr` for the merge agent.
430
+ return
431
+
432
+
433
+ def add_objective_terms(m, d, vars: dict, op_factor):
434
+ """No delay-specific objective terms.
435
+
436
+ Costs are propagated through commodity prices on the source flows
437
+ (the ``v_flow`` referenced by the delay-shifted aggregate is the
438
+ same variable priced in the existing commodity-buy obj term) and
439
+ through any storage-state slacks the surrounding scenario already
440
+ emits. Returns ``None`` to signal "no contribution".
441
+ """
442
+ return None