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
File without changes
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import signal
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ logger = logging.getLogger("flextool.gui")
9
+
10
+
11
+ def _configure_logging(log_dir: Path) -> None:
12
+ """Set up logging for the desktop application.
13
+
14
+ Adds a console handler only when a real stream exists -- under the
15
+ windowed launcher on Windows (``pythonw.exe``) ``sys.stdout`` and
16
+ ``sys.stderr`` are ``None`` -- and always adds a rotating file handler so
17
+ diagnostics survive even when there is no console to print them to.
18
+ """
19
+ from logging.handlers import RotatingFileHandler
20
+
21
+ handlers: list[logging.Handler] = []
22
+
23
+ stream = sys.stderr or sys.stdout # both None under pythonw.exe
24
+ if stream is not None:
25
+ handlers.append(logging.StreamHandler(stream))
26
+
27
+ try:
28
+ log_dir.mkdir(parents=True, exist_ok=True)
29
+ handlers.append(
30
+ RotatingFileHandler(
31
+ log_dir / "flextool_gui.log",
32
+ maxBytes=1_000_000,
33
+ backupCount=3,
34
+ encoding="utf-8",
35
+ )
36
+ )
37
+ except OSError:
38
+ # A missing log file must never stop the GUI from starting.
39
+ pass
40
+
41
+ logging.basicConfig(
42
+ level=logging.WARNING,
43
+ format="%(asctime)s %(name)s %(levelname)s: %(message)s",
44
+ handlers=handlers,
45
+ )
46
+
47
+
48
+ def _install_exception_logging() -> None:
49
+ """Route otherwise-uncaught exceptions to the log.
50
+
51
+ Without a console most users never see (let alone report) a traceback, so
52
+ funnel interpreter-level exceptions into the rotating log file configured
53
+ by :func:`_configure_logging`.
54
+ """
55
+
56
+ def _hook(exc_type, exc, tb) -> None:
57
+ if issubclass(exc_type, KeyboardInterrupt):
58
+ sys.__excepthook__(exc_type, exc, tb)
59
+ return
60
+ logger.critical("Uncaught exception", exc_info=(exc_type, exc, tb))
61
+
62
+ sys.excepthook = _hook
63
+
64
+
65
+ def main() -> None:
66
+ # Ensure projects directory exists and load global settings for theme
67
+ from flextool.gui.project_utils import get_projects_dir
68
+ from flextool.gui.settings_io import load_global_settings
69
+
70
+ projects_dir = get_projects_dir()
71
+ projects_dir.mkdir(parents=True, exist_ok=True)
72
+
73
+ # Set up logging (file fallback first, so nothing is lost without a console)
74
+ _configure_logging(projects_dir)
75
+ _install_exception_logging()
76
+
77
+ settings = load_global_settings(projects_dir)
78
+
79
+ # Create and run the main window, passing the initial theme
80
+ from flextool.gui.main_window import MainWindow
81
+
82
+ root = MainWindow(initial_theme=settings.theme)
83
+
84
+ # Tkinter prints callback exceptions to stderr by default, which is absent
85
+ # under the windowed launcher; send them to the log instead.
86
+ root.report_callback_exception = lambda exc_type, exc, tb: logger.error(
87
+ "Unhandled Tk callback exception", exc_info=(exc_type, exc, tb)
88
+ )
89
+
90
+ # Set up signal handlers for clean shutdown (SIGINT = Ctrl+C, SIGTERM = kill)
91
+ def _signal_handler(signum: int, frame: object) -> None:
92
+ """Handle SIGINT/SIGTERM by cleaning up and exiting."""
93
+ try:
94
+ if hasattr(root, "execution_mgr") and root.execution_mgr:
95
+ root.execution_mgr.cleanup()
96
+ except Exception:
97
+ pass
98
+ try:
99
+ root.destroy()
100
+ except Exception:
101
+ pass
102
+ sys.exit(0)
103
+
104
+ signal.signal(signal.SIGINT, _signal_handler)
105
+ signal.signal(signal.SIGTERM, _signal_handler)
106
+
107
+ # Periodically yield control from tkinter's mainloop so Python can
108
+ # process pending signal handlers (tkinter blocks them on some platforms).
109
+ def _check_signals() -> None:
110
+ root.after(500, _check_signals)
111
+
112
+ root.after(500, _check_signals)
113
+
114
+ root.mainloop()
115
+
116
+
117
+ if __name__ == "__main__":
118
+ main()
@@ -0,0 +1,184 @@
1
+ """Single-source argv builders for the RP-preprocess and calibrate CLIs.
2
+
3
+ These pure functions are the ONE renderer per command. Both the
4
+ ExecutionManager auxiliary-job worker (which runs the subprocess with the
5
+ raw argv list) and the dialog's "Copy CLI command" button (which renders
6
+ the same argv via :func:`command_to_display_string`) go through here, so
7
+ the copied text can never drift from what is actually executed.
8
+
9
+ Targets:
10
+ * ``python -m flextool.representative_periods.preprocess <db_url>
11
+ <scenario> <n_rp> <period_length> [flags]``
12
+ (see ``flextool/representative_periods/preprocess.py::main``)
13
+ * ``python -m flextool.calibrate <db> <scenario> [flags]``
14
+ (see ``flextool/calibrate/_cli.py::build_parser``)
15
+
16
+ Region-scope RP args are intentionally NOT emitted here (out of GUI scope).
17
+ Calibrate's ``--slack-threshold`` is intentionally omitted so the
18
+ calibrator uses its own default.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import shlex
23
+ from collections.abc import Sequence
24
+
25
+ RP_MODULE = "flextool.representative_periods.preprocess"
26
+ CALIBRATE_MODULE = "flextool.calibrate"
27
+
28
+
29
+ def final_write_methods_from_settings(settings) -> list[str]:
30
+ """Map the project's "File outputs" choices to the calibrator's final
31
+ output formats.
32
+
33
+ The calibration loop always leaves a ``parquet`` result tree, so parquet is
34
+ NOT a choice here; the calibrator regenerates the remaining formats from
35
+ that parquet after the loop (no re-solve). This mirrors the regular-run
36
+ write-method assembly in
37
+ :meth:`flextool.gui.execution_manager.ExecutionManager._build_run_command`
38
+ (same ``auto_generate_*`` flags, same order) minus parquet, so a
39
+ calibration's outputs match exactly what a normal run of the same project
40
+ would produce. An empty list means the operator unchecked every file
41
+ output — the caller emits ``--skip-final-outputs`` to honour that.
42
+ """
43
+ methods: list[str] = []
44
+ if settings.auto_generate_scen_plots:
45
+ methods.append("plot")
46
+ if settings.auto_generate_scen_excels:
47
+ methods.append("excel")
48
+ if settings.auto_generate_scen_csvs:
49
+ methods.append("csv")
50
+ if settings.auto_generate_comp_spinedb:
51
+ methods.append("spinedb")
52
+ return methods
53
+
54
+
55
+ def overshoot_pct_to_multiplier(pct: float) -> float:
56
+ """Convert a planning-safety-margin PERCENT into the CLI ``--overshoot``
57
+ multiplier.
58
+
59
+ The dialog stores ``calib_overshoot_pct`` (0 = off); the calibrator CLI
60
+ consumes a multiplier where ``1.0`` means off. ``20`` percent → ``1.2``.
61
+ """
62
+ return 1.0 + pct / 100.0
63
+
64
+
65
+ def build_rp_command(
66
+ python_exe: str,
67
+ db_url: str,
68
+ scenario: str,
69
+ *,
70
+ n_rp: int,
71
+ period_length: int,
72
+ force_sustained: bool,
73
+ force_peak: bool,
74
+ force_window: int | None,
75
+ solves: Sequence[str],
76
+ alternative_name: str | None,
77
+ alternative_description: str | None = None,
78
+ ) -> list[str]:
79
+ """Build the argv for the representative-periods preprocess CLI.
80
+
81
+ ``force_sustained`` maps to ``--force-highest-net-load`` ("sustained net
82
+ load") and ``force_peak`` to ``--force-peak-load`` ("instantaneous
83
+ peak"). Boolean flags are emitted only when True; ``--solves`` only when
84
+ the list is non-empty (comma-joined); ``--alternative-name`` only when
85
+ truthy. ``--alternative-description`` is emitted only when a description
86
+ is supplied (single argv element, so ``shlex`` quotes the spaces).
87
+ """
88
+ argv: list[str] = [
89
+ python_exe,
90
+ "-m",
91
+ RP_MODULE,
92
+ db_url,
93
+ scenario,
94
+ str(n_rp),
95
+ str(period_length),
96
+ ]
97
+ if force_sustained:
98
+ argv.append("--force-highest-net-load")
99
+ if force_peak:
100
+ argv.append("--force-peak-load")
101
+ if force_window is not None:
102
+ argv += ["--force-window", str(force_window)]
103
+ if solves:
104
+ argv += ["--solves", ",".join(solves)]
105
+ if alternative_name:
106
+ argv += ["--alternative-name", str(alternative_name)]
107
+ if alternative_description:
108
+ argv += ["--alternative-description", str(alternative_description)]
109
+ return argv
110
+
111
+
112
+ def build_calibrate_command(
113
+ python_exe: str,
114
+ db_url: str,
115
+ scenario: str,
116
+ *,
117
+ iterations: int,
118
+ sizing: str,
119
+ overshoot: float,
120
+ damping_first: float,
121
+ damping_remaining: float,
122
+ stall_fraction: float,
123
+ warm_start_cache_dir: str | None,
124
+ work_dir: str | None,
125
+ output_location: str | None,
126
+ debug: bool,
127
+ final_write_methods: list[str] | None = None,
128
+ ) -> list[str]:
129
+ """Build the argv for the calibrate CLI.
130
+
131
+ ``overshoot`` is the CLI MULTIPLIER (not the dialog percent); the caller
132
+ converts via :func:`overshoot_pct_to_multiplier`. ``--overshoot`` is
133
+ emitted only when the multiplier differs from ``1.0`` (off). ``--debug``
134
+ is emitted only when True.
135
+
136
+ ``final_write_methods`` selects which formats the calibrator regenerates
137
+ from the final parquet after the loop (see
138
+ :func:`final_write_methods_from_settings`):
139
+
140
+ * ``None`` — emit no final-output flag; the CLI keeps its own default (csv).
141
+ * ``[]`` — emit ``--skip-final-outputs`` (the operator unchecked every File
142
+ output, so leave the results parquet-only).
143
+ * non-empty — emit ``--final-write-methods <methods…>``.
144
+ """
145
+ argv: list[str] = [
146
+ python_exe,
147
+ "-m",
148
+ CALIBRATE_MODULE,
149
+ db_url,
150
+ scenario,
151
+ "--iterations",
152
+ str(iterations),
153
+ ]
154
+ if sizing:
155
+ argv += ["--sizing", str(sizing)]
156
+ if overshoot != 1.0:
157
+ argv += ["--overshoot", str(overshoot)]
158
+ argv += ["--damping-first-iteration", str(damping_first)]
159
+ argv += ["--damping-remaining-iterations", str(damping_remaining)]
160
+ argv += ["--stall-fraction", str(stall_fraction)]
161
+ if warm_start_cache_dir:
162
+ argv += ["--warm-start-cache-dir", str(warm_start_cache_dir)]
163
+ if work_dir:
164
+ argv += ["--work-dir", str(work_dir)]
165
+ if output_location:
166
+ argv += ["--output-location", str(output_location)]
167
+ if final_write_methods is not None:
168
+ if final_write_methods:
169
+ argv += ["--final-write-methods", *[str(m) for m in final_write_methods]]
170
+ else:
171
+ argv.append("--skip-final-outputs")
172
+ if debug:
173
+ argv.append("--debug")
174
+ return argv
175
+
176
+
177
+ def command_to_display_string(argv: Sequence[str]) -> str:
178
+ """Render *argv* as a single copy-pasteable POSIX shell line.
179
+
180
+ Uses :func:`shlex.join`, so ``shlex.split`` on the result reproduces the
181
+ original argv exactly — the copied text and the run argv are the same
182
+ command.
183
+ """
184
+ return shlex.join(argv)