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,147 @@
1
+ """TimingRecorder — per-CLI-invocation phase timings.
2
+
3
+ Writes a single structured CSV at
4
+ ``<work_folder>/solve_data/timings.csv``.
5
+
6
+ Schema (CSV header)::
7
+
8
+ phase,subphase,solve,roll_index,seconds,started_at_iso,cumulative_s
9
+
10
+ One row per phase. ``record(...)`` appends immediately so a crash mid-run
11
+ still leaves usable data. ``section(...)`` is a context-manager helper
12
+ that times an arbitrary block. ``finalize(output_dir)`` copies the file
13
+ to its final per-scenario output destination once the run completes.
14
+
15
+ Always-on; cheap (one stat + one append per phase). Constructed once per
16
+ CLI invocation; tests calling :class:`FlexToolRunner` directly bootstrap
17
+ their own recorder if the CLI did not pass one in.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import csv
22
+ import time
23
+ from contextlib import contextmanager
24
+ from datetime import datetime, timedelta, timezone
25
+ from pathlib import Path
26
+ from typing import Optional
27
+
28
+
29
+ _HEADER = (
30
+ "phase",
31
+ "subphase",
32
+ "solve",
33
+ "roll_index",
34
+ "seconds",
35
+ "started_at_iso",
36
+ "cumulative_s",
37
+ )
38
+
39
+
40
+ class TimingRecorder:
41
+ """One per CLI invocation; lives on ``FlexToolRunner.state``.
42
+
43
+ Captures ``time.perf_counter()`` deltas with absolute UTC timestamps
44
+ and cumulative-since-start seconds for every phase of a flextool run.
45
+ """
46
+
47
+ def __init__(self, work_folder: Path | str, scenario: Optional[str] = None) -> None:
48
+ self.t0 = time.perf_counter()
49
+ self.t0_wall = datetime.now(timezone.utc)
50
+ self.work_folder = Path(work_folder)
51
+ self.scenario = scenario # may be set later if cli didn't have it yet
52
+ self._path = self.work_folder / "solve_data" / "timings.csv"
53
+ self._path.parent.mkdir(parents=True, exist_ok=True)
54
+ self._open_for_write()
55
+
56
+ def _open_for_write(self) -> None:
57
+ """Create the file (truncate any pre-existing one) and write the header."""
58
+ with open(self._path, "w", newline="") as f:
59
+ writer = csv.writer(f)
60
+ writer.writerow(_HEADER)
61
+
62
+ def set_scenario(self, name: str) -> None:
63
+ """Set/overwrite the scenario name (CLI may not know it at __init__)."""
64
+ self.scenario = name
65
+
66
+ def record(
67
+ self,
68
+ phase: str,
69
+ *,
70
+ subphase: str = "",
71
+ solve: str = "",
72
+ roll_index: int | str = "",
73
+ seconds: float,
74
+ t_start: Optional[float] = None,
75
+ ) -> None:
76
+ """Append one row.
77
+
78
+ ``t_start`` is the ``perf_counter()`` value at the START of the
79
+ phase (used to back-compute ``started_at_iso``); if ``None`` we
80
+ assume the phase ended just now (``started_at_iso = now - seconds``).
81
+ """
82
+ now_perf = time.perf_counter()
83
+ cumulative_s = now_perf - self.t0
84
+ if t_start is not None:
85
+ started_perf = t_start
86
+ else:
87
+ started_perf = now_perf - max(0.0, float(seconds))
88
+ # Map perf_counter → wall time via the recorded t0/t0_wall pair.
89
+ started_wall = self.t0_wall + timedelta(seconds=started_perf - self.t0)
90
+ started_iso = started_wall.isoformat()
91
+ row = (
92
+ str(phase),
93
+ str(subphase),
94
+ str(solve),
95
+ str(roll_index),
96
+ f"{float(seconds):.6f}",
97
+ started_iso,
98
+ f"{cumulative_s:.6f}",
99
+ )
100
+ # Atomic append — open/write/close per row so a crash leaves the
101
+ # file in a parseable state.
102
+ with open(self._path, "a", newline="") as f:
103
+ csv.writer(f).writerow(row)
104
+
105
+ @contextmanager
106
+ def section(
107
+ self,
108
+ phase: str,
109
+ *,
110
+ subphase: str = "",
111
+ solve: str = "",
112
+ roll_index: int | str = "",
113
+ ):
114
+ """Time a block: ``with recorder.section('write_input'): ...``."""
115
+ t_start = time.perf_counter()
116
+ try:
117
+ yield
118
+ finally:
119
+ elapsed = time.perf_counter() - t_start
120
+ self.record(
121
+ phase,
122
+ subphase=subphase,
123
+ solve=solve,
124
+ roll_index=roll_index,
125
+ seconds=elapsed,
126
+ t_start=t_start,
127
+ )
128
+
129
+ @property
130
+ def path(self) -> Path:
131
+ """Current location of the timings.csv (in the work folder)."""
132
+ return self._path
133
+
134
+ def finalize(self, output_dir: Path | str) -> None:
135
+ """Copy ``timings.csv`` into the final scenario output dir.
136
+
137
+ Lives alongside ``summary_solve.csv`` so users have one place to
138
+ look. Idempotent: if the source file is missing, this is a no-op.
139
+ """
140
+ src = self._path
141
+ if not src.exists():
142
+ return
143
+ out = Path(output_dir)
144
+ out.mkdir(parents=True, exist_ok=True)
145
+ dst = out / "timings.csv"
146
+ # Plain byte copy — file is small.
147
+ dst.write_bytes(src.read_bytes())
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ FlexTool Workflow Orchestrator
4
+
5
+ Runs the FlexTool workflow in two phases:
6
+ 1. Input preparation (optional): convert tabular data into a FlexTool input
7
+ database. Runs only when --tabular-file-path or --csv-directory-path is
8
+ given; otherwise the existing input database is used as-is.
9
+ 2. Model execution + output write: run the optimisation model and write
10
+ results in the requested formats. Parquet is always produced; --write-methods
11
+ selects which additional formats (plot, csv, excel) to generate alongside.
12
+ """
13
+
14
+ import argparse
15
+ import logging
16
+ import subprocess
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ from flextool.cli._console import run_tool
21
+ from flextool.update_flextool.ensure_settings_db import ensure_settings_db
22
+
23
+
24
+ def main():
25
+ parser = argparse.ArgumentParser(
26
+ description='Execute complete FlexTool workflow',
27
+ formatter_class=argparse.RawDescriptionHelpFormatter,
28
+ epilog="""
29
+ Examples:
30
+ # Full workflow with Excel input
31
+ python execute_flextool_workflow.py input.sqlite results.sqlite my_scenario --tabular-file-path input.xlsx
32
+
33
+ # Full workflow with CSV input
34
+ python execute_flextool_workflow.py input.sqlite results.sqlite my_scenario --csv-directory-path input_data/
35
+
36
+ # Use an existing input database (no tabular/csv source -> input prep is skipped)
37
+ python execute_flextool_workflow.py input.sqlite results.sqlite my_scenario
38
+
39
+ # Add plots and CSV alongside the default parquet output
40
+ python execute_flextool_workflow.py input.sqlite results.sqlite my_scenario --write-methods plot parquet csv
41
+ """
42
+ )
43
+
44
+ # Required arguments
45
+ parser.add_argument('input_db_url', help='Input database URL (e.g., sqlite:///input.sqlite or input.sqlite)')
46
+ parser.add_argument('output_db_url', help='Output database URL for storing result metadata')
47
+ parser.add_argument('scenario_name', help='Name of the scenario to execute')
48
+
49
+ # Input phase arguments. Presence of either flag triggers Phase 1
50
+ # (input preparation); when neither is given, the existing input
51
+ # database is used as-is.
52
+ input_group = parser.add_mutually_exclusive_group()
53
+ input_group.add_argument('--tabular-file-path', help='Path to Excel/ODS input file')
54
+ input_group.add_argument('--csv-directory-path', help='Path to directory containing CSV input files')
55
+
56
+ # Output controls — forwarded to cmd_run_flextool, which writes
57
+ # outputs inline using the live in-memory flex_data + solution.
58
+ parser.add_argument('--write-methods', nargs='+',
59
+ default=['plot', 'parquet', 'csv'],
60
+ choices=['plot', 'parquet', 'excel', 'csv', 'spinedb'],
61
+ help='Output formats to generate (default: plot parquet csv). '
62
+ 'Parquet is the canonical output and is always produced when this flag is omitted.')
63
+ parser.add_argument('--output-subdir', help='Subdirectory for output files (default: scenario_name)')
64
+ parser.add_argument('--output-config', default=None,
65
+ help='Path to output configuration YAML file (default: bundled schemas/default_plots.yaml)')
66
+
67
+ # Additional options
68
+ parser.add_argument('--debug',
69
+ nargs='?',
70
+ const='basic',
71
+ default='off',
72
+ choices=['off', 'basic', 'full'],
73
+ metavar='LEVEL',
74
+ help='Diagnostic verbosity level passed through '
75
+ 'to cmd_run_flextool. Bare ``--debug`` '
76
+ 'selects ``basic``. See cmd_run_flextool '
77
+ '--help for level semantics.')
78
+
79
+ args = parser.parse_args()
80
+
81
+ # Self-heal missing lightweight settings DBs so fresh clones don't
82
+ # fail opaquely when the user forgot to run `flextool-update`. Only
83
+ # seeds output_info / output_settings / comparison_settings by
84
+ # basename; other paths are left untouched.
85
+ for _candidate in (args.output_db_url,):
86
+ try:
87
+ ensure_settings_db(_candidate)
88
+ except Exception as _exc:
89
+ logging.warning("Failed to auto-seed %s: %s", _candidate, _exc)
90
+
91
+ # Phase 1 runs only when a tabular/csv source is provided; otherwise
92
+ # the existing input database is used as-is.
93
+ run_input_prep = bool(args.tabular_file_path or args.csv_directory_path)
94
+
95
+ # Set default output subdirectory to scenario name if not specified
96
+ output_subdir = args.output_subdir if args.output_subdir else args.scenario_name
97
+
98
+ # Phase 1: Input Preparation
99
+ if run_input_prep:
100
+ print(f"\n{'='*70}")
101
+ print("PHASE 1: PREPARING INPUT DATA")
102
+ print(f"{'='*70}")
103
+ if args.tabular_file_path:
104
+ print(f" Input file: {args.tabular_file_path}")
105
+ else:
106
+ print(f" Input directory: {args.csv_directory_path}")
107
+ print(f" Target DB: {args.input_db_url}")
108
+ print()
109
+
110
+ cmd = [sys.executable, '-m', 'flextool.cli.cmd_read_tabular_input', args.input_db_url]
111
+ if args.tabular_file_path:
112
+ cmd.extend(['--tabular-file-path', args.tabular_file_path])
113
+ else:
114
+ cmd.extend(['--csv-directory-path', args.csv_directory_path])
115
+
116
+ result = subprocess.run(cmd)
117
+ if result.returncode != 0:
118
+ print("\n" + "="*70)
119
+ print("ERROR: Input preparation failed with return code", result.returncode)
120
+ print("="*70)
121
+ sys.exit(result.returncode)
122
+
123
+ print(f"\n{'='*70}")
124
+ print("PHASE 1: COMPLETED SUCCESSFULLY")
125
+ print(f"{'='*70}\n")
126
+ else:
127
+ print(f"\n{'='*70}")
128
+ print("PHASE 1: SKIPPED (using existing input database)")
129
+ print(f"{'='*70}\n")
130
+
131
+ # Phase 2: Model Execution + Output Write
132
+ # cmd_run_flextool writes outputs inline using the live in-memory
133
+ # flex_data + solution from the cascade. There is no separate
134
+ # standalone "write outputs" subprocess — the data is gone once the
135
+ # solver exits.
136
+ print(f"\n{'='*70}")
137
+ print("PHASE 2: RUNNING FLEXTOOL MODEL + WRITING OUTPUTS")
138
+ print(f"{'='*70}")
139
+ print(f" Input DB: {args.input_db_url}")
140
+ print(f" Output DB: {args.output_db_url}")
141
+ print(f" Scenario: {args.scenario_name}")
142
+ print(f" Formats: {', '.join(args.write_methods)}")
143
+ print(f" Subdirectory: {output_subdir}")
144
+ print()
145
+
146
+ # Resolve --output-config: bare basename or unset → bundled
147
+ # default from the package; absolute path → used verbatim.
148
+ from flextool._resources import package_data_path
149
+ output_config = args.output_config
150
+ if not output_config or Path(output_config).name in {
151
+ 'default_plots.yaml', 'default_comparison_plots.yaml'
152
+ }:
153
+ output_config = str(package_data_path("schemas/default_plots.yaml"))
154
+
155
+ cmd = [sys.executable, '-m', 'flextool.cli.cmd_run_flextool',
156
+ args.input_db_url, args.output_db_url,
157
+ '--scenario-name', args.scenario_name,
158
+ '--output-config', output_config,
159
+ '--output-subdir', output_subdir,
160
+ '--write-methods'] + args.write_methods
161
+ if args.debug != 'off':
162
+ cmd.append(f'--debug={args.debug}')
163
+
164
+ result = subprocess.run(cmd)
165
+ if result.returncode != 0:
166
+ print("\n" + "="*70)
167
+ print("ERROR: Model execution failed with return code", result.returncode)
168
+ print("="*70)
169
+ sys.exit(result.returncode)
170
+
171
+ print(f"\n{'='*70}")
172
+ print("PHASE 2: COMPLETED SUCCESSFULLY")
173
+ print(f"{'='*70}\n")
174
+
175
+ # Success summary
176
+ print(f"\n{'='*70}")
177
+ print("SUCCESS: COMPLETE WORKFLOW EXECUTED")
178
+ print(f"{'='*70}")
179
+ print(f" Scenario: {args.scenario_name}")
180
+ print(f" Input DB: {args.input_db_url}")
181
+ print(f" Output DB: {args.output_db_url}")
182
+ print(f" Output dir: {output_subdir}/")
183
+ print(f"{'='*70}\n")
184
+
185
+
186
+ if __name__ == '__main__':
187
+ run_tool(main)
@@ -0,0 +1,56 @@
1
+ """CLI entry point for exporting a FlexTool Spine DB to Excel (.xlsx) format."""
2
+
3
+ import argparse
4
+
5
+ from flextool.export_to_tabular import export_to_excel
6
+
7
+
8
+ def main() -> None:
9
+ parser = argparse.ArgumentParser(
10
+ description="Export a FlexTool Spine DB to Excel (.xlsx) format."
11
+ )
12
+ parser.add_argument(
13
+ "db_url",
14
+ help="URL to FlexTool input database (e.g. sqlite:///input_data.sqlite)",
15
+ )
16
+ parser.add_argument(
17
+ "output_path",
18
+ help="Output Excel file path (.xlsx)",
19
+ )
20
+ parser.add_argument(
21
+ "--include-advanced",
22
+ action="store_true",
23
+ help="Include advanced sheets (solve sequences, stochastic data)",
24
+ )
25
+ parser.add_argument(
26
+ "--old-format",
27
+ action="store_true",
28
+ help="Use the old v1 format instead of the self-describing v2 format",
29
+ )
30
+ parser.add_argument(
31
+ "--groups",
32
+ default=None,
33
+ help=(
34
+ "Comma-separated list of parameter_group names to keep "
35
+ "(e.g. 'basics,model,timeline,solve_basics'). Parameters "
36
+ "outside these groups are dropped, and sheets with no surviving "
37
+ "columns are removed unless listed in always_include_with_groups."
38
+ ),
39
+ )
40
+ args = parser.parse_args()
41
+ include_groups = (
42
+ [g.strip() for g in args.groups.split(",") if g.strip()]
43
+ if args.groups
44
+ else None
45
+ )
46
+ export_to_excel(
47
+ args.db_url,
48
+ args.output_path,
49
+ include_advanced=args.include_advanced,
50
+ use_new_format=not args.old_format,
51
+ include_groups=include_groups,
52
+ )
53
+
54
+
55
+ if __name__ == "__main__":
56
+ main()
@@ -0,0 +1,75 @@
1
+ """CLI entry point for importing FlexTool 2.0 sensitivities into a Spine DB."""
2
+
3
+ import argparse
4
+ import logging
5
+ import sys
6
+
7
+ from flextool.cli._console import run_tool
8
+ from flextool.process_inputs.read_old_flextool import (
9
+ read_old_flextool,
10
+ read_old_flextool_sensitivities,
11
+ )
12
+ from flextool.process_inputs.write_old_flextool_to_db import write_sensitivities_to_db
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def main() -> None:
18
+ parser = argparse.ArgumentParser(
19
+ description="Import FlexTool 2.0 sensitivities into a Spine DB."
20
+ )
21
+ parser.add_argument(
22
+ "master_xlsm",
23
+ help="Path to old FlexTool master .xlsm file (contains Sensitivity definitions sheet)",
24
+ )
25
+ parser.add_argument(
26
+ "base_xlsm",
27
+ help="Path to the base data .xlsm file (for unit_type mapping and base data)",
28
+ )
29
+ parser.add_argument(
30
+ "target_db_url",
31
+ help="Target Spine DB URL (must already contain base import)",
32
+ )
33
+ parser.add_argument(
34
+ "--base-alternative",
35
+ default="base",
36
+ help="Name of base alternative in the DB (default: base)",
37
+ )
38
+ parser.add_argument(
39
+ "--verbose", "-v",
40
+ action="store_true",
41
+ help="Enable verbose (DEBUG) logging",
42
+ )
43
+ args = parser.parse_args()
44
+
45
+ logging.basicConfig(
46
+ level=logging.DEBUG if args.verbose else logging.INFO,
47
+ format="%(levelname)s: %(message)s",
48
+ )
49
+
50
+ print(f"Reading base data from: {args.base_xlsm}")
51
+ data = read_old_flextool(args.base_xlsm)
52
+
53
+ print(f"Reading sensitivities from: {args.master_xlsm}")
54
+ sensitivities = read_old_flextool_sensitivities(args.master_xlsm)
55
+
56
+ if not sensitivities:
57
+ print("No sensitivities found. Nothing to import.")
58
+ sys.exit(0)
59
+
60
+ print(f"Found {len(sensitivities)} sensitivity scenario(s).")
61
+ for name, overrides in sensitivities.items():
62
+ print(f" - {name}: {len(overrides)} override(s)")
63
+
64
+ print(f"Writing sensitivities to: {args.target_db_url}")
65
+ write_sensitivities_to_db(
66
+ sensitivities,
67
+ data,
68
+ args.target_db_url,
69
+ base_alternative=args.base_alternative,
70
+ )
71
+ print("Sensitivity import complete!")
72
+
73
+
74
+ if __name__ == "__main__":
75
+ run_tool(main)
@@ -0,0 +1,13 @@
1
+ import argparse
2
+ from flextool.update_flextool.db_migration import migrate_database
3
+
4
+
5
+ def main():
6
+ parser = argparse.ArgumentParser()
7
+ parser.add_argument('filepath', help="Filepath of the database, absolute or relative to flextool folder")
8
+ args = parser.parse_args()
9
+ migrate_database(args.filepath)
10
+
11
+
12
+ if __name__ == '__main__':
13
+ main()