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,284 @@
1
+ """CLI entry point for scenario comparison: argparse + settings resolution → orchestrator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import matplotlib
11
+ from spinedb_api import DatabaseMapping, from_database, Array
12
+ from spinedb_api.filters.alternative_filter import alternative_filter_config
13
+ from spinedb_api.filters.tools import append_filter_config
14
+
15
+ from flextool.cli._console import run_tool
16
+ from flextool.scenario_comparison import orchestrator
17
+ from flextool.scenario_comparison.db_reader import build_scenario_folders_from_dir
18
+ from flextool.update_flextool.ensure_settings_db import ensure_settings_db
19
+
20
+ import logging
21
+
22
+
23
+ def main() -> None:
24
+ matplotlib.use('Agg')
25
+ parser = argparse.ArgumentParser(
26
+ description='Read and combine scenario results from multiple folders based on database information')
27
+ parser.add_argument(
28
+ 'db_url', nargs='?', default=None,
29
+ help='Database URL containing scenario information (e.g., sqlite:///scenarios.db)')
30
+ parser.add_argument(
31
+ '--parquet-base-dir', default=None,
32
+ help='Base directory containing per-scenario parquet subdirectories '
33
+ '(alternative to db_url; requires --alternatives)')
34
+ parser.add_argument(
35
+ '--parquet-subdir',
36
+ default='output_parquet',
37
+ help='Subdirectory containing parquet files (default: output_parquet)')
38
+ parser.add_argument('--settings-db-url', default=None,
39
+ help='Settings database URL (fills in unset params; CLI overrides DB)')
40
+ parser.add_argument(
41
+ '--output-config-path', default=None,
42
+ help='Path to output configuration YAML file (default: templates/default_plots.yaml — '
43
+ 'comparison mode reads scenario_rule + comparison_overrides from each leaf)'
44
+ )
45
+ parser.add_argument('--active-configs', type=str, default=None, nargs="+",
46
+ help='Which plot configurations from config_path yaml to use (default: default)')
47
+ parser.add_argument('--plot-rows', type=int, nargs=2, default=None,
48
+ help='First and last row to plot in time series (default: 0 167)')
49
+ parser.add_argument('--write-to-xlsx', action='store_true', default=None,
50
+ help='Write combined results to Excel file')
51
+ parser.add_argument('--write-dispatch-xlsx', action='store_true', default=None,
52
+ help='Write dispatch data to Excel file in plot directory')
53
+ parser.add_argument('--write-to-ods', action='store_true', default=None)
54
+ parser.add_argument(
55
+ '--alternatives', metavar='S', type=str, nargs='+',
56
+ help='Add alternative names manually')
57
+ parser.add_argument(
58
+ '--plot-dir', default=None,
59
+ help='Directory to plot scenario comparison plots '
60
+ '(default: <project_root>/output_plot_comparisons when the '
61
+ 'compared scenarios share a project, else the CWD-relative '
62
+ 'output_plot_comparisons)'
63
+ )
64
+ parser.add_argument(
65
+ '--dispatch-plots', action='store_true',
66
+ help='Generate dispatch area plots for nodes and nodeGroups'
67
+ )
68
+ parser.add_argument(
69
+ '--show-plots', action='store_true', default=None,
70
+ help='Display plots interactively (in addition to saving)'
71
+ )
72
+ parser.add_argument(
73
+ '--plot-file-format', type=str, default=None,
74
+ choices=['png', 'svg'],
75
+ help='File format for plot output (default: png)'
76
+ )
77
+ parser.add_argument(
78
+ '--excel-dir', default=None,
79
+ help='Directory to write comparison Excel files (default: same as --plot-dir)'
80
+ )
81
+ parser.add_argument(
82
+ '--shared-legend', action='store_true', default=None,
83
+ help='Use shared legend across subplots (default: true)'
84
+ )
85
+ parser.add_argument(
86
+ '--only-first-file-per-plot', action='store_true', default=False,
87
+ help='Only produce the first file for each plot (quick overview mode)'
88
+ )
89
+ parser.add_argument(
90
+ '--comparison-parquet-dir', default=None,
91
+ help='Directory to write combined comparison parquet files for viewer integration'
92
+ )
93
+ parser.add_argument(
94
+ '--debug',
95
+ nargs='?',
96
+ const='basic',
97
+ default='off',
98
+ choices=['off', 'basic', 'full'],
99
+ metavar='LEVEL',
100
+ help='Diagnostic verbosity level (default: off). Any level other '
101
+ 'than ``off`` additionally generates per-node dispatch plots '
102
+ 'for every available node (no node curation). Mirrors the '
103
+ '``--debug`` flag of cmd_run_flextool.',
104
+ )
105
+
106
+ args = parser.parse_args()
107
+ db_url = args.db_url
108
+ parquet_base_dir = args.parquet_base_dir
109
+
110
+ # Self-heal missing lightweight settings DBs so fresh clones don't
111
+ # fail opaquely when the user forgot to run `flextool-update`. Only
112
+ # seeds output_info / output_settings / comparison_settings by
113
+ # basename.
114
+ for _candidate in (args.settings_db_url,):
115
+ try:
116
+ ensure_settings_db(_candidate)
117
+ except Exception as _exc:
118
+ logging.warning("Failed to auto-seed %s: %s", _candidate, _exc)
119
+
120
+ # Validate: need at least one data source
121
+ if db_url is None and parquet_base_dir is None:
122
+ parser.error('Either db_url or --parquet-base-dir must be provided')
123
+ # Validate: --parquet-base-dir requires --alternatives
124
+ if parquet_base_dir is not None and not args.alternatives:
125
+ parser.error('--parquet-base-dir requires --alternatives to specify scenario names')
126
+
127
+ # Resolve parameters: CLI args > settings DB > hardcoded defaults
128
+ output_config_path = args.output_config_path
129
+ active_configs = args.active_configs
130
+ plot_rows = args.plot_rows
131
+ write_to_xlsx = args.write_to_xlsx
132
+ write_dispatch_xlsx = args.write_dispatch_xlsx
133
+ write_to_ods = args.write_to_ods
134
+ show_plots = args.show_plots
135
+ plot_file_format = args.plot_file_format
136
+ shared_legend = args.shared_legend
137
+
138
+ settings_db_url = args.settings_db_url
139
+ if settings_db_url and os.path.exists(settings_db_url.replace('sqlite:///', '')):
140
+ with DatabaseMapping(settings_db_url) as settings_db:
141
+ settings_entities = settings_db.get_entity_items(entity_class_name="settings")
142
+ if len(settings_entities) == 1:
143
+ settings_name = settings_entities[0]["name"]
144
+ settings_params: dict = {}
145
+ for pv in settings_db.get_parameter_value_items(entity_class_name="settings"):
146
+ if pv["entity_byname"] == (settings_name,):
147
+ settings_params[pv["parameter_definition_name"]] = from_database(pv["value"], pv["type"])
148
+
149
+ if output_config_path is None and 'output-config-path' in settings_params:
150
+ output_config_path = str(settings_params['output-config-path'])
151
+
152
+ if active_configs is None and 'active-output-configs' in settings_params:
153
+ val = settings_params['active-output-configs']
154
+ if isinstance(val, str):
155
+ active_configs = [val]
156
+ elif isinstance(val, Array):
157
+ active_configs = list(val.values)
158
+ else:
159
+ active_configs = list(val)
160
+
161
+ if plot_rows is None:
162
+ first = settings_params.get('plot_first_timestep')
163
+ duration = settings_params.get('plot_duration')
164
+ if first is not None and duration is not None:
165
+ plot_rows = [int(first), int(first) + int(duration)]
166
+
167
+ if write_to_xlsx is None and 'write-to-excel' in settings_params:
168
+ write_to_xlsx = bool(settings_params['write-to-excel'])
169
+ if write_dispatch_xlsx is None and 'write-dispatch-to-excel' in settings_params:
170
+ write_dispatch_xlsx = bool(settings_params['write-dispatch-to-excel'])
171
+ if write_to_ods is None and 'write-to-ods' in settings_params:
172
+ write_to_ods = bool(settings_params['write-to-ods'])
173
+ if show_plots is None and 'show-plots' in settings_params:
174
+ show_plots = bool(settings_params['show-plots'])
175
+ if plot_file_format is None and 'plot-file-format' in settings_params:
176
+ plot_file_format = str(settings_params['plot-file-format'])
177
+ if shared_legend is None and 'shared-legend' in settings_params:
178
+ shared_legend = bool(settings_params['shared-legend'])
179
+
180
+ # Apply hardcoded defaults for anything still unset
181
+ from flextool._resources import package_data_path
182
+ _default_plots = str(package_data_path("schemas/default_plots.yaml"))
183
+ if output_config_path is None:
184
+ output_config_path = _default_plots
185
+ elif not os.path.isabs(output_config_path):
186
+ # Legacy: settings DB may carry a repo-relative override like
187
+ # ``templates/default_plots.yaml``. Treat such relative paths
188
+ # as references to the bundled default — the wheel install no
189
+ # longer has a repo root.
190
+ if os.path.basename(output_config_path) in {
191
+ 'default_plots.yaml', 'default_comparison_plots.yaml'
192
+ }:
193
+ output_config_path = _default_plots
194
+ else:
195
+ output_config_path = os.path.abspath(output_config_path)
196
+
197
+ # Migrate stale settings: comparison rendering now lives inside
198
+ # default_plots.yaml (driven by per-leaf ``scenario_rule``). An
199
+ # existing settings DB or user override may still point to the
200
+ # deleted ``default_comparison_plots.yaml`` — silently redirect
201
+ # to the merged file so the run doesn't blow up.
202
+ if (os.path.basename(output_config_path) == 'default_comparison_plots.yaml'
203
+ or not os.path.isfile(output_config_path)):
204
+ if output_config_path != _default_plots:
205
+ logging.info(
206
+ "output-config-path %s not found or superseded — using "
207
+ "bundled %s instead.", output_config_path, _default_plots,
208
+ )
209
+ output_config_path = _default_plots
210
+ if active_configs is None:
211
+ active_configs = ['default']
212
+ if plot_rows is None:
213
+ plot_rows = [0, 167]
214
+ if write_to_xlsx is None:
215
+ write_to_xlsx = False
216
+ if write_dispatch_xlsx is None:
217
+ write_dispatch_xlsx = False
218
+ if write_to_ods is None:
219
+ write_to_ods = False
220
+ if show_plots is None:
221
+ show_plots = False
222
+ if plot_file_format is None:
223
+ plot_file_format = 'png'
224
+ if shared_legend is None:
225
+ shared_legend = True
226
+
227
+ # Build scenario-to-folder mapping from either --parquet-base-dir or db_url
228
+ alternatives = args.alternatives
229
+ pre_built_folders: dict[str, str] | None = None
230
+ parquet_subdir = args.parquet_subdir
231
+
232
+ if parquet_base_dir is not None:
233
+ # Direct parquet directory mode: skip database, build mapping from filesystem
234
+ pre_built_folders = build_scenario_folders_from_dir(
235
+ Path(parquet_base_dir), alternatives
236
+ )
237
+ if not pre_built_folders:
238
+ print("Error: no valid scenario directories found under "
239
+ f"{parquet_base_dir}", file=sys.stderr)
240
+ sys.exit(1)
241
+ # With --parquet-base-dir the parquets sit directly in base_dir/scenario/,
242
+ # so the extra subdirectory level is not needed.
243
+ parquet_subdir = ''
244
+ elif alternatives and db_url is not None:
245
+ # Database mode with alternative filters
246
+ alternative_filter = alternative_filter_config(alternatives)
247
+ db_url = append_filter_config(db_url, alternative_filter)
248
+
249
+ # Resolve dispatch_plots flags
250
+ do_dispatch = args.dispatch_plots
251
+
252
+ # ``--plot-dir`` left unset → the orchestrator re-roots the comparison
253
+ # output dir at ``<project_root>/output_plot_comparisons`` when the
254
+ # compared scenarios share a sensible project root (GUI/Toolbox parity).
255
+ # Carry the bare CWD-relative default so an explicit value still wins and
256
+ # the no-common-root fallback is byte-identical to the old behaviour.
257
+ plot_dir_is_default = args.plot_dir is None
258
+ plot_dir = 'output_plot_comparisons' if plot_dir_is_default else args.plot_dir
259
+
260
+ orchestrator.run(
261
+ db_url=db_url,
262
+ parquet_subdir=parquet_subdir,
263
+ plot_dir=plot_dir,
264
+ output_config_path=output_config_path,
265
+ active_configs=active_configs,
266
+ plot_rows=plot_rows,
267
+ write_to_xlsx=write_to_xlsx,
268
+ write_dispatch_xlsx=write_dispatch_xlsx,
269
+ write_to_ods=write_to_ods,
270
+ show_plots=show_plots,
271
+ dispatch_plots=do_dispatch,
272
+ plot_file_format=plot_file_format,
273
+ scenario_folders=pre_built_folders,
274
+ excel_dir=args.excel_dir,
275
+ shared_legend=shared_legend,
276
+ only_first_file=args.only_first_file_per_plot,
277
+ comparison_parquet_dir=args.comparison_parquet_dir,
278
+ debug=args.debug != 'off',
279
+ plot_dir_is_default=plot_dir_is_default,
280
+ )
281
+
282
+
283
+ if __name__ == '__main__':
284
+ run_tool(main)
@@ -0,0 +1,169 @@
1
+ """Standalone HiGHS-from-MPS subprocess solver.
2
+
3
+ Invoked by FlexTool's ``--save-memory`` path to keep HiGHS' active-solve
4
+ memory footprint isolated from the parent Python process. Reads an MPS
5
+ file written by ``polar_high.Problem.build_only``, runs HiGHS with the
6
+ caller's options, writes a solution file the parent reads back.
7
+
8
+ Intentionally imports nothing from the rest of FlexTool so the child
9
+ process's Python footprint stays small. Only depends on ``highspy``.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ from flextool.cli._console import run_tool
19
+
20
+
21
+ def main(argv: list[str] | None = None) -> int:
22
+ parser = argparse.ArgumentParser(
23
+ description="Solve an MPS file with HiGHS in a subprocess.",
24
+ )
25
+ parser.add_argument("--mps", type=Path, required=True, help="Input MPS file")
26
+ parser.add_argument(
27
+ "--solution", type=Path, required=True,
28
+ help="Output solution file (write_solution_style=0, pretty/sparse)",
29
+ )
30
+ parser.add_argument(
31
+ "--basis", type=Path, default=None,
32
+ help="Optional output basis file",
33
+ )
34
+ parser.add_argument(
35
+ "--warm-basis", type=Path, default=None,
36
+ help="Optional input basis file to warm-start from",
37
+ )
38
+ parser.add_argument(
39
+ "--stats", type=Path, default=None,
40
+ help="Optional output JSON file with solve measurement fields",
41
+ )
42
+ parser.add_argument(
43
+ "--options", type=Path, default=None,
44
+ help="Optional HiGHS options file (key=value per line)",
45
+ )
46
+ args = parser.parse_args(argv)
47
+
48
+ import highspy
49
+
50
+ # ``kWarning`` is non-fatal — HiGHS issues it for things like
51
+ # writeSolution-on-an-LP-without-ranging, options-file lines it
52
+ # didn't recognise, etc. Treat the API call as successful in that
53
+ # case but surface the warning to stderr.
54
+ def _ok(status: object, what: str) -> bool:
55
+ if status == highspy.HighsStatus.kOk:
56
+ return True
57
+ if status == highspy.HighsStatus.kWarning:
58
+ print(
59
+ f"NOTICE: HiGHS {what} returned kWarning (continuing)",
60
+ file=sys.stderr,
61
+ )
62
+ return True
63
+ return False
64
+
65
+ h = highspy.Highs()
66
+
67
+ # Apply options BEFORE readModel so presolve / solver / scaling
68
+ # decisions take effect on the read. HiGHS' own .opt file format
69
+ # is ``key=value`` per line; we use that as the IPC mechanism so
70
+ # FlexTool doesn't need to keep its option-dict translation aligned
71
+ # with this CLI separately.
72
+ if args.options is not None:
73
+ if not _ok(h.readOptions(str(args.options)), "readOptions"):
74
+ print(
75
+ f"ERROR: HiGHS rejected options file {args.options}",
76
+ file=sys.stderr,
77
+ )
78
+ return 2
79
+
80
+ if not _ok(h.readModel(str(args.mps)), "readModel"):
81
+ print(f"ERROR: HiGHS failed to read MPS {args.mps}", file=sys.stderr)
82
+ return 3
83
+
84
+ # Warm-start injection. A rejected / missing / corrupt basis is a
85
+ # safe silent-cold fallback: HiGHS' run() is correct after a failed
86
+ # readBasis, so we log and continue rather than abort the solve.
87
+ warm_basis_used = False
88
+ if args.warm_basis is not None:
89
+ try:
90
+ status = h.readBasis(str(args.warm_basis))
91
+ if status == highspy.HighsStatus.kOk:
92
+ warm_basis_used = True
93
+ print(
94
+ f"INFO: warm-basis accepted from {args.warm_basis}",
95
+ file=sys.stderr,
96
+ )
97
+ else:
98
+ print(
99
+ f"WARNING: warm-basis rejected ({status}) — solving cold",
100
+ file=sys.stderr,
101
+ )
102
+ except Exception as exc:
103
+ print(
104
+ f"WARNING: warm-basis read failed ({exc}) — solving cold",
105
+ file=sys.stderr,
106
+ )
107
+
108
+ if not _ok(h.run(), "run"):
109
+ print("ERROR: HiGHS run() returned non-OK status", file=sys.stderr)
110
+ return 4
111
+
112
+ model_status = h.getModelStatus()
113
+ # Write the solution unconditionally so the parent can inspect
114
+ # whatever HiGHS produced (useful for debugging non-optimal runs).
115
+ # Pretty/sparse format (style=0) — matches what the parent's
116
+ # h.readSolution expects.
117
+ if not _ok(h.writeSolution(str(args.solution), 0), "writeSolution"):
118
+ print(
119
+ f"ERROR: HiGHS failed to write solution {args.solution}",
120
+ file=sys.stderr,
121
+ )
122
+ return 5
123
+
124
+ if args.basis is not None:
125
+ # Best-effort: basis write isn't critical for the parent's
126
+ # output path, and small models may not have a basis on
127
+ # interior-point solves. Warn but don't fail.
128
+ try:
129
+ _ok(h.writeBasis(str(args.basis)), "writeBasis")
130
+ except Exception as exc:
131
+ print(f"WARNING: writeBasis raised: {exc}", file=sys.stderr)
132
+
133
+ if args.stats is not None:
134
+ # Best-effort measurement sidecar for the parent's A/B gating.
135
+ # A stats-write failure must never fail the solve.
136
+ try:
137
+ info = h.getInfo()
138
+ # Wall-clock solve time drives the parent's A/B warm-vs-cold
139
+ # gate. Best-effort: a highspy build without ``getRunTime``
140
+ # (or one that returns junk on a tiny model) falls back to
141
+ # ``-1.0``, which the parent treats as "no timing → skip gate".
142
+ try:
143
+ run_time = float(h.getRunTime())
144
+ except Exception:
145
+ run_time = -1.0
146
+ stats = {
147
+ "simplex_iteration_count": int(info.simplex_iteration_count),
148
+ "model_status": str(model_status),
149
+ "objective": float(h.getObjectiveValue()),
150
+ "warm_basis_used": warm_basis_used,
151
+ "run_time": run_time,
152
+ }
153
+ args.stats.write_text(json.dumps(stats))
154
+ except Exception as exc:
155
+ print(f"WARNING: stats write failed: {exc}", file=sys.stderr)
156
+
157
+ # Exit code carries the model status so the parent can branch on
158
+ # optimality without parsing the solution file first.
159
+ if model_status == highspy.HighsModelStatus.kOptimal:
160
+ return 0
161
+ print(
162
+ f"NOTICE: HiGHS finished with non-optimal status={model_status}",
163
+ file=sys.stderr,
164
+ )
165
+ return 1
166
+
167
+
168
+ if __name__ == "__main__":
169
+ run_tool(main)
@@ -0,0 +1,17 @@
1
+ import argparse
2
+ from flextool.update_flextool.self_update import update_flextool
3
+
4
+
5
+ def main():
6
+ arg_parser = argparse.ArgumentParser()
7
+ arg_parser.add_argument(
8
+ "--skip-git",
9
+ action="store_true",
10
+ help="skip 'git restore' and 'git pull' steps",
11
+ )
12
+ args = arg_parser.parse_args()
13
+ update_flextool(args.skip_git)
14
+
15
+
16
+ if __name__ == "__main__":
17
+ main()
@@ -0,0 +1,125 @@
1
+ from flextool.cli._console import run_tool
2
+ from flextool.process_outputs.write_outputs import write_outputs
3
+ from flextool.update_flextool.ensure_settings_db import ensure_settings_db
4
+ from spinedb_api import DatabaseMapping
5
+ from spinedb_api.filters.tools import name_from_dict
6
+ import logging
7
+ import sys
8
+
9
+ def main():
10
+
11
+ import argparse
12
+
13
+ parser = argparse.ArgumentParser(description='Write FlexTool outputs to various formats')
14
+ parser.add_argument('--input-db-url', type=str, help='URL of the input database with scenario filter (used when run_flextool.py calls this from Toolbox)')
15
+ parser.add_argument('--scenario-name', type=str, help='Name of a scenario that must have raw outputs available (when re-plotting single scenario from terminal)')
16
+ parser.add_argument('--output-locations-db-url', type=str, help='URL of the database that holds the locations of existing outputs (for re-plotting from Toolbox)')
17
+ parser.add_argument('--settings-db-url', type=str, default=None,
18
+ help='URL of the settings database (fills in unset params)')
19
+ parser.add_argument('--config-path', type=str, default=None,
20
+ help='Path to output configuration YAML file (default: templates/default_plots.yaml)')
21
+ parser.add_argument('--active-configs', type=str, nargs='+', default=None,
22
+ help='Which plot configurations from config_path yaml to use (default: default)')
23
+ parser.add_argument('--output-location', type=str, default=None,
24
+ help='Location for the output directories (default: flextool root), overridden by output-locations-db-url')
25
+ parser.add_argument('--subdir', type=str, default=None,
26
+ help='Subdirectory for outputs (default: scenario name)')
27
+ parser.add_argument('--read-parquet-dir', type=str, default=False,
28
+ help='Directory to read existing parquet files from (default: False, reads from raw CSV files)')
29
+ parser.add_argument('--write-methods', type=str, nargs='+', default=None,
30
+ choices=['plot', 'parquet', 'excel', 'csv', 'spinedb'],
31
+ help='Output methods to use (default: plot parquet excel)')
32
+ parser.add_argument('--results-db-url', type=str, default=None,
33
+ help='Target SpineDB URL for the spinedb write-method (default: <output-dir>/results.sqlite)')
34
+ parser.add_argument('--plot-rows', type=int, nargs=2, default=None,
35
+ help='First and last row to plot in time series (default: 0 167)')
36
+ parser.add_argument('--plot-file-format', type=str, default=None,
37
+ choices=['png', 'svg'],
38
+ help='File format for plot output (default: png)')
39
+ parser.add_argument('--debug', action='store_true',
40
+ help='Enable debug output')
41
+ parser.add_argument('--only-first-file-per-plot', action='store_true', default=False,
42
+ help='Only produce the first file for each plot (quick overview mode)')
43
+ parser.add_argument('--single-result', type=str, nargs=6,
44
+ metavar=('KEY', 'CSV_NAME', 'PLOT_NAME', 'PLOT_TYPE', 'SUBPLOTS_PER_ROW', 'LEGEND_POSITION'),
45
+ help='Process a single result (overrides --config): key csv_name plot_name plot_type subplots_per_row legend_position. Use "null" for None values.')
46
+
47
+ args = parser.parse_args()
48
+ input_db_url = args.input_db_url
49
+ output_locations_db_url = args.output_locations_db_url
50
+
51
+ # Self-heal missing lightweight settings DBs so fresh clones don't
52
+ # fail opaquely when the user forgot to run `flextool-update`.
53
+ # Only seeds output_info / output_settings / comparison_settings.
54
+ for _candidate in (output_locations_db_url, args.settings_db_url):
55
+ try:
56
+ ensure_settings_db(_candidate)
57
+ except Exception as _exc:
58
+ logging.warning("Failed to auto-seed %s: %s", _candidate, _exc)
59
+ if args.scenario_name:
60
+ scenario_names = [args.scenario_name]
61
+ else:
62
+ scenario_names = []
63
+
64
+ read_parquet_dir=args.read_parquet_dir
65
+ output_location = args.output_location
66
+
67
+ db_map = None
68
+ try:
69
+ if input_db_url:
70
+ with DatabaseMapping(input_db_url) as db_map_tmp:
71
+ scenario_names = [name_from_dict(db_map_tmp.get_filter_configs()[0])]
72
+ elif output_locations_db_url:
73
+ db_map = DatabaseMapping(output_locations_db_url)
74
+ filter_configs = db_map.get_filter_configs()
75
+ if filter_configs:
76
+ alternative_names = filter_configs[0]['alternatives']
77
+ scenario_names = alternative_names
78
+ read_parquet_dir = True
79
+
80
+ if not scenario_names:
81
+ logging.critical("No scenario provided through any of the arguments: scenario-name or output-locations-db-url (or input-db-url by run_flextool.py)")
82
+ sys.exit(1)
83
+
84
+ if args.subdir:
85
+ subdir = args.subdir
86
+ else:
87
+ subdir = scenario_names[0]
88
+
89
+ for i, scenario_name in enumerate(scenario_names):
90
+ if not input_db_url and output_locations_db_url:
91
+ param_value = db_map.get_parameter_value_item(
92
+ entity_class_name='scenario',
93
+ entity_byname=(scenario_name,),
94
+ parameter_definition_name='output_location',
95
+ alternative_name=scenario_name
96
+ )
97
+ if param_value:
98
+ output_location = param_value['parsed_value']
99
+ else:
100
+ raise FileNotFoundError(f"Could not find output data location directory for scenario {scenario_name} from db {output_locations_db_url}.")
101
+ subdir = scenario_name
102
+
103
+ write_outputs(
104
+ scenario_name=scenario_name,
105
+ output_config_path=args.config_path,
106
+ active_configs=args.active_configs,
107
+ output_funcs=None,
108
+ output_location=output_location,
109
+ subdir=subdir,
110
+ read_parquet_dir=read_parquet_dir,
111
+ write_methods=args.write_methods,
112
+ plot_rows=tuple(args.plot_rows) if args.plot_rows else None,
113
+ debug=args.debug,
114
+ single_result=tuple(args.single_result) if args.single_result else None,
115
+ settings_db_url=args.settings_db_url,
116
+ plot_file_format=args.plot_file_format,
117
+ only_first_file=args.only_first_file_per_plot,
118
+ results_db_url=args.results_db_url,
119
+ )
120
+ finally:
121
+ if db_map is not None:
122
+ db_map.close()
123
+
124
+ if __name__ == '__main__':
125
+ run_tool(main)
@@ -0,0 +1 @@
1
+ """Common utility modules shared across flextool subpackages."""
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env python3
2
+ """Render a memory-shape PNG from a flextool memory-sampler log.
3
+
4
+ Usage:
5
+ plot_mem_shape.py <mem_log_path> <png_path>
6
+
7
+ Plots priv_dirty (live set, red filled area) and rss (blue line) against
8
+ wall time. Legend reports the lower (min) and upper (peak) value of each
9
+ series; no on-canvas text annotations.
10
+ """
11
+ import re
12
+ import sys
13
+
14
+ import matplotlib
15
+
16
+ matplotlib.use("Agg")
17
+ import matplotlib.pyplot as plt
18
+
19
+ _FIELD = re.compile(r"(\w+)=([-+0-9.eE]+)")
20
+
21
+
22
+ def parse(path):
23
+ t, rss, priv = [], [], []
24
+ with open(path) as fh:
25
+ for line in fh:
26
+ if not line.startswith("ts="):
27
+ continue
28
+ f = dict(_FIELD.findall(line))
29
+ try:
30
+ t.append(float(f["mono_s"]))
31
+ rss.append(float(f["rss_gb"]))
32
+ priv.append(float(f["priv_dirty_gb"]))
33
+ except (KeyError, ValueError):
34
+ continue
35
+ return t, rss, priv
36
+
37
+
38
+ def main():
39
+ if len(sys.argv) != 3:
40
+ sys.exit(f"usage: {sys.argv[0]} <mem_log_path> <png_path>")
41
+ mem_path, png_path = sys.argv[1], sys.argv[2]
42
+
43
+ t, rss, priv = parse(mem_path)
44
+ if not t:
45
+ sys.exit(f"no samples parsed from {mem_path}")
46
+
47
+ rss_lo, rss_hi = min(rss), max(rss)
48
+ priv_lo, priv_hi = min(priv), max(priv)
49
+
50
+ fig, ax = plt.subplots(figsize=(14, 6))
51
+
52
+ ax.fill_between(t, priv, color="red", alpha=0.12)
53
+ ax.plot(
54
+ t, priv, color="red", linewidth=1.0,
55
+ label=f"priv_dirty (live set) {priv_lo:.1f}–{priv_hi:.1f} GB",
56
+ )
57
+ #ax.plot(
58
+ # t, rss, color="tab:blue", linewidth=1.0,
59
+ # label=f"rss {rss_lo:.1f}–{rss_hi:.1f} GB",
60
+ #)
61
+
62
+ ax.set_xlim(min(t), max(t))
63
+ ax.set_ylim(0, priv_hi * 1.1)
64
+ ax.set_xlabel("wall time (s)")
65
+ ax.set_ylabel("GB")
66
+ ax.margins(x=0)
67
+ ax.legend(loc="upper right")
68
+
69
+ fig.tight_layout()
70
+ fig.savefig(png_path, dpi=110)
71
+ print(f"wrote {png_path} ({len(t)} samples, "
72
+ f"priv_dirty {priv_lo:.1f}–{priv_hi:.1f} GB, "
73
+ f"rss {rss_lo:.1f}–{rss_hi:.1f} GB)")
74
+
75
+
76
+ if __name__ == "__main__":
77
+ main()