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,432 @@
1
+ """Cluster F — scalar Param-on-Param arithmetic (Δ.10).
2
+
3
+ Lazy-polars helpers for the "scalar arithmetic" cluster identified in
4
+ ``audit/native_data_path_design_derived_clusters.md`` §F. The cluster
5
+ groups Params whose source-driven build is a thin composition of the
6
+ already-cached primitives (``_entity_unitsize_lf`` /
7
+ ``_node_unitsize_lf``, ``pss``, ``nodeBalance``, the ``dt`` (d, t)
8
+ grid). They share the same shape: pull a scalar / per-entity value
9
+ out of Spine, broadcast over a structural set, optionally apply the
10
+ unitsize cascade.
11
+
12
+ Δ.10 lifts six helpers off ``input.py``'s CSV preprocessing:
13
+
14
+ * :func:`p_unitsize_from_source` — ``(p,)`` per-process
15
+ unitsize; cascade ``virtual_unitsize OR existing OR 1000`` filtered
16
+ to processes appearing in ``pss``.
17
+ * :func:`p_state_unitsize_from_source` — ``(n,)`` per-node
18
+ unitsize; same cascade filtered to nodes in ``nodeState``.
19
+ * :func:`p_penalty_up_from_source` /
20
+ :func:`p_penalty_down_from_source` — ``(n, d, t)`` sentinel-
21
+ default scalar broadcast over the node × dt grid.
22
+ * :func:`p_process_source_conversion_flow_coeff_from_source` /
23
+ :func:`p_process_sink_conversion_flow_coeff_from_source` —
24
+ ``(p, source)`` / ``(p, sink)`` conversion-flow coefficients with
25
+ zero-drop semantics on the caller's input/output sets.
26
+
27
+ Architecture invariants (per the Δ.10 hand-off):
28
+
29
+ 1. **Lazy polars throughout.** Every helper returns a lazy chain
30
+ collected once at the rim.
31
+ 2. **None-default skip.** When the parameter has no rows on the
32
+ source side and no scalar default, the helper returns ``None``;
33
+ the caller leaves the field untouched. *Exception:* the two
34
+ unitsize helpers (``p_unitsize`` / ``p_state_unitsize``) return
35
+ ``None`` ONLY for an empty structural set — for a non-empty set
36
+ they are complete-by-construction (see invariant 4), because the
37
+ model multiplies every flow/state variable by them.
38
+ 3. **No defensive gating.** Helpers fail loudly if the cascade
39
+ primitives drift; the parity sweep is the oracle.
40
+ 4. **Unitsize completeness.** ``p_unitsize`` / ``p_state_unitsize``
41
+ are structural LP coefficients, so they must COVER every member of
42
+ their input set. :func:`_unitsize_complete` left-joins the set
43
+ against the cascade and coalesces uncovered members to the cascade's
44
+ own canonical default (:data:`UNITSIZE_DEFAULT`), warning on each so
45
+ a real set/vocabulary divergence stays visible. This is the
46
+ build-side analogue of the output-side completeness carrier
47
+ (``p_all_entity_unitsize``); fixing it here, at the producer, rather
48
+ than guarding ``None`` at each consumer, is the upstream fix.
49
+
50
+ The cluster F existing helpers (``p_slope``, ``p_section``,
51
+ ``p_flow_upper_existing``, ``p_state_upper``, ``p_process_existing_count``)
52
+ remain in :mod:`._derived_params`; this module hosts only the new Δ.10
53
+ helpers — verification of the existing helpers is a code-review pass,
54
+ not a re-port.
55
+ """
56
+ from __future__ import annotations
57
+
58
+ import logging
59
+ from typing import TYPE_CHECKING
60
+
61
+ import polars as pl
62
+
63
+ from polar_high import Param
64
+
65
+ from flextool.engine_polars._axis_enums import (
66
+ alias_to_axis,
67
+ cast_frame_axes,
68
+ get_global_axis_enums,
69
+ rename_to_axis,
70
+ )
71
+
72
+ from ._derived_params import (
73
+ UNITSIZE_DEFAULT,
74
+ _entity_unitsize_lf,
75
+ _node_unitsize_lf,
76
+ _try_param,
77
+ )
78
+
79
+ if TYPE_CHECKING:
80
+ from flextool.engine_polars._input_source import InputSource
81
+
82
+ logger = logging.getLogger(__name__)
83
+
84
+
85
+ def _unitsize_complete(keys_lf: "pl.LazyFrame",
86
+ us_lf: "pl.LazyFrame",
87
+ axis: str,
88
+ *,
89
+ what: str) -> pl.DataFrame:
90
+ """Project the unitsize cascade onto a structural ``axis`` set so the
91
+ result COVERS every member of that set — never a subset, never empty
92
+ for a non-empty input.
93
+
94
+ ``p_unitsize`` / ``p_state_unitsize`` are *structural* LP coefficients:
95
+ ``build_flextool`` multiplies every ``v_flow`` (resp. ``v_state`` /
96
+ ``v_invest``) by them. A member of ``keys_lf`` (the pss processes /
97
+ nodeState nodes the model creates variables for) that is missing from
98
+ the cascade must therefore NOT be dropped — an incomplete projection
99
+ silently drops constraint terms, and an *empty* one yields ``None``
100
+ and crashes the build at ``model.py`` with
101
+ ``TypeError: ... 'Expr' and 'NoneType'``.
102
+
103
+ We left-join the set against the cascade and coalesce any uncovered
104
+ member to :data:`UNITSIZE_DEFAULT` — the same fallback the cascade
105
+ itself applies to an entity with no explicit unitsize — so the
106
+ invariant "every variable's unitsize is defined" holds by
107
+ construction. The join key is compared as ``Utf8`` so value-equal
108
+ members match regardless of any Enum-dtype edge between the structural
109
+ set's axis and the entity-union cascade axis. A coalesced member is a
110
+ genuine set/vocabulary divergence upstream, so we ``warning`` the
111
+ offending tokens rather than absorb them silently.
112
+
113
+ For a healthy model (every member covered) this is byte-identical to
114
+ the prior inner-join projection: the left-join matches all rows and no
115
+ coalesce fires.
116
+ """
117
+ keys = (keys_lf.select(pl.col(axis)).unique()
118
+ .with_columns(pl.col(axis).cast(pl.Utf8).alias("_k")))
119
+ rhs = us_lf.select(pl.col(axis).cast(pl.Utf8).alias("_k"), pl.col("us"))
120
+ joined = keys.join(rhs, on="_k", how="left").collect()
121
+ missing = joined.filter(pl.col("us").is_null())
122
+ if missing.height:
123
+ names = missing.get_column(axis).cast(pl.Utf8).to_list()
124
+ shown = ", ".join(names[:20]) + (" …" if missing.height > 20 else "")
125
+ logger.warning(
126
+ "%s: %d %r member(s) absent from the entity-unitsize cascade; "
127
+ "applying the canonical %.0f default. This signals a structural "
128
+ "set / entity-vocabulary divergence upstream: %s",
129
+ what, missing.height, axis, UNITSIZE_DEFAULT, shown,
130
+ )
131
+ return (joined
132
+ .select(pl.col(axis),
133
+ pl.col("us").fill_null(UNITSIZE_DEFAULT).alias("value"))
134
+ .sort(axis))
135
+
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # §F.1 — p_unitsize (per-process unitsize, filtered to pss)
139
+ # ---------------------------------------------------------------------------
140
+
141
+
142
+ def p_unitsize_from_source(source: "InputSource",
143
+ pss: pl.DataFrame | None,
144
+ ) -> Param | None:
145
+ """``p_unitsize[p]`` — per-process unitsize cascade restricted to
146
+ processes appearing in *pss*.
147
+
148
+ Mirrors ``flextool/engine_polars/input.py:800-825``::
149
+
150
+ unitsize_long = _read_unitsize(p_entity_unitsize.csv)
151
+ unitsize_p = unitsize_long.filter(p ∈ pss["p"].unique())
152
+
153
+ The Spine source for ``p_entity_unitsize`` is the cascade::
154
+
155
+ unitsize = virtual_unitsize (if explicitly set non-zero)
156
+ OR existing (if explicitly set non-zero)
157
+ OR 1000.0
158
+
159
+ Already lazified in :func:`._derived_params._entity_unitsize_lf`
160
+ (cluster B/C primitive); we project the cascade onto the processes in
161
+ *pss*. Returns ``None`` only when *pss* is empty — for a non-empty
162
+ *pss* the result is COMPLETE (every process covered, uncovered ones
163
+ defaulted to :data:`UNITSIZE_DEFAULT`), because the model treats
164
+ ``p_unitsize`` as a structural coefficient on every ``v_flow``. See
165
+ :func:`_unitsize_complete`.
166
+ """
167
+ if pss is None or pss.height == 0:
168
+ return None
169
+ # Phase 4.8f: defend axis-aware join keys on incoming frame param.
170
+ _enums = get_global_axis_enums()
171
+ if _enums is not None:
172
+ pss = cast_frame_axes(pss, _enums)
173
+ us_lf = _entity_unitsize_lf(source).pipe(rename_to_axis, {"e": "p"})
174
+ df = _unitsize_complete(pss.lazy(), us_lf, "p", what="p_unitsize")
175
+ if df.height == 0:
176
+ return None
177
+ return Param(("p",), df)
178
+
179
+
180
+ # ---------------------------------------------------------------------------
181
+ # §F.2 — p_state_unitsize (per-node unitsize, filtered to nodeState)
182
+ # ---------------------------------------------------------------------------
183
+
184
+
185
+ def p_state_unitsize_from_source(source: "InputSource",
186
+ nodeState_df: pl.DataFrame | None,
187
+ ) -> Param | None:
188
+ """``p_state_unitsize[n]`` — per-node unitsize restricted to nodes
189
+ in *nodeState_df*.
190
+
191
+ Mirrors ``input.py:1780-1789``::
192
+
193
+ state_us_long = unitsize_long.filter(n ∈ nodeState["n"])
194
+
195
+ Uses the canonical :func:`._derived_params._node_unitsize_lf`
196
+ cascade. Returns ``None`` only when *nodeState_df* is empty — for a
197
+ non-empty *nodeState_df* the result is COMPLETE (every node covered,
198
+ uncovered ones defaulted to :data:`UNITSIZE_DEFAULT`), because the
199
+ model treats ``p_state_unitsize`` as a structural coefficient on every
200
+ ``v_state`` / ``v_invest`` term. See :func:`_unitsize_complete`.
201
+ """
202
+ if nodeState_df is None or nodeState_df.height == 0:
203
+ return None
204
+ # Phase 4.8f: defend axis-aware join keys on incoming frame param.
205
+ _enums = get_global_axis_enums()
206
+ if _enums is not None:
207
+ nodeState_df = cast_frame_axes(nodeState_df, _enums)
208
+ us_lf = _node_unitsize_lf(source)
209
+ df = _unitsize_complete(nodeState_df.lazy(), us_lf, "n",
210
+ what="p_state_unitsize")
211
+ if df.height == 0:
212
+ return None
213
+ return Param(("n",), df)
214
+
215
+
216
+ # ---------------------------------------------------------------------------
217
+ # §F.3 — p_penalty_up / p_penalty_down (sentinel-default scalar broadcast)
218
+ # ---------------------------------------------------------------------------
219
+
220
+
221
+ def _penalty_param_from_source(source: "InputSource",
222
+ parameter_name: str,
223
+ nodeBalance_df: pl.DataFrame | None,
224
+ dt: pl.DataFrame | None,
225
+ ) -> Param | None:
226
+ """Build ``p_penalty_<dir>[n, d, t]`` by broadcasting the per-node
227
+ scalar / Map (period→time) over the (n, d, t) grid restricted to
228
+ nodes in *nodeBalance_df*.
229
+
230
+ ``node.penalty_up`` / ``node.penalty_down`` carry a sentinel
231
+ default (e.g. 10000.0) on the schema; the source plugin returns one
232
+ row per node with the scalar (default-broadcast). The CSV path
233
+ slices ``pdtNode.csv`` already broadcast to (n, d, t); we mirror by
234
+ cross-joining the per-node value against the active-solve dt grid.
235
+
236
+ Map-shaped inputs (per-period or per-(period, time) overrides) are
237
+ passed through their own indices and joined on the matching subset
238
+ of (d, t). Scalar inputs broadcast to every (d, t).
239
+ """
240
+ if (nodeBalance_df is None or nodeBalance_df.height == 0
241
+ or dt is None or dt.height == 0):
242
+ return None
243
+ # Phase 4.8f: defend axis-aware join keys on incoming frame params.
244
+ _enums = get_global_axis_enums()
245
+ if _enums is not None:
246
+ nodeBalance_df = cast_frame_axes(nodeBalance_df, _enums)
247
+ dt = cast_frame_axes(dt, _enums)
248
+ df = _try_param(source, "node", parameter_name)
249
+ if df is None or df.height == 0:
250
+ return None
251
+ nb_lf = nodeBalance_df.lazy().select(pl.col("n")).unique()
252
+ # Defensive re-cast: re-cast d/t to canonical Enum so joins below
253
+ # against ``base`` (which alias_to_axis-casts already) match dtype
254
+ # even when ``dt`` arrives with Utf8 d/t.
255
+ dt_lf = (dt.lazy()
256
+ .select(alias_to_axis("d", "d"), alias_to_axis("t", "t"))
257
+ .unique())
258
+ cols = df.columns
259
+ period_col = next((c for c in ("period", "d", "x") if c in cols), None)
260
+ time_col = next((c for c in ("t", "time", "step") if c in cols), None)
261
+ base = df.lazy().select(
262
+ alias_to_axis("name", "n"),
263
+ *([alias_to_axis(period_col, "d")] if period_col else []),
264
+ *([alias_to_axis(time_col, "t")] if time_col else []),
265
+ pl.col("value").cast(pl.Float64),
266
+ )
267
+ if period_col and time_col:
268
+ out_lf = (nb_lf
269
+ .join(base, on="n", how="inner")
270
+ .join(dt_lf, on=["d", "t"], how="inner"))
271
+ elif period_col:
272
+ out_lf = (nb_lf
273
+ .join(base, on="n", how="inner")
274
+ .join(dt_lf, on="d", how="inner"))
275
+ elif time_col:
276
+ out_lf = (nb_lf
277
+ .join(base, on="n", how="inner")
278
+ .join(dt_lf, on="t", how="inner"))
279
+ else:
280
+ out_lf = (nb_lf
281
+ .join(base, on="n", how="inner")
282
+ .join(dt_lf, how="cross"))
283
+ out = (out_lf
284
+ .select("n", "d", "t", "value")
285
+ .sort("n", "d", "t")
286
+ .collect())
287
+ if out.height == 0:
288
+ return None
289
+ return Param(("n", "d", "t"), out)
290
+
291
+
292
+ def p_penalty_up_from_source(source: "InputSource",
293
+ nodeBalance_df: pl.DataFrame | None,
294
+ dt: pl.DataFrame | None,
295
+ ) -> Param | None:
296
+ """``p_penalty_up[n, d, t]`` — broadcast ``node.penalty_up`` over
297
+ nodeBalance × dt. See :func:`_penalty_param_from_source`.
298
+ """
299
+ return _penalty_param_from_source(source, "penalty_up",
300
+ nodeBalance_df, dt)
301
+
302
+
303
+ def p_penalty_down_from_source(source: "InputSource",
304
+ nodeBalance_df: pl.DataFrame | None,
305
+ dt: pl.DataFrame | None,
306
+ ) -> Param | None:
307
+ """``p_penalty_down[n, d, t]`` — broadcast ``node.penalty_down`` over
308
+ nodeBalance × dt. See :func:`_penalty_param_from_source`.
309
+ """
310
+ return _penalty_param_from_source(source, "penalty_down",
311
+ nodeBalance_df, dt)
312
+
313
+
314
+ # ---------------------------------------------------------------------------
315
+ # §F.4 — p_process_source_conversion_flow_coeff /
316
+ # p_process_sink_conversion_flow_coeff
317
+ # ---------------------------------------------------------------------------
318
+ #
319
+ # Mirrors input.py:950-1002. The CSV path reads
320
+ # p_process_source_conversion_flow_coeff.csv /
321
+ # p_process_sink_conversion_flow_coeff.csv (always emitted by flextool's
322
+ # input writer for indirect units), then:
323
+ #
324
+ # 1. Anti-joins zero-coef rows out of the inputs / outputs sets.
325
+ # 2. If any non-default (≠ 1.0) non-zero coef remains on a surviving
326
+ # (p, source) / (p, sink) pair, builds a Param keyed on the *full*
327
+ # surviving set (default-fill 1.0 where not listed). The default-
328
+ # fill is structural: ``v_flow * Param`` would inner-join and drop
329
+ # rows otherwise.
330
+ # 3. If every coef is 1.0 (the trivial CHP-base case), the Param is
331
+ # ``None`` — model.py's gate falls through to the no-coef path.
332
+ #
333
+ # Spine source: ``unit__inputNode.conversion_flow_coeff`` /
334
+ # ``unit__outputNode.conversion_flow_coeff``. The default value on
335
+ # the schema is 1.0; values are stored on the relationship class.
336
+
337
+
338
+ def _flow_coef_from_source(source: "InputSource",
339
+ relationship_class: str,
340
+ node_role: str,
341
+ indirect_pairs: pl.DataFrame | None,
342
+ ) -> tuple[pl.DataFrame | None,
343
+ Param | None]:
344
+ """Build (zero_pairs, coef_param) for a relationship's flow
345
+ coefficient.
346
+
347
+ *relationship_class* — ``"unit__inputNode"`` or ``"unit__outputNode"``.
348
+ *node_role* — ``"source"`` or ``"sink"`` (the column name to alias
349
+ the node dim to in the output).
350
+ *indirect_pairs* — the caller's surviving (p, source) / (p, sink)
351
+ set (post upstream filters), used to gate Param generation and
352
+ default-fill.
353
+
354
+ Returns:
355
+ * ``zero_pairs`` — ``(p, <node_role>)`` rows where coef == 0; the
356
+ caller anti-joins these out of its own inputs / outputs set.
357
+ * ``coef_param`` — ``Param((p, <node_role>), value)`` covering
358
+ *every* surviving pair (default-fill 1.0) iff any non-default,
359
+ non-zero coef is present; otherwise ``None``.
360
+ """
361
+ df = _try_param(source, relationship_class, "conversion_flow_coeff")
362
+ if df is None or df.height == 0:
363
+ return None, None
364
+ cols = df.columns
365
+ # The relationship has two dims: "unit" + "node". source.parameter
366
+ # returns them as columns of the same names.
367
+ unit_col = "unit" if "unit" in cols else cols[0]
368
+ node_col = "node" if "node" in cols else cols[1]
369
+ base = df.lazy().select(
370
+ alias_to_axis(unit_col, "p"),
371
+ alias_to_axis(node_col, node_role),
372
+ pl.col("value").cast(pl.Float64).alias("coef"),
373
+ )
374
+ zero = (base.filter(pl.col("coef") == 0.0)
375
+ .select("p", node_role)
376
+ .collect())
377
+ if zero.height == 0:
378
+ zero = None
379
+ if indirect_pairs is None or indirect_pairs.height == 0:
380
+ return zero, None
381
+ # Phase 4.8f: defend axis-aware join keys on incoming frame param.
382
+ _enums = get_global_axis_enums()
383
+ if _enums is not None:
384
+ indirect_pairs = cast_frame_axes(indirect_pairs, _enums)
385
+ nondef = base.filter(
386
+ (pl.col("coef") != 0.0) & (pl.col("coef") != 1.0))
387
+ if nondef.collect().height == 0:
388
+ return zero, None
389
+ pair_lf = (indirect_pairs.lazy()
390
+ .select("p", node_role).unique())
391
+ merged = (pair_lf
392
+ .join(base, on=["p", node_role], how="left")
393
+ .with_columns(pl.col("coef").fill_null(1.0))
394
+ .select("p", node_role, pl.col("coef").alias("value"))
395
+ .sort("p", node_role)
396
+ .collect())
397
+ if merged.height == 0:
398
+ return zero, None
399
+ return zero, Param(("p", node_role), merged)
400
+
401
+
402
+ def p_process_source_conversion_flow_coeff_from_source(
403
+ source: "InputSource",
404
+ process_input_flows: pl.DataFrame | None,
405
+ ) -> tuple[pl.DataFrame | None, Param | None]:
406
+ """``p_process_source_conversion_flow_coeff`` for indirect units.
407
+
408
+ Returns ``(zero_pairs, coef_param)`` mirroring
409
+ ``input.py:_load_indirect``'s contract (lines 950-978). The
410
+ caller anti-joins ``zero_pairs`` from its inputs set and assigns
411
+ ``coef_param`` to ``flex_data.p_process_source_conversion_flow_coeff``.
412
+
413
+ *process_input_flows* — the caller's surviving (p, source) set
414
+ (post upstream zero-drop / classifier filter). Empty / None →
415
+ return ``(None, None)``.
416
+ """
417
+ return _flow_coef_from_source(
418
+ source, "unit__inputNode", "source", process_input_flows)
419
+
420
+
421
+ def p_process_sink_conversion_flow_coeff_from_source(
422
+ source: "InputSource",
423
+ process_output_flows: pl.DataFrame | None,
424
+ ) -> tuple[pl.DataFrame | None, Param | None]:
425
+ """``p_process_sink_conversion_flow_coeff`` for indirect units.
426
+
427
+ Symmetric counterpart to
428
+ :func:`p_process_source_conversion_flow_coeff_from_source` — see
429
+ that helper's docstring for the contract.
430
+ """
431
+ return _flow_coef_from_source(
432
+ source, "unit__outputNode", "sink", process_output_flows)