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.
- flextool/__init__.py +41 -0
- flextool/_mem_sampler.py +193 -0
- flextool/_resources.py +43 -0
- flextool/calibrate/__init__.py +51 -0
- flextool/calibrate/__main__.py +11 -0
- flextool/calibrate/_cli.py +316 -0
- flextool/calibrate/_db_alt.py +166 -0
- flextool/calibrate/_final_outputs.py +110 -0
- flextool/calibrate/_guard.py +151 -0
- flextool/calibrate/_loop.py +558 -0
- flextool/calibrate/_readers.py +223 -0
- flextool/calibrate/_report.py +263 -0
- flextool/calibrate/_sizing.py +699 -0
- flextool/calibrate/_solve.py +134 -0
- flextool/calibrate/_solve_status.py +495 -0
- flextool/cli/__init__.py +9 -0
- flextool/cli/_console.py +51 -0
- flextool/cli/_timing.py +147 -0
- flextool/cli/cmd_execute_flextool_workflow.py +187 -0
- flextool/cli/cmd_export_to_tabular.py +56 -0
- flextool/cli/cmd_import_sensitivities.py +75 -0
- flextool/cli/cmd_migrate_database.py +13 -0
- flextool/cli/cmd_open_results_db.py +269 -0
- flextool/cli/cmd_read_matpower.py +66 -0
- flextool/cli/cmd_read_old_flextool.py +63 -0
- flextool/cli/cmd_read_self_describing_tabular_input.py +50 -0
- flextool/cli/cmd_read_tabular_input.py +81 -0
- flextool/cli/cmd_run_flextool.py +1095 -0
- flextool/cli/cmd_scenario_results.py +284 -0
- flextool/cli/cmd_solve_mps.py +169 -0
- flextool/cli/cmd_update_flextool.py +17 -0
- flextool/cli/cmd_write_outputs.py +125 -0
- flextool/common_utils/__init__.py +1 -0
- flextool/common_utils/plot_mem_shape.py +77 -0
- flextool/common_utils/precision.py +451 -0
- flextool/decomposition/__init__.py +0 -0
- flextool/decomposition/region_decomposition.py +128 -0
- flextool/decomposition/region_filter.py +1261 -0
- flextool/engine_polars/__init__.py +110 -0
- flextool/engine_polars/_axis_enums.py +742 -0
- flextool/engine_polars/_benders.py +3462 -0
- flextool/engine_polars/_block_layout.py +1479 -0
- flextool/engine_polars/_blocks.py +1515 -0
- flextool/engine_polars/_commodity_ladder.py +660 -0
- flextool/engine_polars/_cumulative_invest.py +1165 -0
- flextool/engine_polars/_db_loader.py +153 -0
- flextool/engine_polars/_db_reader.py +127 -0
- flextool/engine_polars/_dc_power_flow.py +445 -0
- flextool/engine_polars/_delay.py +442 -0
- flextool/engine_polars/_derived_arithmetic.py +432 -0
- flextool/engine_polars/_derived_block.py +990 -0
- flextool/engine_polars/_derived_branch.py +769 -0
- flextool/engine_polars/_derived_existing.py +1353 -0
- flextool/engine_polars/_derived_npv.py +1297 -0
- flextool/engine_polars/_derived_params.py +9850 -0
- flextool/engine_polars/_derived_profile.py +881 -0
- flextool/engine_polars/_derived_walks.py +276 -0
- flextool/engine_polars/_determinism.py +70 -0
- flextool/engine_polars/_direct_params.py +2186 -0
- flextool/engine_polars/_dump_csvs.py +1009 -0
- flextool/engine_polars/_emit_arc_unions.py +1631 -0
- flextool/engine_polars/_emit_calc_params.py +729 -0
- flextool/engine_polars/_emit_chain_params.py +709 -0
- flextool/engine_polars/_emit_co2_accumulators.py +400 -0
- flextool/engine_polars/_emit_dispatchers.py +690 -0
- flextool/engine_polars/_emit_energy_margin.py +125 -0
- flextool/engine_polars/_emit_energy_margin_adder.py +290 -0
- flextool/engine_polars/_emit_entity_annual.py +428 -0
- flextool/engine_polars/_emit_inflow_scaling.py +1420 -0
- flextool/engine_polars/_emit_leaf_sets.py +550 -0
- flextool/engine_polars/_emit_lp_scaling.py +665 -0
- flextool/engine_polars/_emit_mid_sets.py +859 -0
- flextool/engine_polars/_emit_pdt_params.py +759 -0
- flextool/engine_polars/_emit_per_solve.py +774 -0
- flextool/engine_polars/_emit_period_calc.py +504 -0
- flextool/engine_polars/_emit_period_params.py +2398 -0
- flextool/engine_polars/_emit_provider_io.py +141 -0
- flextool/engine_polars/_emit_reserve.py +574 -0
- flextool/engine_polars/_emit_solve_time.py +311 -0
- flextool/engine_polars/_emit_solve_writers.py +1249 -0
- flextool/engine_polars/_flex_data_accumulator.py +388 -0
- flextool/engine_polars/_flex_data_provider.py +478 -0
- flextool/engine_polars/_group_slack.py +1253 -0
- flextool/engine_polars/_inmemory_reader.py +140 -0
- flextool/engine_polars/_input_source.py +336 -0
- flextool/engine_polars/_invest_seeds.py +191 -0
- flextool/engine_polars/_native_input_writer.py +100 -0
- flextool/engine_polars/_native_run_model.py +1348 -0
- flextool/engine_polars/_orchestration.py +4314 -0
- flextool/engine_polars/_output_writer.py +439 -0
- flextool/engine_polars/_param_shapes.py +1595 -0
- flextool/engine_polars/_parquet_bundle.py +723 -0
- flextool/engine_polars/_pdt_join.py +167 -0
- flextool/engine_polars/_pdt_lookup.py +547 -0
- flextool/engine_polars/_per_solve_sets.py +335 -0
- flextool/engine_polars/_projection_params.py +2056 -0
- flextool/engine_polars/_provider_keys.py +173 -0
- flextool/engine_polars/_provider_translators.py +225 -0
- flextool/engine_polars/_recursive_solve.py +703 -0
- flextool/engine_polars/_region_filter.py +2508 -0
- flextool/engine_polars/_reserve.py +649 -0
- flextool/engine_polars/_solve_acceptance.py +331 -0
- flextool/engine_polars/_solve_config.py +1001 -0
- flextool/engine_polars/_solve_context.py +885 -0
- flextool/engine_polars/_solve_handoff.py +164 -0
- flextool/engine_polars/_solve_state.py +232 -0
- flextool/engine_polars/_solver_base.py +36 -0
- flextool/engine_polars/_solver_dispatch.py +511 -0
- flextool/engine_polars/_spinedb_reader.py +1165 -0
- flextool/engine_polars/_stochastic.py +593 -0
- flextool/engine_polars/_subprocess_solve.py +1838 -0
- flextool/engine_polars/_timeline.py +1416 -0
- flextool/engine_polars/_vectorize.py +438 -0
- flextool/engine_polars/_warm.py +858 -0
- flextool/engine_polars/autoscale/__init__.py +107 -0
- flextool/engine_polars/autoscale/_config.py +218 -0
- flextool/engine_polars/autoscale/_layer2.py +1253 -0
- flextool/engine_polars/autoscale/_layer2_types.py +584 -0
- flextool/engine_polars/autoscale/_quantity_types.py +621 -0
- flextool/engine_polars/autoscale/_report.py +336 -0
- flextool/engine_polars/chain.py +259 -0
- flextool/engine_polars/input.py +6638 -0
- flextool/engine_polars/model.py +4754 -0
- flextool/env_check.py +388 -0
- flextool/export_to_tabular/__init__.py +5 -0
- flextool/export_to_tabular/db_reader.py +224 -0
- flextool/export_to_tabular/excel_writer.py +3559 -0
- flextool/export_to_tabular/export_settings.yaml +377 -0
- flextool/export_to_tabular/export_to_excel.py +227 -0
- flextool/export_to_tabular/formatting.py +543 -0
- flextool/export_to_tabular/sheet_config.py +876 -0
- flextool/gui/__init__.py +0 -0
- flextool/gui/__main__.py +118 -0
- flextool/gui/calibrate_commands.py +184 -0
- flextool/gui/calibrate_jobs.py +424 -0
- flextool/gui/check_tree.py +142 -0
- flextool/gui/cli_format.py +83 -0
- flextool/gui/config_parser.py +68 -0
- flextool/gui/data_models.py +362 -0
- flextool/gui/db_editor_integration.py +202 -0
- flextool/gui/db_version_check.py +269 -0
- flextool/gui/dialogs/__init__.py +0 -0
- flextool/gui/dialogs/add_dialog.py +1098 -0
- flextool/gui/dialogs/calibrate_dialog.py +1259 -0
- flextool/gui/dialogs/file_picker.py +473 -0
- flextool/gui/dialogs/group_picker.py +299 -0
- flextool/gui/dialogs/migration_consent_dialog.py +106 -0
- flextool/gui/dialogs/migration_progress_dialog.py +237 -0
- flextool/gui/dialogs/plot_dialog.py +459 -0
- flextool/gui/dialogs/plot_settings_picker.py +2184 -0
- flextool/gui/dialogs/project_dialog.py +426 -0
- flextool/gui/dialogs/update_dialog.py +212 -0
- flextool/gui/downsampling.py +88 -0
- flextool/gui/error_handling.py +50 -0
- flextool/gui/execution_manager.py +1715 -0
- flextool/gui/execution_window.py +1377 -0
- flextool/gui/hover_tooltip.py +111 -0
- flextool/gui/input_sources.py +730 -0
- flextool/gui/main_window.py +6181 -0
- flextool/gui/network_graph.py +215 -0
- flextool/gui/output_actions.py +393 -0
- flextool/gui/output_log_window.py +159 -0
- flextool/gui/platform_utils.py +421 -0
- flextool/gui/plot_cache.py +88 -0
- flextool/gui/plot_canvas.py +543 -0
- flextool/gui/plot_config_reader.py +272 -0
- flextool/gui/project_utils.py +100 -0
- flextool/gui/result_viewer.py +4394 -0
- flextool/gui/scenario_key.py +162 -0
- flextool/gui/scenario_lists.py +516 -0
- flextool/gui/settings_io.py +360 -0
- flextool/gui/solve_reader.py +103 -0
- flextool/gui/tree_reorder.py +88 -0
- flextool/gui/ui_metrics.py +420 -0
- flextool/input_derivation/__init__.py +281 -0
- flextool/input_derivation/_commodity_ladder.py +375 -0
- flextool/input_derivation/_commodity_ladder_sets.py +70 -0
- flextool/input_derivation/_dc_power_flow.py +377 -0
- flextool/input_derivation/_method_constants.py +77 -0
- flextool/input_derivation/_process_method.py +258 -0
- flextool/input_derivation/_specs.py +1026 -0
- flextool/input_derivation/_validators.py +321 -0
- flextool/lean_parquet.py +159 -0
- flextool/model_builder/__init__.py +5 -0
- flextool/model_builder/build_model.py +589 -0
- flextool/model_builder/encoding.py +67 -0
- flextool/model_builder/names.py +34 -0
- flextool/model_builder/profiles.py +129 -0
- flextool/plot_outputs/__init__.py +14 -0
- flextool/plot_outputs/axis_helpers.py +355 -0
- flextool/plot_outputs/color_template.py +888 -0
- flextool/plot_outputs/config.py +171 -0
- flextool/plot_outputs/format_helpers.py +345 -0
- flextool/plot_outputs/legend_helpers.py +143 -0
- flextool/plot_outputs/orchestrator.py +1141 -0
- flextool/plot_outputs/perf.py +37 -0
- flextool/plot_outputs/plan.py +1787 -0
- flextool/plot_outputs/plot_bars.py +1510 -0
- flextool/plot_outputs/plot_bars_detail.py +753 -0
- flextool/plot_outputs/plot_lines.py +951 -0
- flextool/plot_outputs/shared_manifest.py +564 -0
- flextool/plot_outputs/subplot_helpers.py +137 -0
- flextool/process_inputs/__init__.py +188 -0
- flextool/process_inputs/import_old_excel_input.json +4159 -0
- flextool/process_inputs/read_matpower.py +451 -0
- flextool/process_inputs/read_old_flextool.py +1288 -0
- flextool/process_inputs/read_self_describing_excel.py +1423 -0
- flextool/process_inputs/read_tabular_with_specification.py +1114 -0
- flextool/process_inputs/write_old_flextool_to_db.py +3077 -0
- flextool/process_inputs/write_self_describing_to_db.py +977 -0
- flextool/process_inputs/write_to_input_db.py +269 -0
- flextool/process_outputs/__init__.py +7 -0
- flextool/process_outputs/_annualize.py +55 -0
- flextool/process_outputs/_inmemory_helpers.py +292 -0
- flextool/process_outputs/_output_meta.py +672 -0
- flextool/process_outputs/calc_capacity_flows.py +107 -0
- flextool/process_outputs/calc_connections.py +136 -0
- flextool/process_outputs/calc_costs.py +260 -0
- flextool/process_outputs/calc_group_flows.py +192 -0
- flextool/process_outputs/calc_slacks.py +103 -0
- flextool/process_outputs/calc_storage_vre.py +160 -0
- flextool/process_outputs/drop_levels.py +208 -0
- flextool/process_outputs/handoff_writers.py +1315 -0
- flextool/process_outputs/out_ancillary.py +544 -0
- flextool/process_outputs/out_capacity.py +179 -0
- flextool/process_outputs/out_costs.py +334 -0
- flextool/process_outputs/out_flowgroup.py +189 -0
- flextool/process_outputs/out_flows.py +301 -0
- flextool/process_outputs/out_group.py +475 -0
- flextool/process_outputs/out_node.py +190 -0
- flextool/process_outputs/persist_realized_slice.py +601 -0
- flextool/process_outputs/process_results.py +24 -0
- flextool/process_outputs/read_highs_solution.py +2256 -0
- flextool/process_outputs/read_parameters.py +1799 -0
- flextool/process_outputs/read_sets.py +1095 -0
- flextool/process_outputs/read_variables.py +553 -0
- flextool/process_outputs/solve_order.py +81 -0
- flextool/process_outputs/spinedb_replay.py +412 -0
- flextool/process_outputs/union_realized_slice.py +224 -0
- flextool/process_outputs/write_outputs.py +1286 -0
- flextool/process_outputs/write_spinedb.py +1267 -0
- flextool/representative_periods/__init__.py +5 -0
- flextool/representative_periods/clustering.py +165 -0
- flextool/representative_periods/force_include.py +563 -0
- flextool/representative_periods/netload.py +365 -0
- flextool/representative_periods/netload_inputs.py +345 -0
- flextool/representative_periods/netload_iterate.py +722 -0
- flextool/representative_periods/preprocess.py +948 -0
- flextool/representative_periods/scenario_stack.py +195 -0
- flextool/representative_periods/weights.py +124 -0
- flextool/scenario_comparison/__init__.py +13 -0
- flextool/scenario_comparison/config_builder.py +158 -0
- flextool/scenario_comparison/constants.py +20 -0
- flextool/scenario_comparison/data_models.py +222 -0
- flextool/scenario_comparison/db_reader.py +399 -0
- flextool/scenario_comparison/dispatch_data.py +1002 -0
- flextool/scenario_comparison/dispatch_mappings.py +205 -0
- flextool/scenario_comparison/dispatch_plots.py +691 -0
- flextool/scenario_comparison/input_entity_colors.py +319 -0
- flextool/scenario_comparison/orchestrator.py +453 -0
- flextool/scenario_comparison/plan_union.py +244 -0
- flextool/scenario_comparison/plot_settings_seed.py +205 -0
- flextool/schemas/AXIS_CONTRACT.md +71 -0
- flextool/schemas/canonical_databases/howto_aggregate_output.json +6225 -0
- flextool/schemas/canonical_databases/howto_connections.json +5606 -0
- flextool/schemas/canonical_databases/howto_demand.json +5518 -0
- flextool/schemas/canonical_databases/howto_hydro_reservoir.json +6239 -0
- flextool/schemas/canonical_databases/howto_hydro_reservoir_with_pump.json +5933 -0
- flextool/schemas/canonical_databases/howto_non_sync_and_curtailment.json +5794 -0
- flextool/schemas/canonical_databases/howto_ramp_and_start_up.json +5707 -0
- flextool/schemas/canonical_databases/howto_stochastics.json +6032 -0
- flextool/schemas/canonical_databases/templates_examples.json +13532 -0
- flextool/schemas/canonical_databases/templates_time_settings_only.json +5340 -0
- flextool/schemas/comparison_settings_template.json +197 -0
- flextool/schemas/default_plot_settings.yaml +260 -0
- flextool/schemas/default_plots.yaml +2293 -0
- flextool/schemas/flextool_axis_contract.json +303 -0
- flextool/schemas/flextool_axis_contract.schema.json +247 -0
- flextool/schemas/old_flextool_import_template.json +4443 -0
- flextool/schemas/output_info_template.json +48 -0
- flextool/schemas/output_settings_template.json +256 -0
- flextool/schemas/pre_v26/flextool_template_constant_default.json +2105 -0
- flextool/schemas/pre_v26/flextool_template_default_optional_output.json +2152 -0
- flextool/schemas/pre_v26/flextool_template_default_value.json +2094 -0
- flextool/schemas/pre_v26/flextool_template_drop_down.json +2080 -0
- flextool/schemas/pre_v26/flextool_template_lifetime_method.json +1990 -0
- flextool/schemas/pre_v26/flextool_template_optional_outputs.json +2094 -0
- flextool/schemas/pre_v26/flextool_template_output_node_flows.json +2105 -0
- flextool/schemas/pre_v26/flextool_template_results_master.json +493 -0
- flextool/schemas/pre_v26/flextool_template_rolling_start_remove.json +2087 -0
- flextool/schemas/pre_v26/flextool_template_rolling_window.json +2059 -0
- flextool/schemas/pre_v26/flextool_template_storage_binding_defaults.json +46 -0
- flextool/schemas/pre_v26/flextool_template_v2.json +1990 -0
- flextool/schemas/pre_v26/flextool_template_v25.json +3864 -0
- flextool/schemas/spinedb_results_schema.json +581 -0
- flextool/schemas/spinedb_schema.json +4636 -0
- flextool/solver_config/copt.opt.template +18 -0
- flextool/solver_config/cplex.opt.template +25 -0
- flextool/solver_config/gurobi.opt.template +18 -0
- flextool/solver_config/highs.opt.template +18 -0
- flextool/solver_config/xpress.opt.template +26 -0
- flextool/spinedb_backend/__init__.py +26 -0
- flextool/spinedb_backend/_axis_enums.py +1119 -0
- flextool/spinedb_backend/_backend.py +1139 -0
- flextool/update_flextool/__init__.py +12 -0
- flextool/update_flextool/canonical_databases.py +251 -0
- flextool/update_flextool/db_migration.py +7108 -0
- flextool/update_flextool/ensure_settings_db.py +138 -0
- flextool/update_flextool/export_database.py +103 -0
- flextool/update_flextool/extend_tests_fixture.py +772 -0
- flextool/update_flextool/generate_canonical.py +274 -0
- flextool/update_flextool/initialize_database.py +42 -0
- flextool/update_flextool/install_info.py +225 -0
- flextool/update_flextool/self_update.py +464 -0
- flextool/update_flextool/sync_master_json_template.py +125 -0
- flextool/update_flextool/test_fixtures.py +187 -0
- flextool-4.0.0.dist-info/METADATA +217 -0
- flextool-4.0.0.dist-info/RECORD +322 -0
- flextool-4.0.0.dist-info/WHEEL +5 -0
- flextool-4.0.0.dist-info/entry_points.txt +17 -0
- flextool-4.0.0.dist-info/licenses/LICENSE.txt +19 -0
- 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()
|