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,543 @@
1
+ """Cell formatting constants and helper functions for the Excel export.
2
+
3
+ Provides fill colours, fonts, and per-layout formatting functions that match
4
+ the existing FlexTool Excel template. All theme colours reference the Office
5
+ default colour theme shipped with openpyxl workbooks.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from openpyxl.styles import Font, PatternFill
11
+ from openpyxl.styles.colors import Color
12
+ from openpyxl.utils import get_column_letter
13
+ from openpyxl.worksheet.worksheet import Worksheet
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Colour / font constants
17
+ # ---------------------------------------------------------------------------
18
+
19
+ # Header row (row 2 in constant/periodic sheets) fills
20
+ FILL_ALT_HEADER = PatternFill(
21
+ patternType="solid", fgColor=Color(theme=9, tint=0.3999755851924192)
22
+ )
23
+ FILL_ENTITY_HEADER = PatternFill(
24
+ patternType="solid", fgColor=Color(theme=7, tint=0.5999938962981048)
25
+ )
26
+ FILL_PARAM_HEADER = PatternFill(
27
+ patternType="solid", fgColor=Color(theme=4, tint=0.5999938962981048)
28
+ )
29
+
30
+ # Data row fills (rows 3+ in constant/periodic sheets)
31
+ FILL_ALT_DATA = PatternFill(
32
+ patternType="solid", fgColor=Color(theme=9, tint=0.5999938962981048)
33
+ )
34
+ FILL_ENTITY_DATA = PatternFill(
35
+ patternType="solid", fgColor=Color(theme=7, tint=0.7999816888943144)
36
+ )
37
+
38
+ # Timeseries-specific fills
39
+ FILL_TIME_HEADER = PatternFill(
40
+ patternType="solid", fgColor=Color(theme=0, tint=-0.249977111117893)
41
+ )
42
+ FILL_TIME_DATA = PatternFill(
43
+ patternType="solid", fgColor=Color(theme=0, tint=-0.1499984740745262)
44
+ )
45
+
46
+ # Description/data-type label cells (row 1-2, definition column) – medium grey, black font
47
+ FILL_DESC_ROW = PatternFill(
48
+ patternType="solid", fgColor=Color(theme=0, tint=-0.3499862666707358)
49
+ )
50
+ FONT_DESC_ROW = Font(color=Color(theme=1, tint=0.0))
51
+
52
+ # Description/data-type data cells (row 1-2, parameter columns) – lighter grey, black font
53
+ FILL_DESC_DATA = PatternFill(
54
+ patternType="solid", fgColor=Color(theme=0, tint=-0.249977111117893)
55
+ )
56
+ FONT_DESC_DATA = Font(color=Color(theme=1, tint=0.0))
57
+
58
+ # Definition column background (darker grey) — conveys "do not fill here"
59
+ FILL_DEF_COL = PatternFill(
60
+ patternType="solid", fgColor=Color(theme=0, tint=-0.3499862666707358)
61
+ )
62
+
63
+ # 'navigate' label in row 1 of each sheet (blue link colour)
64
+ FONT_NAVIGATE_LINK = Font(color=Color(theme=10, tint=0.0))
65
+
66
+ # Parameter *label* cell (e.g. the word "parameter" in timeseries sheets)
67
+ FILL_PARAM_LABEL = PatternFill(
68
+ patternType="solid", fgColor=Color(theme=4, tint=0.3999755851924192)
69
+ )
70
+
71
+ # Parameter data columns — slightly darker grey than default white
72
+ FILL_PARAM_DATA = PatternFill(
73
+ patternType="solid", fgColor=Color(theme=0, tint=-0.0499893185216834)
74
+ )
75
+
76
+ # Index dimension columns (period, time, constraint) — light green
77
+ FILL_INDEX_HEADER = PatternFill(
78
+ patternType="solid", fgColor="C6EFCE"
79
+ )
80
+ FILL_INDEX_DATA = PatternFill(
81
+ patternType="solid", fgColor="E2EFDA"
82
+ )
83
+
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Formatting helpers
87
+ # ---------------------------------------------------------------------------
88
+
89
+
90
+ def format_constant_sheet(
91
+ ws: Worksheet,
92
+ n_entity_cols: int,
93
+ n_extra_cols: int = 0,
94
+ has_entity_alt: bool = True,
95
+ ) -> None:
96
+ """Apply formatting to a constant-layout sheet.
97
+
98
+ Column layout (1-based):
99
+ 1 alternative
100
+ 2 .. 1+n_entity_cols entity dimension columns
101
+ next n_extra_cols extra columns (e.g. left_node, right_node)
102
+ remaining parameter columns (+ optional Entity Alternative)
103
+
104
+ Row layout:
105
+ 1 description / navigate
106
+ 2 headers
107
+ 3+ data
108
+ """
109
+ max_row = ws.max_row
110
+ max_col = ws.max_column
111
+
112
+ 1 + n_entity_cols + n_extra_cols + 1 # 1-based
113
+
114
+ for col in range(1, max_col + 1):
115
+ # --- Row 1: description row ---
116
+ cell_r1 = ws.cell(row=1, column=col)
117
+ if col == 1:
118
+ # 'navigate' cell gets link font, no dark fill
119
+ pass
120
+ elif cell_r1.value is not None:
121
+ cell_r1.fill = FILL_DESC_ROW
122
+ cell_r1.font = FONT_DESC_ROW
123
+
124
+ # --- Row 2: header row ---
125
+ cell_r2 = ws.cell(row=2, column=col)
126
+ if col == 1:
127
+ cell_r2.fill = FILL_ALT_HEADER
128
+ elif col <= 1 + n_entity_cols + n_extra_cols:
129
+ cell_r2.fill = FILL_ENTITY_HEADER
130
+ else:
131
+ cell_r2.fill = FILL_PARAM_HEADER
132
+
133
+ # --- Rows 3+: data rows ---
134
+ for row in range(3, max_row + 1):
135
+ cell = ws.cell(row=row, column=col)
136
+ if col == 1:
137
+ cell.fill = FILL_ALT_DATA
138
+ elif col <= 1 + n_entity_cols + n_extra_cols:
139
+ cell.fill = FILL_ENTITY_DATA
140
+ # parameter data cells: no fill (default white)
141
+
142
+
143
+ def format_periodic_sheet(
144
+ ws: Worksheet,
145
+ n_entity_cols: int,
146
+ n_extra_cols: int = 0,
147
+ ) -> None:
148
+ """Apply formatting to a periodic-layout sheet.
149
+
150
+ Same as constant but:
151
+ - The period / index column after entity columns gets entity colouring.
152
+ - No Entity Alternative column.
153
+
154
+ Column layout (1-based):
155
+ 1 alternative
156
+ 2 .. 1+n_entity_cols entity dimension columns
157
+ next n_extra_cols extra columns
158
+ remaining parameter columns
159
+ """
160
+ max_row = ws.max_row
161
+ max_col = ws.max_column
162
+
163
+ entity_end_col = 1 + n_entity_cols + n_extra_cols # last entity-ish column
164
+
165
+ for col in range(1, max_col + 1):
166
+ # --- Row 1: description row ---
167
+ cell_r1 = ws.cell(row=1, column=col)
168
+ if col == 1:
169
+ pass # navigate link
170
+ elif cell_r1.value is not None:
171
+ cell_r1.fill = FILL_DESC_ROW
172
+ cell_r1.font = FONT_DESC_ROW
173
+
174
+ # --- Row 2: header row ---
175
+ cell_r2 = ws.cell(row=2, column=col)
176
+ if col == 1:
177
+ cell_r2.fill = FILL_ALT_HEADER
178
+ elif col <= entity_end_col:
179
+ cell_r2.fill = FILL_ENTITY_HEADER
180
+ else:
181
+ cell_r2.fill = FILL_PARAM_HEADER
182
+
183
+ # --- Rows 3+: data rows ---
184
+ for row in range(3, max_row + 1):
185
+ cell = ws.cell(row=row, column=col)
186
+ if col == 1:
187
+ cell.fill = FILL_ALT_DATA
188
+ elif col <= entity_end_col:
189
+ cell.fill = FILL_ENTITY_DATA
190
+ # parameter data cells: no fill
191
+
192
+
193
+ def format_timeseries_sheet(
194
+ ws: Worksheet,
195
+ n_header_rows: int,
196
+ ) -> None:
197
+ """Apply formatting to a transposed timeseries-layout sheet.
198
+
199
+ Row layout (1-based):
200
+ 1 alternative row (B='alternative', C+= alt values)
201
+ 2 parameter row (B='parameter', C+= param names)
202
+ 3 .. n_header_rows-1 entity dim rows (B=dim name, C+= entity names)
203
+ n_header_rows last entity/time (A='time', B=dim, C+= entity names)
204
+ n_header_rows+1 .. data rows (A=time values, C+= numeric data)
205
+
206
+ Column layout:
207
+ A navigate / empty / 'time' label / time values
208
+ B row labels ('alternative', 'parameter', entity dim names)
209
+ C+ data columns
210
+ """
211
+ max_row = ws.max_row
212
+ max_col = ws.max_column
213
+
214
+ for row in range(1, max_row + 1):
215
+ for col in range(1, max_col + 1):
216
+ cell = ws.cell(row=row, column=col)
217
+
218
+ if col == 1:
219
+ # Column A
220
+ if row == 1:
221
+ # navigate cell – handled by add_navigate_link
222
+ pass
223
+ elif row < n_header_rows:
224
+ # empty cells above the time row
225
+ pass
226
+ elif row == n_header_rows:
227
+ cell.fill = FILL_TIME_HEADER
228
+ else:
229
+ # time data values
230
+ cell.fill = FILL_TIME_DATA
231
+
232
+ elif col == 2:
233
+ # Column B – row labels
234
+ if row == 1:
235
+ cell.fill = FILL_ALT_HEADER
236
+ elif row == 2:
237
+ cell.fill = FILL_PARAM_LABEL
238
+ elif row <= n_header_rows:
239
+ cell.fill = FILL_ENTITY_HEADER
240
+ # data rows in col B: no fill
241
+
242
+ else:
243
+ # Columns C+ – data columns
244
+ if row == 1:
245
+ cell.fill = FILL_ALT_DATA
246
+ elif row == 2:
247
+ cell.fill = FILL_PARAM_HEADER
248
+ elif row <= n_header_rows:
249
+ cell.fill = FILL_ENTITY_DATA
250
+ # data rows: no fill
251
+
252
+
253
+ def format_link_sheet(ws: Worksheet) -> None:
254
+ """Apply formatting to a link-only (relationship) sheet.
255
+
256
+ Row 1: entity headers. Rows 2+: entity data.
257
+ """
258
+ max_row = ws.max_row
259
+ max_col = ws.max_column
260
+
261
+ for col in range(1, max_col + 1):
262
+ ws.cell(row=1, column=col).fill = FILL_ENTITY_HEADER
263
+ for row in range(2, max_row + 1):
264
+ ws.cell(row=row, column=col).fill = FILL_ENTITY_DATA
265
+
266
+
267
+ def add_navigate_link(ws: Worksheet) -> None:
268
+ """Write a 'navigate' hyperlink in cell A1."""
269
+ cell = ws.cell(row=1, column=1)
270
+ cell.value = "navigate"
271
+ cell.hyperlink = "#navigate!A1"
272
+ cell.font = FONT_NAVIGATE_LINK
273
+
274
+
275
+ # ---------------------------------------------------------------------------
276
+ # v2 format formatting helpers
277
+ # ---------------------------------------------------------------------------
278
+
279
+
280
+ def format_constant_sheet_v2(
281
+ ws: Worksheet,
282
+ n_entity_cols: int,
283
+ n_extra_cols: int = 0,
284
+ def_col: int = 3,
285
+ index_cols: set[int] | None = None,
286
+ last_data_col: int = 0,
287
+ def_row: int = 3,
288
+ ) -> None:
289
+ """Apply formatting to a v2 constant-layout sheet.
290
+
291
+ Row layout (1-based):
292
+ 1 .. def_row-1 metadata rows (description, data type, optional
293
+ default — same dark/light fill pair regardless
294
+ of which slot they occupy)
295
+ def_row definition row (alternative, entity: X, parameter,
296
+ param names)
297
+ def_row+1 .. data
298
+
299
+ Column layout (1-based):
300
+ 1 alternative
301
+ 2 .. 1+n_entity_cols entity dimension columns
302
+ next n_extra_cols extra columns (direction, index, etc.)
303
+ def_col definition column (description/data type/parameter)
304
+ def_col+1 .. parameter columns
305
+
306
+ Args:
307
+ index_cols: 1-based column numbers that are index dimensions
308
+ (period, time, constraint) — these get a light green fill
309
+ instead of entity fill.
310
+ def_row: Row of the definition / parameter-name header. Each
311
+ metadata row above it (description, data type, optionally
312
+ default) shares the same fill pair.
313
+ """
314
+ if index_cols is None:
315
+ index_cols = set()
316
+
317
+ max_row = ws.max_row
318
+ max_col = last_data_col if last_data_col > 0 else ws.max_column
319
+
320
+ for col in range(1, max_col + 1):
321
+ is_index = col in index_cols
322
+
323
+ # --- Metadata rows: 1..(def_row-1) ---
324
+ # Each one paints a label cell at def_col and data cells from
325
+ # def_col+1 onward. Row 1 column 1 is reserved for the navigate
326
+ # hyperlink and stays unfilled.
327
+ for meta_row in range(1, def_row):
328
+ cell = ws.cell(row=meta_row, column=col)
329
+ if col == 1 and meta_row == 1:
330
+ continue # navigate link
331
+ if col == def_col and cell.value is not None:
332
+ cell.fill = FILL_DESC_ROW
333
+ cell.font = FONT_DESC_ROW
334
+ elif col > def_col and cell.value is not None:
335
+ cell.fill = FILL_DESC_DATA
336
+ cell.font = FONT_DESC_DATA
337
+
338
+ # --- Definition row: def_row ---
339
+ cell_def = ws.cell(row=def_row, column=col)
340
+ if col == 1:
341
+ cell_def.fill = FILL_ALT_HEADER
342
+ elif is_index:
343
+ cell_def.fill = FILL_INDEX_HEADER
344
+ elif col < def_col:
345
+ cell_def.fill = FILL_ENTITY_HEADER
346
+ elif col == def_col:
347
+ cell_def.fill = FILL_PARAM_LABEL
348
+ else:
349
+ cell_def.fill = FILL_PARAM_HEADER
350
+
351
+ # --- Data rows: from def_row+1 onward ---
352
+ # Conditional formatting paints a 100-row buffer past the actual data.
353
+ data_start = def_row + 1
354
+ if max_row >= data_start:
355
+ from openpyxl.formatting.rule import CellIsRule
356
+
357
+ _always = ['"§§§NEVER§§§"']
358
+ data_range_end = max_row + 100
359
+
360
+ for col in range(1, (last_data_col if last_data_col > 0 else max_col) + 1):
361
+ is_index = col in index_cols
362
+ cl = get_column_letter(col)
363
+ rng = f"{cl}{data_start}:{cl}{data_range_end}"
364
+
365
+ if col == 1:
366
+ fill = FILL_ALT_DATA
367
+ elif is_index:
368
+ fill = FILL_INDEX_DATA
369
+ elif col == def_col:
370
+ fill = FILL_DEF_COL
371
+ elif col < def_col:
372
+ fill = FILL_ENTITY_DATA
373
+ elif col > def_col:
374
+ fill = FILL_PARAM_DATA
375
+ else:
376
+ continue
377
+
378
+ ws.conditional_formatting.add(
379
+ rng, CellIsRule(operator="notEqual", formula=_always, fill=fill)
380
+ )
381
+
382
+
383
+ def format_periodic_sheet_v2(
384
+ ws: Worksheet,
385
+ n_entity_cols: int,
386
+ n_extra_cols: int = 0,
387
+ def_col: int = 3,
388
+ index_cols: set[int] | None = None,
389
+ last_data_col: int = 0,
390
+ def_row: int = 3,
391
+ ) -> None:
392
+ """Apply formatting to a v2 periodic-layout sheet.
393
+
394
+ Same structure as format_constant_sheet_v2 but for periodic sheets.
395
+ """
396
+ format_constant_sheet_v2(
397
+ ws, n_entity_cols, n_extra_cols, def_col, index_cols,
398
+ last_data_col, def_row=def_row,
399
+ )
400
+
401
+
402
+ def format_timeseries_sheet_v2(
403
+ ws: Worksheet,
404
+ n_header_rows: int,
405
+ single_param: bool = False,
406
+ row_types: dict[int, str] | None = None,
407
+ last_data_col: int = 0,
408
+ ) -> None:
409
+ """Apply formatting to a v2 transposed timeseries-layout sheet.
410
+
411
+ Formatting is based on the *row_types* dict which maps 1-based row
412
+ numbers to their semantic type: 'description', 'data_type', 'alternative',
413
+ 'parameter', 'entity', 'param_info' (grey single-param row).
414
+
415
+ If *row_types* is not provided, it is inferred from *single_param*.
416
+ """
417
+ max_col = ws.max_column
418
+
419
+ if row_types is None:
420
+ row_types = {}
421
+ if single_param:
422
+ row_types = {1: "param_info", 2: "alternative", 3: "entity"}
423
+ else:
424
+ row_types = {1: "alternative", 2: "parameter", 3: "entity"}
425
+
426
+ # Map row type to (label_fill, data_fill) for col 2 and cols 3+
427
+ type_fills = {
428
+ "description": (FILL_DESC_ROW, FILL_DESC_DATA, FONT_DESC_ROW, FONT_DESC_DATA),
429
+ "data_type": (FILL_DESC_ROW, FILL_DESC_DATA, FONT_DESC_ROW, FONT_DESC_DATA),
430
+ "default": (FILL_DESC_ROW, FILL_DESC_DATA, FONT_DESC_ROW, FONT_DESC_DATA),
431
+ "alternative": (FILL_ALT_HEADER, FILL_ALT_DATA, None, None),
432
+ "parameter": (FILL_PARAM_LABEL, FILL_PARAM_HEADER, None, None),
433
+ "entity": (FILL_ENTITY_HEADER, FILL_ENTITY_DATA, None, None),
434
+ "param_info": (FILL_DEF_COL, FILL_DEF_COL, None, None),
435
+ }
436
+
437
+ # Format header rows (per-cell — only a few rows, only data columns)
438
+ header_col_end = last_data_col if last_data_col > 0 else max_col
439
+ for row in range(1, n_header_rows + 1):
440
+ rtype = row_types.get(row)
441
+
442
+ for col in range(1, header_col_end + 1):
443
+ cell = ws.cell(row=row, column=col)
444
+
445
+ if col == 1:
446
+ if row == n_header_rows:
447
+ cell.fill = FILL_INDEX_HEADER
448
+ # else: navigate link or empty — no fill
449
+
450
+ elif rtype in type_fills:
451
+ label_fill, data_fill, label_font, data_font = type_fills[rtype]
452
+ if col == 2:
453
+ cell.fill = label_fill
454
+ if label_font:
455
+ cell.font = label_font
456
+ else:
457
+ cell.fill = data_fill
458
+ if data_font:
459
+ cell.font = data_font
460
+
461
+ # Note: Column A data fill is NOT applied here — each writer adds its own
462
+ # (FILL_TIME_DATA for timeseries, FILL_INDEX_DATA for array sheets).
463
+
464
+
465
+ def format_link_sheet_v2(ws: Worksheet) -> None:
466
+ """Apply formatting to a v2 link-only (relationship) sheet.
467
+
468
+ Row 1: navigate
469
+ Row 2: entity definition row (entity: X, Y | dim1 | dim2)
470
+ Rows 3+: entity data
471
+ """
472
+ max_row = ws.max_row
473
+ max_col = ws.max_column
474
+
475
+ for col in range(1, max_col + 1):
476
+ # Row 2: entity header row
477
+ ws.cell(row=2, column=col).fill = FILL_ENTITY_HEADER
478
+ # Rows 3+: entity data
479
+ for row in range(3, max_row + 1):
480
+ ws.cell(row=row, column=col).fill = FILL_ENTITY_DATA
481
+
482
+
483
+ def auto_column_width(
484
+ ws: Worksheet,
485
+ min_param_width: float = 10,
486
+ non_param_width: float = 22,
487
+ def_col_width: float = 11,
488
+ index_col_width: float = 12,
489
+ max_width: float = 40,
490
+ header_row: int = 0,
491
+ def_col: int = 0,
492
+ index_cols: set[int] | None = None,
493
+ last_data_col: int = 0,
494
+ ) -> None:
495
+ """Size columns based on their role and header content.
496
+
497
+ Args:
498
+ min_param_width: Minimum width for parameter columns (right of def_col).
499
+ non_param_width: Width for structural columns (alternative, entity).
500
+ def_col_width: Width for the definition column.
501
+ index_col_width: Width for index columns (period, time, constraint).
502
+ max_width: Maximum column width cap.
503
+ header_row: Row containing parameter names for sizing. When 0,
504
+ scan all rows (for navigate, scenario, etc.).
505
+ def_col: 1-based column of the definition column (the boundary).
506
+ When 0, all columns use content-based sizing.
507
+ index_cols: 1-based column numbers that are index dimensions.
508
+ last_data_col: Last column of the data area (1-based). Columns
509
+ beyond this are not sized (e.g. convenience/reference sections
510
+ set their own widths). When 0, size all columns.
511
+ """
512
+ if index_cols is None:
513
+ index_cols = set()
514
+
515
+ end_col = last_data_col if last_data_col > 0 else (ws.max_column or 0)
516
+ for col_idx in range(1, end_col + 1):
517
+ if def_col > 0 and col_idx in index_cols:
518
+ width = index_col_width
519
+ elif def_col > 0 and col_idx < def_col:
520
+ width = non_param_width
521
+ elif def_col > 0 and col_idx == def_col:
522
+ width = def_col_width
523
+ elif def_col > 0 and header_row > 0:
524
+ # Parameter column — size to header row text
525
+ header_val = ws.cell(row=header_row, column=col_idx).value
526
+ if header_val is not None:
527
+ header_len = len(str(header_val)) + 1
528
+ else:
529
+ header_len = 0
530
+ width = max(min_param_width, min(header_len, max_width))
531
+ else:
532
+ # No def_col (navigate, scenario, etc.) — scan all rows
533
+ longest = non_param_width
534
+ for row_idx in range(1, min((ws.max_row or 0) + 1, 200)):
535
+ value = ws.cell(row=row_idx, column=col_idx).value
536
+ if value is not None:
537
+ length = len(str(value)) + 1
538
+ if length > longest:
539
+ longest = length
540
+ width = min(longest, max_width)
541
+
542
+ col_letter = get_column_letter(col_idx)
543
+ ws.column_dimensions[col_letter].width = width