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,584 @@
1
+ """Layer 2 (semantic per-type scaling) family registries.
2
+
3
+ This module declares, for every variable and constraint family FlexTool
4
+ emits, the :class:`QuantityType` that determines its Layer-2 scaling
5
+ group.
6
+
7
+ The registry is populated by walking every ``add_var(...)`` and
8
+ ``add_cstr(...)`` call site in ``flextool/engine_polars/`` (see comments
9
+ on each entry). An unregistered name raises :class:`KeyError` at
10
+ lookup time — Layer 2 refuses to silently default, on principle (see
11
+ ``feedback_no_shortcuts`` user memory).
12
+
13
+ Per :class:`CstrFamily` semantics:
14
+
15
+ * ``rhs_type=None`` is a sentinel for *user-supplied composite-LHS
16
+ constraints* (the ``process_constraint_*`` / ``node_balance_fix_*``
17
+ family) and the small number of structural rows whose RHS is
18
+ identically zero (``dc_flow_eq``, ``fix_v_invest_no_investment_eq``)
19
+ — Layer 2 has no per-row scaling to apply there. The column scaler
20
+ on each LHS variable still propagates through these rows.
21
+ * ``member_class_resolver="group_capacity"`` is the trigger for
22
+ :func:`resolve_group_capacity_type` — the constraint name itself is
23
+ ``[MW or MWh]`` ambiguous (group invest / divest / cumulative
24
+ capacity); apply-time inspects the constraint suffix to pick POWER
25
+ vs ENERGY.
26
+ """
27
+ from __future__ import annotations
28
+
29
+ from dataclasses import dataclass
30
+ from typing import Optional
31
+
32
+ from ._quantity_types import QuantityType
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class VarFamily:
37
+ """Per-variable Layer-2 metadata.
38
+
39
+ Attributes
40
+ ----------
41
+ column_type:
42
+ :class:`QuantityType` of one unit of the variable as it appears
43
+ in the LP **column** (before any per-row coefficient
44
+ multiplication). Layer 2 uses this to pick the column scaler.
45
+ multiplier_param:
46
+ Name of the parameter (e.g. ``"p_unitsize"``) whose magnitude
47
+ the column scaler is *expected* to absorb so the matrix entry
48
+ ``A_{ij} = multiplier * coef`` lands near 1. ``None`` if no
49
+ such single dominant multiplier applies. Informational; the
50
+ Layer-2 bucketing in :func:`_layer2.bucket_coefficients`
51
+ derives factors from observed magnitudes, not from this hint.
52
+ """
53
+
54
+ column_type: QuantityType
55
+ multiplier_param: Optional[str] = None
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class CstrFamily:
60
+ """Per-constraint Layer-2 metadata.
61
+
62
+ Attributes
63
+ ----------
64
+ rhs_type:
65
+ :class:`QuantityType` of the RHS / row-bound vector for this
66
+ family. Layer 2 uses this to pick the row scaler. ``None``
67
+ explicitly opts out of per-row scaling (user-defined
68
+ composite-LHS constraints; zero-RHS structural rows).
69
+ member_class_resolver:
70
+ When set to ``"group_capacity"``, Layer 2 ignores ``rhs_type``
71
+ and calls :func:`resolve_group_capacity_type` with the
72
+ constraint's suffix (``_p`` / ``_n``) to choose POWER vs ENERGY
73
+ at apply time. Used by the
74
+ ``maxInvestGroup_entity_*`` / ``maxDivestGroup_entity_*`` /
75
+ ``maxInvest_entity_total*`` / ``maxDivest_entity_total*`` /
76
+ ``maxCumulative_capacity`` / ``minCumulative_capacity`` /
77
+ ``minInvest_entity_total*`` / ``minDivest_entity_total*``
78
+ families that carry ``[MW or MWh]`` group-capacity RHS params.
79
+ """
80
+
81
+ rhs_type: Optional[QuantityType]
82
+ member_class_resolver: Optional[str] = None
83
+
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Variable families.
87
+ #
88
+ # The Layer-2 ``column_type`` is the type of one unit of the LP column
89
+ # variable itself. Matrix-entry physical units are
90
+ # ``column_type × multiplier_param`` (e.g. v_flow's column is
91
+ # DIMENSIONLESS; its row coefficient is ``p_unitsize`` → POWER on the
92
+ # row). Bucketing in Layer 2 uses the *effective* type per (row, col)
93
+ # pair, not the column type alone.
94
+
95
+ VARIABLE_FAMILIES: dict[str, VarFamily] = {
96
+ # ── Process / connection flow (dispatched per timestep).
97
+ # flextool/engine_polars/model.py:482
98
+ "v_flow": VarFamily(QuantityType.DIMENSIONLESS, multiplier_param="p_unitsize"),
99
+
100
+ # ── Storage / nodeBalance slacks. Carry ENERGY directly (no
101
+ # unitsize multiplier on the column).
102
+ # flextool/engine_polars/model.py:484
103
+ "vq_state_up": VarFamily(QuantityType.ENERGY),
104
+ # flextool/engine_polars/model.py:485
105
+ "vq_state_down": VarFamily(QuantityType.ENERGY),
106
+
107
+ # ── Storage state (per-step, per-block). ENERGY column.
108
+ # flextool/engine_polars/model.py:490
109
+ "v_state": VarFamily(QuantityType.ENERGY),
110
+ # flextool/engine_polars/model.py:502
111
+ "v_state_inter": VarFamily(QuantityType.ENERGY),
112
+ # flextool/engine_polars/model.py:505
113
+ "v_state_rp_start": VarFamily(QuantityType.ENERGY),
114
+
115
+ # ── Unit-commitment families. Column counts a number of units
116
+ # online / starting / shutting; DIMENSIONLESS. The matrix coef
117
+ # ``p_unitsize`` (or ``v_unitsize`` from process_existing_count)
118
+ # turns the row into POWER as needed. Integer variants are
119
+ # mathematically the same column type.
120
+ # flextool/engine_polars/model.py:511
121
+ "v_online_linear": VarFamily(QuantityType.DIMENSIONLESS, multiplier_param="p_unitsize"),
122
+ # flextool/engine_polars/model.py:512
123
+ "v_startup_linear": VarFamily(QuantityType.DIMENSIONLESS, multiplier_param="p_unitsize"),
124
+ # flextool/engine_polars/model.py:513
125
+ "v_shutdown_linear": VarFamily(QuantityType.DIMENSIONLESS, multiplier_param="p_unitsize"),
126
+ # flextool/engine_polars/model.py:518
127
+ "v_online_integer": VarFamily(QuantityType.DIMENSIONLESS, multiplier_param="p_unitsize"),
128
+ # flextool/engine_polars/model.py:520
129
+ "v_startup_integer": VarFamily(QuantityType.DIMENSIONLESS, multiplier_param="p_unitsize"),
130
+ # flextool/engine_polars/model.py:521
131
+ "v_shutdown_integer": VarFamily(QuantityType.DIMENSIONLESS, multiplier_param="p_unitsize"),
132
+
133
+ # ── Investment / divestment counts. DIMENSIONLESS column whose
134
+ # matrix coefficient ``p_unitsize`` (process / connection) or
135
+ # ``p_state_unitsize`` (node) sets the row's effective POWER /
136
+ # ENERGY type.
137
+ # flextool/engine_polars/model.py:523
138
+ "v_invest_p": VarFamily(QuantityType.DIMENSIONLESS, multiplier_param="p_unitsize"),
139
+ # flextool/engine_polars/model.py:525
140
+ "v_divest_p": VarFamily(QuantityType.DIMENSIONLESS, multiplier_param="p_unitsize"),
141
+ # flextool/engine_polars/model.py:527
142
+ "v_invest_n": VarFamily(QuantityType.DIMENSIONLESS, multiplier_param="p_state_unitsize"),
143
+ # flextool/engine_polars/model.py:529
144
+ "v_divest_n": VarFamily(QuantityType.DIMENSIONLESS, multiplier_param="p_state_unitsize"),
145
+
146
+ # ── Reserve. DIMENSIONLESS column; ``p_unitsize`` multiplier
147
+ # carries the per-row POWER coefficient.
148
+ # flextool/engine_polars/_reserve.py:275
149
+ "v_reserve": VarFamily(QuantityType.DIMENSIONLESS, multiplier_param="p_unitsize"),
150
+ # flextool/engine_polars/_reserve.py:282
151
+ "vq_reserve": VarFamily(QuantityType.POWER),
152
+
153
+ # ── Commodity ladder. v_trade is DIMENSIONLESS; row coef
154
+ # ``unitsize`` turns it into ENERGY for the ladder caps and the
155
+ # balance row.
156
+ # flextool/engine_polars/_commodity_ladder.py:399
157
+ "v_trade": VarFamily(QuantityType.DIMENSIONLESS, multiplier_param="unitsize"),
158
+
159
+ # ── Group-level slacks. Direct physical units.
160
+ # flextool/engine_polars/_group_slack.py:522
161
+ "vq_capacity_margin": VarFamily(QuantityType.POWER),
162
+ # flextool/engine_polars/_group_slack.py:902
163
+ "vq_inertia": VarFamily(QuantityType.INERTIA),
164
+ # flextool/engine_polars/_group_slack.py:1045
165
+ "vq_non_synchronous": VarFamily(QuantityType.POWER),
166
+
167
+ # ── DC power flow.
168
+ # ``v_angle`` (radians). Dimensionless in our taxonomy.
169
+ # flextool/engine_polars/_dc_power_flow.py:250
170
+ "v_angle": VarFamily(QuantityType.DIMENSIONLESS),
171
+ # ``v_flow_back`` mirrors v_flow on the reverse direction.
172
+ # flextool/engine_polars/_dc_power_flow.py:296
173
+ "v_flow_back": VarFamily(QuantityType.DIMENSIONLESS, multiplier_param="p_unitsize"),
174
+
175
+ # ── Benders (Option C) hand-built master families. The hand master
176
+ # (``master="hand"``) is built AUTOSCALE-OFF, so Layer 2 never scales
177
+ # these columns; registering them keeps the registry-coverage invariant
178
+ # complete (CLAUDE.md #1).
179
+ # ``f`` = cross-region trade flow, in the SAME unitsize-normalised units
180
+ # as the region half-flow ``v_flow`` ⇒ same family as v_flow.
181
+ # flextool/engine_polars/_benders.py:483
182
+ "f": VarFamily(QuantityType.DIMENSIONLESS, multiplier_param="p_unitsize"),
183
+ # ``C`` = invested trade-connection capacity (normalised) ⇒ same family
184
+ # as v_invest_p.
185
+ # flextool/engine_polars/_benders.py:473
186
+ "C": VarFamily(QuantityType.DIMENSIONLESS, multiplier_param="p_unitsize"),
187
+ # ``eta`` = per-region recourse cost (enters the objective at coef 1.0
188
+ # carrying the region's currency cost) ⇒ CURRENCY column.
189
+ # flextool/engine_polars/_benders.py:489
190
+ "eta": VarFamily(QuantityType.CURRENCY),
191
+ }
192
+
193
+
194
+ # ---------------------------------------------------------------------------
195
+ # Constraint families.
196
+ #
197
+ # The key is either the full constraint name (exact match) or a prefix
198
+ # (the constraint name's ``startswith(key + "_")``). Suffixed UC
199
+ # families (``maxOnline_linear`` / ``maxOnline_integer``) are matched
200
+ # by prefix; the per-family Layer-2 type is the same for both because
201
+ # they carry the same RHS structure.
202
+ #
203
+ # ``rhs_type=None`` ⇒ Layer 2 does NOT apply a per-row factor to that
204
+ # family (the rows participate in column scaling only).
205
+
206
+ # Suffix-resolved (`_p` ⇒ POWER, `_n` ⇒ ENERGY) group capacity rows
207
+ # are flagged via member_class_resolver="group_capacity".
208
+
209
+ CONSTRAINT_FAMILIES: dict[str, CstrFamily] = {
210
+ # ── Node balance (the central energy equation) — ENERGY.
211
+ # flextool/engine_polars/model.py:1336
212
+ "nodeBalance_eq": CstrFamily(QuantityType.ENERGY),
213
+ # Per-period energy balance for balance_within_period nodes — ENERGY.
214
+ # flextool/engine_polars/model.py:nodeBalancePeriod_eq
215
+ "nodeBalancePeriod_eq": CstrFamily(QuantityType.ENERGY),
216
+ # flextool/engine_polars/model.py:1792
217
+ "nodeBalanceBlock_eq": CstrFamily(QuantityType.ENERGY),
218
+
219
+ # ── Storage state max / state ladder. ENERGY.
220
+ # flextool/engine_polars/model.py:2788
221
+ "maxState": CstrFamily(QuantityType.ENERGY),
222
+ # flextool/engine_polars/model.py:1159
223
+ "maxState_rp_start": CstrFamily(QuantityType.ENERGY),
224
+ # flextool/engine_polars/model.py:1570
225
+ "stateConstantWithinBlock_eq": CstrFamily(QuantityType.ENERGY),
226
+ # flextool/engine_polars/model.py:905
227
+ "rp_inter_period_balance": CstrFamily(QuantityType.ENERGY),
228
+ # flextool/engine_polars/model.py:1039
229
+ "rp_inter_period_cyclic": CstrFamily(QuantityType.ENERGY),
230
+ # flextool/engine_polars/model.py:1144
231
+ "rp_inter_period_max_state": CstrFamily(QuantityType.ENERGY),
232
+ # flextool/engine_polars/model.py:2985
233
+ "storage_state_start_binding": CstrFamily(QuantityType.ENERGY),
234
+ # flextool/engine_polars/model.py:3072
235
+ "storage_state_start_binding_cyclic_period": CstrFamily(QuantityType.ENERGY),
236
+ # flextool/engine_polars/model.py:3124
237
+ "storage_state_solve_horizon_reference_value": CstrFamily(QuantityType.ENERGY),
238
+
239
+ # ── Flow caps from process side. DIMENSIONLESS (capacity coefficient).
240
+ # The cap is a fraction × existing_count; the row's effective type
241
+ # lives in the coefficient, not the RHS.
242
+ # flextool/engine_polars/model.py:2045
243
+ "maxFlow": CstrFamily(QuantityType.DIMENSIONLESS),
244
+ # flextool/engine_polars/model.py:2075
245
+ "maxFlow_negCap": CstrFamily(QuantityType.DIMENSIONLESS),
246
+ # Reverse-flow capacity cap on v_flow_back for method_2way_1var_off
247
+ # arcs (DC + non-DC). flextool/engine_polars/model.py (maxFlow_back).
248
+ "maxFlow_back": CstrFamily(QuantityType.DIMENSIONLESS),
249
+ # Legacy name for the DC-only back-flow cap; the cap is now emitted as
250
+ # ``maxFlow_back`` over the full arc set. Retained for back-compat.
251
+ "maxToSink_back": CstrFamily(QuantityType.DIMENSIONLESS),
252
+ # Ramp constraints, prefix-matched: ramp_<side>_<dir>_constraint.
253
+ # flextool/engine_polars/model.py:2164
254
+ "ramp": CstrFamily(QuantityType.DIMENSIONLESS),
255
+
256
+ # ── Investment caps — entity scope.
257
+ # flextool/engine_polars/model.py:2189
258
+ "maxInvest_var_bound": CstrFamily(QuantityType.POWER),
259
+ # flextool/engine_polars/model.py:2197
260
+ "maxDivest_var_bound": CstrFamily(QuantityType.POWER),
261
+ # flextool/engine_polars/model.py:2217
262
+ "maxInvest_var_bound_n": CstrFamily(QuantityType.ENERGY),
263
+ # flextool/engine_polars/model.py:2225
264
+ "maxDivest_var_bound_n": CstrFamily(QuantityType.ENERGY),
265
+ # flextool/engine_polars/model.py:2257
266
+ "maxInvest_entity_period_p": CstrFamily(QuantityType.POWER),
267
+ # flextool/engine_polars/model.py:2274
268
+ "maxInvest_entity_period_n": CstrFamily(QuantityType.ENERGY),
269
+ # flextool/engine_polars/model.py:2292
270
+ "maxDivest_entity_period_p": CstrFamily(QuantityType.POWER),
271
+ # flextool/engine_polars/model.py:2309
272
+ "maxDivest_entity_period_n": CstrFamily(QuantityType.ENERGY),
273
+ # flextool/engine_polars/model.py:2380
274
+ "maxInvest_entity_total": CstrFamily(QuantityType.POWER),
275
+ # flextool/engine_polars/model.py:2432
276
+ "maxInvest_entity_total_n": CstrFamily(QuantityType.ENERGY),
277
+ # flextool/engine_polars/model.py:2405
278
+ "maxDivest_entity_total": CstrFamily(QuantityType.POWER),
279
+ # flextool/engine_polars/model.py:2457
280
+ "maxDivest_entity_total_n": CstrFamily(QuantityType.ENERGY),
281
+
282
+ # min-invest / min-divest entity totals — group_capacity resolved
283
+ # via _p / _n suffix in _cumulative_invest.py.
284
+ # flextool/engine_polars/_cumulative_invest.py:456 (minInvest_entity_total_p)
285
+ # flextool/engine_polars/_cumulative_invest.py:496 (minInvest_entity_total_n)
286
+ "minInvest_entity_total": CstrFamily(
287
+ None, member_class_resolver="group_capacity"
288
+ ),
289
+ # flextool/engine_polars/_cumulative_invest.py:464 (minDivest_entity_total_p)
290
+ # flextool/engine_polars/_cumulative_invest.py:504 (minDivest_entity_total_n)
291
+ "minDivest_entity_total": CstrFamily(
292
+ None, member_class_resolver="group_capacity"
293
+ ),
294
+
295
+ # min-invest / min-divest entity period (prefix-matched _p/_n) —
296
+ # POWER for _p, ENERGY for _n.
297
+ # flextool/engine_polars/_cumulative_invest.py:381 (minInvest_entity_period_p)
298
+ # flextool/engine_polars/_cumulative_invest.py:395 (minInvest_entity_period_n)
299
+ "minInvest_entity_period": CstrFamily(
300
+ None, member_class_resolver="group_capacity"
301
+ ),
302
+ "minDivest_entity_period": CstrFamily(
303
+ None, member_class_resolver="group_capacity"
304
+ ),
305
+
306
+ # Group invest / divest variants — `[MW or MWh]` resolved at
307
+ # apply time.
308
+ # flextool/engine_polars/_cumulative_invest.py:681 (maxInvestGroup_entity_period_p)
309
+ # flextool/engine_polars/_cumulative_invest.py:701 (maxInvestGroup_entity_period_n)
310
+ "maxInvestGroup_entity_period": CstrFamily(
311
+ None, member_class_resolver="group_capacity"
312
+ ),
313
+ "minInvestGroup_entity_period": CstrFamily(
314
+ None, member_class_resolver="group_capacity"
315
+ ),
316
+ "maxDivestGroup_entity_period": CstrFamily(
317
+ None, member_class_resolver="group_capacity"
318
+ ),
319
+ "minDivestGroup_entity_period": CstrFamily(
320
+ None, member_class_resolver="group_capacity"
321
+ ),
322
+ # flextool/engine_polars/_cumulative_invest.py:786 (maxInvestGroup_entity_total_p)
323
+ # flextool/engine_polars/_cumulative_invest.py:819 (maxInvestGroup_entity_total_n)
324
+ "maxInvestGroup_entity_total": CstrFamily(
325
+ None, member_class_resolver="group_capacity"
326
+ ),
327
+ "minInvestGroup_entity_total": CstrFamily(
328
+ None, member_class_resolver="group_capacity"
329
+ ),
330
+ "maxDivestGroup_entity_total": CstrFamily(
331
+ None, member_class_resolver="group_capacity"
332
+ ),
333
+ "minDivestGroup_entity_total": CstrFamily(
334
+ None, member_class_resolver="group_capacity"
335
+ ),
336
+ # flextool/engine_polars/_cumulative_invest.py:845 (maxInvestGroup_entity_cumulative_p)
337
+ # flextool/engine_polars/_cumulative_invest.py:869 (maxInvestGroup_entity_cumulative_n)
338
+ "maxInvestGroup_entity_cumulative": CstrFamily(
339
+ None, member_class_resolver="group_capacity"
340
+ ),
341
+ "minInvestGroup_entity_cumulative": CstrFamily(
342
+ None, member_class_resolver="group_capacity"
343
+ ),
344
+
345
+ # Cumulative capacity per-entity — `_p` / `_n` suffix.
346
+ # flextool/engine_polars/_cumulative_invest.py:588 (maxCumulative_capacity_p)
347
+ # flextool/engine_polars/_cumulative_invest.py:632 (maxCumulative_capacity_n)
348
+ "maxCumulative_capacity": CstrFamily(
349
+ None, member_class_resolver="group_capacity"
350
+ ),
351
+ "minCumulative_capacity": CstrFamily(
352
+ None, member_class_resolver="group_capacity"
353
+ ),
354
+
355
+ # Group-level cumulative / period / instant flow — POWER.
356
+ # flextool/engine_polars/_cumulative_invest.py:1050 (maxCumulative_flow_solve)
357
+ # flextool/engine_polars/_cumulative_invest.py:1085 (maxCumulative_flow_period)
358
+ "maxCumulative_flow_solve": CstrFamily(QuantityType.POWER),
359
+ "minCumulative_flow_solve": CstrFamily(QuantityType.POWER),
360
+ "maxCumulative_flow_period": CstrFamily(QuantityType.POWER),
361
+ "minCumulative_flow_period": CstrFamily(QuantityType.POWER),
362
+ # flextool/engine_polars/_cumulative_invest.py:1111 (maxInstant_flow / minInstant_flow)
363
+ "maxInstant_flow": CstrFamily(QuantityType.POWER),
364
+ "minInstant_flow": CstrFamily(QuantityType.POWER),
365
+
366
+ # ── User-defined constraints: composite LHS, RHS in caller's
367
+ # coordinates. Layer 2 skips per-row scaling.
368
+ # flextool/engine_polars/model.py:2740 (process_constraint_equal / _less_than / _greater_than)
369
+ "process_constraint_equal": CstrFamily(None),
370
+ "process_constraint_less_than": CstrFamily(None),
371
+ "process_constraint_greater_than": CstrFamily(None),
372
+
373
+ # ── Node-balance fix (used by manual demand patching). Zero RHS
374
+ # in the most common case; treat as DIMENSIONLESS so the row
375
+ # coefficient is the rescaled quantity.
376
+ # flextool/engine_polars/model.py:1392
377
+ "node_balance_fix_quantity_eq_lower": CstrFamily(None),
378
+ # flextool/engine_polars/model.py:1539
379
+ "node_storage_usage_fix_le": CstrFamily(None),
380
+
381
+ # ── Conversion (efficiency-balanced flow row).
382
+ # flextool/engine_polars/model.py:2509
383
+ "conversion_indirect": CstrFamily(QuantityType.ENERGY),
384
+
385
+ # ── CO2 caps — EMISSION_MASS.
386
+ # flextool/engine_polars/model.py:2542
387
+ "co2_max_period": CstrFamily(QuantityType.EMISSION_MASS),
388
+ # flextool/engine_polars/model.py:2577
389
+ "co2_max_total": CstrFamily(QuantityType.EMISSION_MASS),
390
+
391
+ # ── Unit-commitment (online / startup / shutdown / minimum times).
392
+ # All DIMENSIONLESS — they bound counts.
393
+ # flextool/engine_polars/model.py:3732 (maxOnline_<sfx>)
394
+ "maxOnline": CstrFamily(QuantityType.DIMENSIONLESS),
395
+ # flextool/engine_polars/model.py:3735 (maxStartup_<sfx>)
396
+ "maxStartup": CstrFamily(QuantityType.DIMENSIONLESS),
397
+ # flextool/engine_polars/model.py:3738 (maxShutdown_<sfx>)
398
+ "maxShutdown": CstrFamily(QuantityType.DIMENSIONLESS),
399
+ # flextool/engine_polars/model.py:3744 (online__startup_<sfx>)
400
+ "online__startup": CstrFamily(QuantityType.DIMENSIONLESS),
401
+ # flextool/engine_polars/model.py:3748 (online__shutdown_<sfx>)
402
+ "online__shutdown": CstrFamily(QuantityType.DIMENSIONLESS),
403
+ # flextool/engine_polars/model.py:3770 (maxFlow_online_<sfx>)
404
+ "maxFlow_online": CstrFamily(QuantityType.DIMENSIONLESS),
405
+ # flextool/engine_polars/model.py:3785 (minFlow_minload_<sfx>)
406
+ "minFlow_minload": CstrFamily(QuantityType.DIMENSIONLESS),
407
+ # flextool/engine_polars/model.py:3814 (minimum_uptime_<sfx>)
408
+ "minimum_uptime": CstrFamily(QuantityType.DIMENSIONLESS),
409
+ # flextool/engine_polars/model.py:3853 (minimum_downtime_<sfx>)
410
+ "minimum_downtime": CstrFamily(QuantityType.DIMENSIONLESS),
411
+
412
+ # ── DC PF.
413
+ # flextool/engine_polars/_dc_power_flow.py:397 — RHS=0, skip.
414
+ "dc_flow_eq": CstrFamily(None),
415
+ # flextool/engine_polars/_dc_power_flow.py:340 — fixed-zero angle
416
+ # at the reference node; RHS=0, skip per-row scaling.
417
+ "dc_reference_angle_eq": CstrFamily(None),
418
+
419
+ # ── Reserve.
420
+ # flextool/engine_polars/_reserve.py:373
421
+ "reserveBalance_timeseries_eq": CstrFamily(QuantityType.POWER),
422
+ # flextool/engine_polars/_reserve.py:449
423
+ "reserveBalance_dynamic_eq": CstrFamily(QuantityType.POWER),
424
+ # flextool/engine_polars/_reserve.py:508 (reserveBalance_up_n_1_eq / _down_n_1_eq)
425
+ "reserveBalance_up_n_1_eq": CstrFamily(QuantityType.POWER),
426
+ "reserveBalance_down_n_1_eq": CstrFamily(QuantityType.POWER),
427
+ # flextool/engine_polars/_reserve.py:576 (reserve_process_upward / _downward)
428
+ "reserve_process_upward": CstrFamily(QuantityType.POWER),
429
+ "reserve_process_downward": CstrFamily(QuantityType.POWER),
430
+
431
+ # ── Group slack constraints.
432
+ # flextool/engine_polars/_group_slack.py:859
433
+ "capacityMargin": CstrFamily(QuantityType.POWER),
434
+ # flextool/engine_polars/_group_slack.py:984
435
+ "inertia_constraint": CstrFamily(QuantityType.INERTIA),
436
+ # flextool/engine_polars/_group_slack.py:1157
437
+ "non_sync_constraint": CstrFamily(QuantityType.POWER),
438
+
439
+ # ── Commodity ladder. ENERGY (MWh).
440
+ # flextool/engine_polars/_commodity_ladder.py:521
441
+ "commodity_ladder_balance": CstrFamily(QuantityType.ENERGY),
442
+ # flextool/engine_polars/_commodity_ladder.py:561 ← the H2_trade trigger
443
+ "ladder_tier_cap_annual_roll": CstrFamily(QuantityType.ENERGY),
444
+ # flextool/engine_polars/_commodity_ladder.py:607
445
+ "ladder_tier_cap_cumulative_roll": CstrFamily(QuantityType.ENERGY),
446
+
447
+ # ── Forbid-invest rows. RHS = 0 by construction; skip per-row.
448
+ # flextool/engine_polars/_cumulative_invest.py:284, 298
449
+ "fix_v_invest_no_investment_eq_p": CstrFamily(None),
450
+ "fix_v_invest_no_investment_eq_n": CstrFamily(None),
451
+
452
+ # ── Non-anticipativity (cross-branch equality). RHS = 0; skip.
453
+ # flextool/engine_polars/model.py:273, 306, 336, 364
454
+ "non_anticipativity_storage_use": CstrFamily(None),
455
+ "non_anticipativity_online_integer": CstrFamily(None),
456
+ "non_anticipativity_online_linear": CstrFamily(None),
457
+ "non_anticipativity_reserve": CstrFamily(None),
458
+
459
+ # ── Profile constraints (per timestep).
460
+ # Node profiles emit at model.py:2879 with names
461
+ # ``profile_state_upper_limit`` / ``_lower_limit`` / ``_fixed``;
462
+ # process profiles at model.py:3914 with ``profile_flow_*``. Per
463
+ # the autoscaler handoff, profile state is POWER for unit /
464
+ # connection, ENERGY for node. Node-side rows go through one
465
+ # call site (``_add_node_profile_cstr``) so we register the names
466
+ # here without a member_class_resolver — they are exclusively
467
+ # ENERGY-typed. Process-side ``profile_flow_*`` rows are POWER.
468
+ "profile_state_upper_limit": CstrFamily(QuantityType.ENERGY),
469
+ "profile_state_lower_limit": CstrFamily(QuantityType.ENERGY),
470
+ "profile_state_fixed": CstrFamily(QuantityType.ENERGY),
471
+ "profile_flow_upper_limit": CstrFamily(QuantityType.POWER),
472
+ "profile_flow_lower_limit": CstrFamily(QuantityType.POWER),
473
+ "profile_flow_fixed": CstrFamily(QuantityType.POWER),
474
+
475
+ # ── Benders (Option C) hand-built master constraints. The hand master
476
+ # is built AUTOSCALE-OFF, so Layer 2 never scales these rows; registering
477
+ # them keeps the registry-coverage invariant complete (CLAUDE.md #1).
478
+ # ``maxInvest``: C[conn] <= max_units — capacity bound, mirrors the
479
+ # entity-scope ``maxInvest_var_bound`` (POWER RHS).
480
+ # flextool/engine_polars/_benders.py:520
481
+ "maxInvest": CstrFamily(QuantityType.POWER),
482
+ # ``trade_capacity``: C[conn] - f[arc,d,t] >= 0 — the RHS is identically
483
+ # zero (no per-row scaling), so CstrFamily(None) like the other zero-RHS
484
+ # structural rows; the column scalers on C / f still propagate.
485
+ # flextool/engine_polars/_benders.py:504
486
+ "trade_capacity": CstrFamily(None),
487
+ }
488
+
489
+
490
+ def lookup_var(name: str) -> VarFamily:
491
+ """Return the :class:`VarFamily` registered for ``name``.
492
+
493
+ Raises :class:`KeyError` for any unregistered variable; Layer 2
494
+ must refuse to silently treat unknown variables as DIMENSIONLESS,
495
+ which would hide drift between this registry and the LP build.
496
+ """
497
+ return VARIABLE_FAMILIES[name]
498
+
499
+
500
+ def lookup_cstr(name: str) -> CstrFamily:
501
+ """Return the :class:`CstrFamily` registered for ``name``.
502
+
503
+ Exact match wins. Otherwise we strip a trailing ``_linear``,
504
+ ``_integer``, ``_p``, ``_n`` suffix and retry (covers
505
+ ``maxOnline_linear`` → ``maxOnline``, ``maxInvest_entity_total_n``
506
+ is already exact, ``minInvest_entity_total_p`` →
507
+ ``minInvest_entity_total``). Suffix stripping is conservative:
508
+ only the known UC and member-class suffixes are removed.
509
+
510
+ Raises :class:`KeyError` if no registration matches.
511
+ """
512
+ if name in CONSTRAINT_FAMILIES:
513
+ return CONSTRAINT_FAMILIES[name]
514
+ # Strip UC suffixes first.
515
+ for sfx in ("_linear", "_integer"):
516
+ if name.endswith(sfx):
517
+ base = name[: -len(sfx)]
518
+ if base in CONSTRAINT_FAMILIES:
519
+ return CONSTRAINT_FAMILIES[base]
520
+ # Then member-class suffix (_p / _n). This is the prefix that
521
+ # member_class_resolver entries cover. We only strip when the
522
+ # *unsuffixed* name matches an entry whose resolver is set, so a
523
+ # row that happens to end with ``_p`` but is registered with an
524
+ # explicit suffix (e.g. ``maxInvest_entity_period_p``) stays on
525
+ # the exact-match path.
526
+ for sfx in ("_p", "_n"):
527
+ if name.endswith(sfx):
528
+ base = name[: -len(sfx)]
529
+ if (base in CONSTRAINT_FAMILIES
530
+ and CONSTRAINT_FAMILIES[base].member_class_resolver):
531
+ return CONSTRAINT_FAMILIES[base]
532
+ # Prefix dispatch (the contract documented in this module's header and
533
+ # in test_registry_coverage): a registry key ``K`` matches a dynamic
534
+ # constraint name when ``name.startswith(K + "_")``. This is what the
535
+ # ``"ramp"`` entry relies on — the ramp family
536
+ # (``ramp_{side}_{dir}_constraint``, model.py:2328) carries middle and
537
+ # suffix tokens (``_sink_up_constraint``) that neither the exact-match
538
+ # nor the UC / member-class suffix-strip paths above can resolve.
539
+ # Longest matching key wins so a more specific registration is never
540
+ # shadowed by a shorter prefix. Runs last, after exact + suffix-strip,
541
+ # so it can only resolve names those paths leave unresolved — never
542
+ # change an existing resolution.
543
+ prefix_match: Optional[str] = None
544
+ for key in CONSTRAINT_FAMILIES:
545
+ if name.startswith(key + "_") and (
546
+ prefix_match is None or len(key) > len(prefix_match)):
547
+ prefix_match = key
548
+ if prefix_match is not None:
549
+ return CONSTRAINT_FAMILIES[prefix_match]
550
+ raise KeyError(name)
551
+
552
+
553
+ def resolve_cstr_rhs_type(name: str) -> Optional[QuantityType]:
554
+ """Final per-name rhs_type after suffix resolution.
555
+
556
+ For ``member_class_resolver="group_capacity"`` families, the
557
+ constraint name's ``_p`` / ``_n`` suffix selects POWER vs ENERGY
558
+ (via :func:`resolve_group_capacity_type`). Returns ``None`` for
559
+ families that opt out of per-row scaling.
560
+ """
561
+ fam = lookup_cstr(name)
562
+ if fam.member_class_resolver == "group_capacity":
563
+ if name.endswith("_p"):
564
+ return QuantityType.POWER
565
+ if name.endswith("_n"):
566
+ return QuantityType.ENERGY
567
+ # Unsuffixed (shouldn't happen for group-capacity registered
568
+ # entries) — fall through. Surface as KeyError so we don't
569
+ # silently misclassify.
570
+ raise KeyError(
571
+ f"{name!r}: group_capacity resolver requires _p / _n suffix"
572
+ )
573
+ return fam.rhs_type
574
+
575
+
576
+ __all__ = [
577
+ "VarFamily",
578
+ "CstrFamily",
579
+ "VARIABLE_FAMILIES",
580
+ "CONSTRAINT_FAMILIES",
581
+ "lookup_var",
582
+ "lookup_cstr",
583
+ "resolve_cstr_rhs_type",
584
+ ]