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,547 @@
1
+ """Per-(d, t) fallback lookups used by ``entity_period_calc_params``.
2
+
3
+ Three lookup classes implementing the 4-/6-/7-branch fallback chains:
4
+
5
+ * :class:`PdLookup` — 4-branch period-only resolution (used by
6
+ ``pdNode`` / ``pdProcess``).
7
+ * :class:`PdtLookup` — 7- or 9-branch period+time fallback over
8
+ pbt/pd/pt/p + class-level defaults.
9
+ * :class:`PdtLookupPerSide` — 6-branch period+time fallback for the
10
+ (process, side) per-arc tables.
11
+
12
+ Why a class (not a polars-lazy frame)?
13
+ --------------------------------------
14
+
15
+ Consumers call ``.get(entity, param, period, time)`` per output row in
16
+ the inner emission loop. The lookup is a layered
17
+ ``dict[tuple, float]`` cascade — optimal for the access pattern.
18
+ A polars-lazy join chain would be slower at the small fixture sizes
19
+ used by these writers and would obscure the 4-/7-branch semantics.
20
+
21
+ The CSV-read helpers below return plain dicts so the ``.get()`` hot
22
+ path stays pure Python (no polars overhead per call).
23
+
24
+ Branching semantics:
25
+ * 4-branch (``PdLookup``): pd row → branch-sum → p scalar → 0.
26
+ * 7-branch (``PdtLookup``): stochastic-fold → parent-period-fold →
27
+ pd or pt (order toggled by ``time_first_priority``) → p scalar →
28
+ ``param ∈ param_def1`` → class default → 0.
29
+ * 6-branch (``PdtLookupPerSide``): stochastic-fold → parent-period-fold
30
+ → pd → pt → p → 0.
31
+ """
32
+ from __future__ import annotations
33
+
34
+ from pathlib import Path
35
+
36
+ from flextool.engine_polars._emit_provider_io import _provider_key
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Native-frame row helpers.
41
+ #
42
+ # These read the in-memory polars frame directly from the Provider via
43
+ # ``provider.get(_provider_key(path))`` instead of round-tripping through
44
+ # CSV text (the legacy ``_provider_open`` + ``csv.reader`` path).
45
+ #
46
+ # Type-fidelity contract — reproduce *exactly* what ``csv.reader`` over a
47
+ # ``DataFrame.write_csv`` serialisation would have yielded:
48
+ #
49
+ # * Key columns (the dict-key positions) were ``csv.reader`` strings.
50
+ # ``write_csv`` serialises ``null`` → ``""`` and any scalar (Enum /
51
+ # Int / Float / Utf8) → its string form. We therefore coerce each
52
+ # key cell with :func:`_cell_str` (``None`` → ``""``, else ``str``)
53
+ # and apply the original truthiness guard to the *string* form so a
54
+ # null cell is skipped (matching the legacy ``if row[i]`` test) while
55
+ # a literal ``"0"`` is kept.
56
+ # * Value columns were re-coerced with ``float(...)``. We apply
57
+ # ``float(...)`` to the native cell — harmless on an already-float
58
+ # frame, necessary on an int frame, and identical to the legacy
59
+ # ``float(str_cell)`` on a stringified-value frame. A value that
60
+ # cannot be parsed as a float is skipped (matching the legacy
61
+ # ``except ValueError: continue``).
62
+ #
63
+ # ``provider.get`` returns data rows only (no header), so there is no
64
+ # header row to skip; an empty / missing frame yields the same empty
65
+ # dict / list the legacy loop produced.
66
+ # ---------------------------------------------------------------------------
67
+
68
+ def _cell_str(value: "object | None") -> str:
69
+ """Reproduce a ``csv.reader`` cell string for a native frame value.
70
+
71
+ ``DataFrame.write_csv`` renders ``null`` as the empty string and every
72
+ other scalar as its textual form; ``csv.reader`` then reads those
73
+ strings back. Mirror that here so dict keys stay byte-identical to
74
+ the legacy CSV round-trip.
75
+ """
76
+ return "" if value is None else str(value)
77
+
78
+
79
+ def _read_pd(path: Path,
80
+ *, provider: "object | None" = None) -> dict[tuple[str, str, str], float]:
81
+ """4-col frame: (entity, param, period, value)."""
82
+ out: dict[tuple[str, str, str], float] = {}
83
+ df = provider.get(_provider_key(path))
84
+ if df is None:
85
+ return out
86
+ for row in df.iter_rows():
87
+ if len(row) < 4:
88
+ continue
89
+ c0, c1, c2 = _cell_str(row[0]), _cell_str(row[1]), _cell_str(row[2])
90
+ if c0 and c1 and c2:
91
+ try:
92
+ out[(c0, c1, c2)] = float(row[3])
93
+ except (ValueError, TypeError):
94
+ continue
95
+ return out
96
+
97
+
98
+ def _read_p(path: Path,
99
+ *, provider: "object | None" = None) -> dict[tuple[str, str], float]:
100
+ """3-col frame: (entity, param, value)."""
101
+ out: dict[tuple[str, str], float] = {}
102
+ df = provider.get(_provider_key(path))
103
+ if df is None:
104
+ return out
105
+ for row in df.iter_rows():
106
+ if len(row) < 3:
107
+ continue
108
+ c0, c1 = _cell_str(row[0]), _cell_str(row[1])
109
+ if c0 and c1:
110
+ try:
111
+ out[(c0, c1)] = float(row[2])
112
+ except (ValueError, TypeError):
113
+ continue
114
+ return out
115
+
116
+
117
+ def _read_pt(path: Path,
118
+ *, provider: "object | None" = None) -> dict[tuple[str, str, str], float]:
119
+ """4-col frame: (entity, param, time, value)."""
120
+ out: dict[tuple[str, str, str], float] = {}
121
+ df = provider.get(_provider_key(path))
122
+ if df is None:
123
+ return out
124
+ for row in df.iter_rows():
125
+ if len(row) < 4:
126
+ continue
127
+ c0, c1, c2 = _cell_str(row[0]), _cell_str(row[1]), _cell_str(row[2])
128
+ if c0 and c1 and c2:
129
+ try:
130
+ out[(c0, c1, c2)] = float(row[3])
131
+ except (ValueError, TypeError):
132
+ continue
133
+ return out
134
+
135
+
136
+ def _read_pbt(path: Path,
137
+ *, provider: "object | None" = None) -> dict[tuple[str, str, str, str, str], float]:
138
+ """6-col frame: (entity, param, branch, time_start, time, value)."""
139
+ out: dict[tuple[str, str, str, str, str], float] = {}
140
+ df = provider.get(_provider_key(path))
141
+ if df is None:
142
+ return out
143
+ for row in df.iter_rows():
144
+ if len(row) < 6:
145
+ continue
146
+ c = [_cell_str(row[i]) for i in range(5)]
147
+ if all(c):
148
+ try:
149
+ out[(c[0], c[1], c[2], c[3], c[4])] = float(row[5])
150
+ except (ValueError, TypeError):
151
+ continue
152
+ return out
153
+
154
+
155
+ def _read_pd_3(path: Path,
156
+ *, provider: "object | None" = None) -> dict[tuple[str, str, str, str], float]:
157
+ """5-col frame: (e1, e2, param, period, value)."""
158
+ out: dict[tuple[str, str, str, str], float] = {}
159
+ df = provider.get(_provider_key(path))
160
+ if df is None:
161
+ return out
162
+ for row in df.iter_rows():
163
+ if len(row) < 5:
164
+ continue
165
+ c = [_cell_str(row[i]) for i in range(4)]
166
+ if all(c):
167
+ try:
168
+ out[(c[0], c[1], c[2], c[3])] = float(row[4])
169
+ except (ValueError, TypeError):
170
+ continue
171
+ return out
172
+
173
+
174
+ def _read_pt_3(path: Path,
175
+ *, provider: "object | None" = None) -> dict[tuple[str, str, str, str], float]:
176
+ """5-col frame: (e1, e2, param, time, value)."""
177
+ out: dict[tuple[str, str, str, str], float] = {}
178
+ df = provider.get(_provider_key(path))
179
+ if df is None:
180
+ return out
181
+ for row in df.iter_rows():
182
+ if len(row) < 5:
183
+ continue
184
+ c = [_cell_str(row[i]) for i in range(4)]
185
+ if all(c):
186
+ try:
187
+ out[(c[0], c[1], c[2], c[3])] = float(row[4])
188
+ except (ValueError, TypeError):
189
+ continue
190
+ return out
191
+
192
+
193
+ def _read_p_3(path: Path,
194
+ *, provider: "object | None" = None) -> dict[tuple[str, str, str], float]:
195
+ """4-col frame: (e1, e2, param, value)."""
196
+ out: dict[tuple[str, str, str], float] = {}
197
+ df = provider.get(_provider_key(path))
198
+ if df is None:
199
+ return out
200
+ for row in df.iter_rows():
201
+ if len(row) < 4:
202
+ continue
203
+ c = [_cell_str(row[i]) for i in range(3)]
204
+ if all(c):
205
+ try:
206
+ out[(c[0], c[1], c[2])] = float(row[3])
207
+ except (ValueError, TypeError):
208
+ continue
209
+ return out
210
+
211
+
212
+ def _read_pbt_3(path: Path,
213
+ *, provider: "object | None" = None) -> dict[tuple[str, str, str, str, str, str], float]:
214
+ """7-col frame: (e1, e2, param, branch, time_start, time, value)."""
215
+ out: dict[tuple[str, str, str, str, str, str], float] = {}
216
+ df = provider.get(_provider_key(path))
217
+ if df is None:
218
+ return out
219
+ for row in df.iter_rows():
220
+ if len(row) < 7:
221
+ continue
222
+ c = [_cell_str(row[i]) for i in range(6)]
223
+ if all(c):
224
+ try:
225
+ out[(c[0], c[1], c[2], c[3], c[4], c[5])] = float(row[6])
226
+ except (ValueError, TypeError):
227
+ continue
228
+ return out
229
+
230
+
231
+ def _read_pairs_to_dict(path: Path, key_col: int,
232
+ *, provider: "object | None" = None) -> dict[str, list[str]]:
233
+ """Generic two-col frame → ``key_col → list[other_col]``."""
234
+ out: dict[str, list[str]] = {}
235
+ df = provider.get(_provider_key(path))
236
+ if df is None:
237
+ return out
238
+ other_col = 1 - key_col
239
+ for row in df.iter_rows():
240
+ if len(row) < 2:
241
+ continue
242
+ c0, c1 = _cell_str(row[0]), _cell_str(row[1])
243
+ if c0 and c1:
244
+ out.setdefault((c0, c1)[key_col], []).append((c0, c1)[other_col])
245
+ return out
246
+
247
+
248
+ def _read_period_branch(path: Path,
249
+ *, provider: "object | None" = None) -> dict[str, list[str]]:
250
+ """Build ``d → [db]`` from ``period__branch`` (col0=db, col1=d)."""
251
+ out: dict[str, list[str]] = {}
252
+ df = provider.get(_provider_key(path))
253
+ if df is None:
254
+ return out
255
+ for row in df.iter_rows():
256
+ if len(row) < 2:
257
+ continue
258
+ c0, c1 = _cell_str(row[0]), _cell_str(row[1])
259
+ if c0 and c1:
260
+ out.setdefault(c1, []).append(c0)
261
+ return out
262
+
263
+
264
+ def _read_singles(path: Path,
265
+ *, provider: "object | None" = None) -> list[str]:
266
+ df = provider.get(_provider_key(path))
267
+ if df is None:
268
+ return []
269
+ out: list[str] = []
270
+ for row in df.iter_rows():
271
+ if not row:
272
+ continue
273
+ c0 = _cell_str(row[0])
274
+ if c0:
275
+ out.append(c0)
276
+ return out
277
+
278
+
279
+ # ---------------------------------------------------------------------------
280
+ # Param-default enums (mirror flextool_base.dat — pinned here to avoid
281
+ # importing the legacy preprocessing tree).
282
+ # ---------------------------------------------------------------------------
283
+
284
+ PROCESS_PARAM_DEF1: frozenset[str] = frozenset({"efficiency", "availability"})
285
+ NODE_PARAM_DEF1: frozenset[str] = frozenset({"availability"})
286
+
287
+
288
+ def read_class_defaults(path: Path, class_name: str,
289
+ *, provider: "object | None" = None) -> dict[str, float]:
290
+ """Read ``input/default_values.csv`` filtered to ``class_name``."""
291
+ out: dict[str, float] = {}
292
+ df = provider.get(_provider_key(path))
293
+ if df is None:
294
+ return out
295
+ for row in df.iter_rows():
296
+ if len(row) < 3:
297
+ continue
298
+ c0, c1 = _cell_str(row[0]), _cell_str(row[1])
299
+ if c0 == class_name and c1:
300
+ try:
301
+ out[c1] = float(row[2])
302
+ except (ValueError, TypeError):
303
+ continue
304
+ return out
305
+
306
+
307
+ # ---------------------------------------------------------------------------
308
+ # PdLookup — 4-branch period-only resolution.
309
+ # ---------------------------------------------------------------------------
310
+
311
+ class PdLookup:
312
+ """Mod's 4-branch period-param resolution (pdProcess / pdNode @ L1252/L1216).
313
+
314
+ Branch 1: explicit (e, param, d) row in ``pd_<class>``.
315
+ Branch 2: any ``(e, param, db)`` where ``(db, d)`` ∈ period__branch — sum.
316
+ Branch 3: scalar fallback in ``p_<class>``.
317
+ Branch 4: 0.
318
+ """
319
+
320
+ def __init__(
321
+ self,
322
+ pd_csv: Path, # input/pd_<class>.csv
323
+ p_csv: Path, # input/p_<class>.csv
324
+ period_branch_csv: Path, # solve_data/period__branch.csv
325
+ *,
326
+ provider: "object | None" = None,
327
+ ) -> None:
328
+ self._pd = _read_pd(pd_csv, provider=provider)
329
+ self._p = _read_p(p_csv, provider=provider)
330
+ self._branches_for_d = _read_period_branch(period_branch_csv, provider=provider)
331
+
332
+ def get(self, e: str, param: str, d: str) -> float:
333
+ if (e, param, d) in self._pd:
334
+ return self._pd[(e, param, d)]
335
+ branches = self._branches_for_d.get(d, ())
336
+ branched = [
337
+ self._pd[(e, param, db)]
338
+ for db in branches
339
+ if (e, param, db) in self._pd
340
+ ]
341
+ if branched:
342
+ return sum(branched)
343
+ if (e, param) in self._p:
344
+ return self._p[(e, param)]
345
+ return 0.0
346
+
347
+
348
+ # ---------------------------------------------------------------------------
349
+ # PdtLookup — 7/9-branch period+time fallback.
350
+ # ---------------------------------------------------------------------------
351
+
352
+ class PdtLookup:
353
+ """Mod's 7/9-branch ``pdtProcess`` / ``pdtNode`` resolution.
354
+
355
+ Outer index ``(e, param, d, t)``. Branches in order:
356
+
357
+ 1. stochastic-fold: e ∈ groupStochastic-flagged group — sum pbt rows
358
+ over ts ∈ first_timesteps[d], tb ∈ solve_branch[d].
359
+ 2. parent-period-fold: for each pe with (pe, d) ∈ period__branch,
360
+ sum pbt rows over ts ∈ first_timesteps[d], tb ∈ solve_branch[pe].
361
+ 3-4. pt or pd row (order toggled by ``time_first_priority``;
362
+ pdtNode prefers time, pdtProcess prefers period).
363
+ 5. p scalar.
364
+ 6. ``param ∈ param_def1`` → 1.
365
+ 7. class-default (pdtNode only).
366
+ 8. 0.
367
+ """
368
+
369
+ def __init__(
370
+ self,
371
+ pbt_csv: Path,
372
+ pd_csv: Path,
373
+ pt_csv: Path,
374
+ p_csv: Path,
375
+ period_time_first_csv: Path,
376
+ solve_branch_csv: Path,
377
+ period_branch_csv: Path,
378
+ group_entity_csv: Path,
379
+ group_stochastic_csv: Path,
380
+ param_def1: frozenset[str],
381
+ time_first_priority: bool = False,
382
+ class_default_values: dict[str, float] | None = None,
383
+ *,
384
+ provider: "object | None" = None,
385
+ ) -> None:
386
+ self._pbt = _read_pbt(pbt_csv, provider=provider)
387
+ self._pd = _read_pd(pd_csv, provider=provider)
388
+ self._pt = _read_pt(pt_csv, provider=provider)
389
+ self._p = _read_p(p_csv, provider=provider)
390
+ self._ts_for_d = _read_pairs_to_dict(period_time_first_csv, key_col=0, provider=provider)
391
+ self._tb_for_d = _read_pairs_to_dict(solve_branch_csv, key_col=0, provider=provider)
392
+ self._pe_for_d = _read_pairs_to_dict(period_branch_csv, key_col=1, provider=provider)
393
+ groups_stoch = frozenset(_read_singles(group_stochastic_csv, provider=provider))
394
+ self._stoch_entity: set[str] = set()
395
+ ge_df = provider.get(_provider_key(group_entity_csv))
396
+ if ge_df is not None:
397
+ for row in ge_df.iter_rows():
398
+ if len(row) < 2:
399
+ continue
400
+ c0, c1 = _cell_str(row[0]), _cell_str(row[1])
401
+ if c0 in groups_stoch and c1:
402
+ self._stoch_entity.add(c1)
403
+ self._param_def1 = param_def1
404
+ self._time_first = time_first_priority
405
+ self._class_defaults = class_default_values or {}
406
+
407
+ def get(self, e: str, param: str, d: str, t: str) -> float:
408
+ # Branch 1: stochastic fold-in.
409
+ if e in self._stoch_entity:
410
+ ts_list = self._ts_for_d.get(d, ())
411
+ tb_list = self._tb_for_d.get(d, ())
412
+ total = 0.0
413
+ hit = False
414
+ for tb in tb_list:
415
+ for ts in ts_list:
416
+ v = self._pbt.get((e, param, tb, ts, t))
417
+ if v is not None:
418
+ total += v
419
+ hit = True
420
+ if hit:
421
+ return total
422
+ # Branch 2: parent-period fold-in.
423
+ ts_list = self._ts_for_d.get(d, ())
424
+ pe_list = self._pe_for_d.get(d, ())
425
+ if pe_list and ts_list:
426
+ total = 0.0
427
+ hit = False
428
+ for pe in pe_list:
429
+ for tb in self._tb_for_d.get(pe, ()):
430
+ for ts in ts_list:
431
+ v = self._pbt.get((e, param, tb, ts, t))
432
+ if v is not None:
433
+ total += v
434
+ hit = True
435
+ if hit:
436
+ return total
437
+ # Branches 3-4: pt vs pd order toggle.
438
+ if self._time_first:
439
+ v = self._pt.get((e, param, t))
440
+ if v is not None:
441
+ return v
442
+ v = self._pd.get((e, param, d))
443
+ if v is not None:
444
+ return v
445
+ else:
446
+ v = self._pd.get((e, param, d))
447
+ if v is not None:
448
+ return v
449
+ v = self._pt.get((e, param, t))
450
+ if v is not None:
451
+ return v
452
+ # Branch 5: scalar.
453
+ v = self._p.get((e, param))
454
+ if v is not None:
455
+ return v
456
+ # Branch 6: param_def1 default-1.
457
+ if param in self._param_def1:
458
+ return 1.0
459
+ # Branch 7: class default.
460
+ if param in self._class_defaults:
461
+ return self._class_defaults[param]
462
+ # Branch 8: 0.
463
+ return 0.0
464
+
465
+
466
+ # ---------------------------------------------------------------------------
467
+ # PdtLookupPerSide — 6-branch for (process, side) entities.
468
+ # ---------------------------------------------------------------------------
469
+
470
+ class PdtLookupPerSide:
471
+ """6-branch pdt lookup for ``pdtProcess_source`` / ``pdtProcess_sink``.
472
+
473
+ Entity key is ``(process, side)``. No def1 branch; otherwise same
474
+ cascade as :class:`PdtLookup` minus the param_def1 / class-default
475
+ layers.
476
+ """
477
+
478
+ def __init__(
479
+ self,
480
+ pbt_csv: Path,
481
+ pd_csv: Path,
482
+ pt_csv: Path,
483
+ p_csv: Path,
484
+ period_time_first_csv: Path,
485
+ solve_branch_csv: Path,
486
+ period_branch_csv: Path,
487
+ group_process_csv: Path,
488
+ group_stochastic_csv: Path,
489
+ *,
490
+ provider: "object | None" = None,
491
+ ) -> None:
492
+ self._pbt = _read_pbt_3(pbt_csv, provider=provider)
493
+ self._pd = _read_pd_3(pd_csv, provider=provider)
494
+ self._pt = _read_pt_3(pt_csv, provider=provider)
495
+ self._p = _read_p_3(p_csv, provider=provider)
496
+ self._ts_for_d = _read_pairs_to_dict(period_time_first_csv, key_col=0, provider=provider)
497
+ self._tb_for_d = _read_pairs_to_dict(solve_branch_csv, key_col=0, provider=provider)
498
+ self._pe_for_d = _read_pairs_to_dict(period_branch_csv, key_col=1, provider=provider)
499
+ groups_stoch = frozenset(_read_singles(group_stochastic_csv, provider=provider))
500
+ self._stoch_process: set[str] = set()
501
+ gp_df = provider.get(_provider_key(group_process_csv))
502
+ if gp_df is not None:
503
+ for row in gp_df.iter_rows():
504
+ if len(row) < 2:
505
+ continue
506
+ c0, c1 = _cell_str(row[0]), _cell_str(row[1])
507
+ if c0 in groups_stoch and c1:
508
+ self._stoch_process.add(c1)
509
+
510
+ def get(self, p: str, side: str, param: str, d: str, t: str) -> float:
511
+ if p in self._stoch_process:
512
+ ts_list = self._ts_for_d.get(d, ())
513
+ tb_list = self._tb_for_d.get(d, ())
514
+ total = 0.0
515
+ hit = False
516
+ for tb in tb_list:
517
+ for ts in ts_list:
518
+ v = self._pbt.get((p, side, param, tb, ts, t))
519
+ if v is not None:
520
+ total += v
521
+ hit = True
522
+ if hit:
523
+ return total
524
+ ts_list = self._ts_for_d.get(d, ())
525
+ pe_list = self._pe_for_d.get(d, ())
526
+ if pe_list and ts_list:
527
+ total = 0.0
528
+ hit = False
529
+ for pe in pe_list:
530
+ for tb in self._tb_for_d.get(pe, ()):
531
+ for ts in ts_list:
532
+ v = self._pbt.get((p, side, param, tb, ts, t))
533
+ if v is not None:
534
+ total += v
535
+ hit = True
536
+ if hit:
537
+ return total
538
+ v = self._pd.get((p, side, param, d))
539
+ if v is not None:
540
+ return v
541
+ v = self._pt.get((p, side, param, t))
542
+ if v is not None:
543
+ return v
544
+ v = self._p.get((p, side, param))
545
+ if v is not None:
546
+ return v
547
+ return 0.0