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,459 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import tkinter as tk
|
|
5
|
+
from dataclasses import replace
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from tkinter import messagebox, ttk
|
|
8
|
+
|
|
9
|
+
from flextool.gui.check_tree import CheckTreeController
|
|
10
|
+
from flextool.gui.config_parser import parse_plot_configs
|
|
11
|
+
from flextool.gui.dialogs.file_picker import FilePickerDialog
|
|
12
|
+
from flextool.gui.data_models import PlotSettings, ProjectSettings
|
|
13
|
+
from flextool.gui.settings_io import save_project_settings
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _flextool_root() -> Path:
|
|
19
|
+
"""Return the user's working directory (where project files live).
|
|
20
|
+
|
|
21
|
+
Historically this was the repo root (three levels up from ``gui/``).
|
|
22
|
+
In a wheel install that path is inside ``site-packages/`` and not
|
|
23
|
+
user-relevant; CWD is the workspace.
|
|
24
|
+
"""
|
|
25
|
+
return Path.cwd()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _relative_config_path(abs_path: Path) -> str:
|
|
29
|
+
"""Return the path relative to CWD when possible."""
|
|
30
|
+
try:
|
|
31
|
+
return str(abs_path.relative_to(Path.cwd()))
|
|
32
|
+
except ValueError:
|
|
33
|
+
return str(abs_path)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _PlotSection:
|
|
37
|
+
"""Widget group for one plot-settings section (single or comparison).
|
|
38
|
+
|
|
39
|
+
Manages a ``ttk.LabelFrame`` containing start-time / duration entries,
|
|
40
|
+
a config-file selector, and a list of config checkboxes.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
parent: tk.Misc,
|
|
46
|
+
label: str,
|
|
47
|
+
settings: PlotSettings,
|
|
48
|
+
default_config_file: str,
|
|
49
|
+
show_dispatch: bool = False,
|
|
50
|
+
project_path: Path | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
self._settings = settings
|
|
53
|
+
self._default_config_file = default_config_file
|
|
54
|
+
self._project_path = project_path
|
|
55
|
+
|
|
56
|
+
self.frame = ttk.LabelFrame(parent, text=label, padding=10)
|
|
57
|
+
self.frame.columnconfigure(1, weight=1)
|
|
58
|
+
|
|
59
|
+
# ── Start time ─────────────────────────────────────────────
|
|
60
|
+
row = 0
|
|
61
|
+
ttk.Label(self.frame, text="Start timestep (integer):").grid(
|
|
62
|
+
row=row, column=0, sticky="w", pady=(0, 4),
|
|
63
|
+
)
|
|
64
|
+
self._start_var = tk.StringVar(value=str(settings.start_time))
|
|
65
|
+
start_entry = ttk.Entry(self.frame, textvariable=self._start_var, width=10)
|
|
66
|
+
start_entry.grid(row=row, column=1, sticky="w", padx=(10, 0), pady=(0, 4))
|
|
67
|
+
_register_int_validation(start_entry)
|
|
68
|
+
|
|
69
|
+
# ── Duration ───────────────────────────────────────────────
|
|
70
|
+
row = 1
|
|
71
|
+
ttk.Label(self.frame, text="Duration (number of timesteps):").grid(
|
|
72
|
+
row=row, column=0, sticky="w", pady=(0, 4),
|
|
73
|
+
)
|
|
74
|
+
self._duration_var = tk.StringVar(value=str(settings.duration))
|
|
75
|
+
dur_entry = ttk.Entry(self.frame, textvariable=self._duration_var, width=10)
|
|
76
|
+
dur_entry.grid(row=row, column=1, sticky="w", padx=(10, 0), pady=(0, 4))
|
|
77
|
+
_register_int_validation(dur_entry)
|
|
78
|
+
|
|
79
|
+
# ── Config file selector ───────────────────────────────────
|
|
80
|
+
row = 2
|
|
81
|
+
config_file = settings.config_file or default_config_file
|
|
82
|
+
self._config_path = _resolve_config_path(config_file)
|
|
83
|
+
|
|
84
|
+
config_row = ttk.Frame(self.frame)
|
|
85
|
+
config_row.grid(row=row, column=0, columnspan=2, sticky="ew", pady=(4, 4))
|
|
86
|
+
config_row.columnconfigure(1, weight=1)
|
|
87
|
+
|
|
88
|
+
ttk.Label(config_row, text="Plot config file:").grid(row=0, column=0, sticky="w")
|
|
89
|
+
self._config_label_var = tk.StringVar(
|
|
90
|
+
value=_relative_config_path(self._config_path)
|
|
91
|
+
)
|
|
92
|
+
ttk.Label(config_row, textvariable=self._config_label_var).grid(
|
|
93
|
+
row=0, column=1, sticky="w", padx=(5, 5),
|
|
94
|
+
)
|
|
95
|
+
ttk.Button(config_row, text="Change", command=self._on_change_config).grid(
|
|
96
|
+
row=0, column=2, sticky="e",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# ── Dispatch checkbox (comparison section only) ───────────
|
|
100
|
+
self._dispatch_var: tk.BooleanVar | None = None
|
|
101
|
+
if show_dispatch:
|
|
102
|
+
row = 3
|
|
103
|
+
dispatch_row = ttk.Frame(self.frame)
|
|
104
|
+
dispatch_row.grid(row=row, column=0, columnspan=2, sticky="ew", pady=(4, 2))
|
|
105
|
+
|
|
106
|
+
self._dispatch_var = tk.BooleanVar(value=settings.dispatch_plots)
|
|
107
|
+
ttk.Checkbutton(
|
|
108
|
+
dispatch_row, text="Dispatch plots", variable=self._dispatch_var,
|
|
109
|
+
).pack(side="left")
|
|
110
|
+
|
|
111
|
+
# ── "Just one file per plot" checkbox ─────────────────────
|
|
112
|
+
row = 4 if show_dispatch else 3
|
|
113
|
+
self._only_first_file_var = tk.BooleanVar(value=settings.only_first_file)
|
|
114
|
+
ttk.Checkbutton(
|
|
115
|
+
self.frame, text="Just one file per plot", variable=self._only_first_file_var,
|
|
116
|
+
).grid(row=row, column=0, columnspan=2, sticky="w", pady=(4, 2))
|
|
117
|
+
|
|
118
|
+
# ── Active configs list (Treeview with checkboxes) ─────────
|
|
119
|
+
row += 1
|
|
120
|
+
ttk.Label(self.frame, text="Active configs:").grid(
|
|
121
|
+
row=row, column=0, sticky="w", pady=(4, 2),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
row += 1
|
|
125
|
+
self.frame.rowconfigure(row, weight=1)
|
|
126
|
+
from flextool.gui.ui_metrics import get_metrics
|
|
127
|
+
_metrics = get_metrics(self)
|
|
128
|
+
self._lh = _metrics.lh
|
|
129
|
+
cw = _metrics.cw
|
|
130
|
+
|
|
131
|
+
config_frame = ttk.Frame(self.frame)
|
|
132
|
+
config_frame.grid(row=row, column=0, columnspan=2, sticky="nsew", pady=(0, 4))
|
|
133
|
+
config_frame.columnconfigure(0, weight=1)
|
|
134
|
+
config_frame.rowconfigure(0, weight=1)
|
|
135
|
+
|
|
136
|
+
self._config_tree = ttk.Treeview(
|
|
137
|
+
config_frame,
|
|
138
|
+
columns=("check", "name"),
|
|
139
|
+
show="headings",
|
|
140
|
+
selectmode="extended",
|
|
141
|
+
height=6,
|
|
142
|
+
)
|
|
143
|
+
self._config_tree.heading("check", text="\u25bd")
|
|
144
|
+
self._config_tree.heading("name", text="Config")
|
|
145
|
+
self._config_tree.column("check", width=int(cw * 3.45), minwidth=int(cw * 3.45), stretch=False)
|
|
146
|
+
self._config_tree.column("name", width=cw * 20, minwidth=cw * 10, stretch=True)
|
|
147
|
+
self._config_tree.grid(row=0, column=0, sticky="nsew")
|
|
148
|
+
|
|
149
|
+
config_scroll = ttk.Scrollbar(config_frame, orient="vertical", command=self._config_tree.yview)
|
|
150
|
+
config_scroll.grid(row=0, column=1, sticky="ns")
|
|
151
|
+
self._config_tree.configure(yscrollcommand=config_scroll.set)
|
|
152
|
+
|
|
153
|
+
self._config_check_ctrl = CheckTreeController(
|
|
154
|
+
self._config_tree,
|
|
155
|
+
check_column="check",
|
|
156
|
+
checked_glyph=self._CHECK_ON,
|
|
157
|
+
unchecked_glyph=self._CHECK_OFF,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
self._populate_configs()
|
|
161
|
+
|
|
162
|
+
# ── Public helpers ────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
# Unicode checkbox characters (same as main window)
|
|
165
|
+
_CHECK_ON = "\u25a0" # ■
|
|
166
|
+
_CHECK_OFF = "\u25a1" # □
|
|
167
|
+
|
|
168
|
+
def collect(self) -> PlotSettings:
|
|
169
|
+
"""Return a ``PlotSettings`` built from the current widget state."""
|
|
170
|
+
start = _parse_int(self._start_var.get(), self._settings.start_time)
|
|
171
|
+
duration = _parse_int(self._duration_var.get(), self._settings.duration)
|
|
172
|
+
config_file = _relative_config_path(self._config_path)
|
|
173
|
+
active: list[str] = []
|
|
174
|
+
for item in self._config_tree.get_children():
|
|
175
|
+
values = self._config_tree.item(item, "values")
|
|
176
|
+
if values and values[0] == self._CHECK_ON:
|
|
177
|
+
active.append(values[1])
|
|
178
|
+
dispatch = self._dispatch_var.get() if self._dispatch_var is not None else self._settings.dispatch_plots
|
|
179
|
+
return PlotSettings(
|
|
180
|
+
start_time=start,
|
|
181
|
+
duration=duration,
|
|
182
|
+
config_file=config_file,
|
|
183
|
+
active_configs=active,
|
|
184
|
+
dispatch_plots=dispatch,
|
|
185
|
+
only_first_file=self._only_first_file_var.get(),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# ── Internal ──────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
def _populate_configs(self) -> None:
|
|
191
|
+
"""Populate the config Treeview from the YAML file."""
|
|
192
|
+
for item in self._config_tree.get_children():
|
|
193
|
+
self._config_tree.delete(item)
|
|
194
|
+
|
|
195
|
+
config_names = parse_plot_configs(self._config_path)
|
|
196
|
+
if not config_names:
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
saved_active = self._settings.active_configs
|
|
200
|
+
saved_config_file = self._settings.config_file or self._default_config_file
|
|
201
|
+
current_rel = _relative_config_path(self._config_path)
|
|
202
|
+
is_same_file = (current_rel == saved_config_file)
|
|
203
|
+
|
|
204
|
+
for name in config_names:
|
|
205
|
+
if is_same_file and saved_active:
|
|
206
|
+
check = self._CHECK_ON if name in saved_active else self._CHECK_OFF
|
|
207
|
+
else:
|
|
208
|
+
check = self._CHECK_ON
|
|
209
|
+
self._config_tree.insert("", "end", values=(check, name))
|
|
210
|
+
|
|
211
|
+
def _on_change_config(self) -> None:
|
|
212
|
+
"""Open a file chooser to select a different YAML config file."""
|
|
213
|
+
root = _flextool_root()
|
|
214
|
+
initial_dir = self._config_path.parent
|
|
215
|
+
if not initial_dir.is_dir():
|
|
216
|
+
initial_dir = root
|
|
217
|
+
|
|
218
|
+
# Determine dialog size from the top-level window
|
|
219
|
+
try:
|
|
220
|
+
toplevel = self.frame.winfo_toplevel()
|
|
221
|
+
main_window_width = toplevel.winfo_width()
|
|
222
|
+
screen_height = toplevel.winfo_screenheight()
|
|
223
|
+
except Exception:
|
|
224
|
+
main_window_width = 700
|
|
225
|
+
screen_height = 800
|
|
226
|
+
|
|
227
|
+
picker = FilePickerDialog(
|
|
228
|
+
self.frame,
|
|
229
|
+
title="Select plot config file",
|
|
230
|
+
initialdir=str(initial_dir),
|
|
231
|
+
filetypes=[
|
|
232
|
+
("YAML files", "*.yaml *.yml"),
|
|
233
|
+
("All files", "*"),
|
|
234
|
+
],
|
|
235
|
+
multiple=False,
|
|
236
|
+
width=main_window_width,
|
|
237
|
+
height=int(screen_height * 0.75),
|
|
238
|
+
)
|
|
239
|
+
filepath = picker.result
|
|
240
|
+
if not filepath:
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
new_path = Path(filepath) if not isinstance(filepath, Path) else filepath
|
|
244
|
+
if not new_path.is_file():
|
|
245
|
+
messagebox.showerror(
|
|
246
|
+
"Invalid file",
|
|
247
|
+
f"Cannot read:\n{new_path}",
|
|
248
|
+
parent=self.frame,
|
|
249
|
+
)
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
self._config_path = new_path
|
|
253
|
+
self._config_label_var.set(_relative_config_path(new_path))
|
|
254
|
+
self._populate_configs()
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class PlotDialog(tk.Toplevel):
|
|
258
|
+
"""Modal dialog for configuring plot settings.
|
|
259
|
+
|
|
260
|
+
A "Colors, order..." button (top row) opens the shared per-project
|
|
261
|
+
``plot_settings.yaml`` editor, which applies to ALL plots. Below it
|
|
262
|
+
a single section holds the plot-generation settings (start-time,
|
|
263
|
+
duration, config-file selection, dispatch toggle and active-config
|
|
264
|
+
checkboxes). Single-scenario and comparison plots used to take
|
|
265
|
+
separate config files; they now share ``default_plots.yaml``, so one
|
|
266
|
+
section drives both — its values are written to both the single and
|
|
267
|
+
comparison ``PlotSettings`` (still consumed by separate output
|
|
268
|
+
commands) on OK.
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
def __init__(
|
|
272
|
+
self,
|
|
273
|
+
parent: tk.Tk,
|
|
274
|
+
project_path: Path,
|
|
275
|
+
settings: ProjectSettings,
|
|
276
|
+
) -> None:
|
|
277
|
+
super().__init__(parent)
|
|
278
|
+
self.title("Plot settings")
|
|
279
|
+
self._project_path = project_path
|
|
280
|
+
self._settings = settings
|
|
281
|
+
|
|
282
|
+
# ── Modal behaviour ──────────────────────────────────────
|
|
283
|
+
self.transient(parent)
|
|
284
|
+
self.grab_set()
|
|
285
|
+
|
|
286
|
+
# ── Font metrics for DPI-aware sizing ───────────────────
|
|
287
|
+
from flextool.gui.ui_metrics import get_metrics
|
|
288
|
+
_metrics = get_metrics(self)
|
|
289
|
+
cw: int = _metrics.cw
|
|
290
|
+
lh: int = _metrics.lh
|
|
291
|
+
|
|
292
|
+
# ── Dialog size ──────────────────────────────────────────
|
|
293
|
+
# One section now (was two side-by-side), so roughly half-width.
|
|
294
|
+
self.geometry(f"{cw * 104}x{lh * 42}")
|
|
295
|
+
self.resizable(True, True)
|
|
296
|
+
self.minsize(cw * 76, lh * 28)
|
|
297
|
+
|
|
298
|
+
self._build_widgets()
|
|
299
|
+
|
|
300
|
+
# Close via window-manager "X"
|
|
301
|
+
self.protocol("WM_DELETE_WINDOW", self._on_ok)
|
|
302
|
+
|
|
303
|
+
# Centre on parent
|
|
304
|
+
self.update_idletasks()
|
|
305
|
+
px = parent.winfo_rootx()
|
|
306
|
+
py = parent.winfo_rooty()
|
|
307
|
+
pw = parent.winfo_width()
|
|
308
|
+
ph = parent.winfo_height()
|
|
309
|
+
w = self.winfo_width()
|
|
310
|
+
h = self.winfo_height()
|
|
311
|
+
x = px + (pw - w) // 2
|
|
312
|
+
y = py + (ph - h) // 2
|
|
313
|
+
self.geometry(f"+{x}+{y}")
|
|
314
|
+
|
|
315
|
+
# Block until closed
|
|
316
|
+
parent.wait_window(self)
|
|
317
|
+
|
|
318
|
+
# ── Widget construction ──────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
def _build_widgets(self) -> None:
|
|
321
|
+
self.columnconfigure(0, weight=1)
|
|
322
|
+
self.rowconfigure(1, weight=1) # the settings section expands
|
|
323
|
+
|
|
324
|
+
# ── "Colors, order..." on its own row at the top ─────────
|
|
325
|
+
# ``plot_settings.yaml`` (colors / order / sign) applies to ALL
|
|
326
|
+
# plots — single, comparison and dispatch — so its editor sits in
|
|
327
|
+
# its own row above the (shared) plot-generation settings rather
|
|
328
|
+
# than tucked into a corner.
|
|
329
|
+
top_frame = ttk.Frame(self)
|
|
330
|
+
top_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=(10, 5))
|
|
331
|
+
ttk.Button(
|
|
332
|
+
top_frame, text="Colors, order...", command=self._on_change_colors,
|
|
333
|
+
).pack(side="left")
|
|
334
|
+
|
|
335
|
+
# ── Plot-generation settings (single + comparison share one) ─
|
|
336
|
+
# Single-scenario and comparison plots used to take separate
|
|
337
|
+
# config files; they now share ``default_plots.yaml``, so one
|
|
338
|
+
# section drives both. ``dispatch_plots`` (comparison-only) is
|
|
339
|
+
# carried from the comparison settings into this merged section.
|
|
340
|
+
section_settings = replace(
|
|
341
|
+
self._settings.single_plot_settings,
|
|
342
|
+
dispatch_plots=self._settings.comparison_plot_settings.dispatch_plots,
|
|
343
|
+
)
|
|
344
|
+
self._section = _PlotSection(
|
|
345
|
+
self,
|
|
346
|
+
label="Plot settings:",
|
|
347
|
+
settings=section_settings,
|
|
348
|
+
default_config_file="templates/default_plots.yaml",
|
|
349
|
+
show_dispatch=True,
|
|
350
|
+
project_path=self._project_path,
|
|
351
|
+
)
|
|
352
|
+
self._section.frame.grid(
|
|
353
|
+
row=1, column=0, sticky="nsew", padx=10, pady=(0, 5),
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# ── Button row ───────────────────────────────────────────
|
|
357
|
+
btn_frame = ttk.Frame(self)
|
|
358
|
+
btn_frame.grid(row=2, column=0, sticky="ew", padx=10, pady=(5, 10))
|
|
359
|
+
|
|
360
|
+
ttk.Button(btn_frame, text="OK", width=10, command=self._on_ok).pack(
|
|
361
|
+
side="right",
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# ── Actions ──────────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
def _on_change_colors(self) -> None:
|
|
367
|
+
"""Open the shared per-project plot-settings (colors/order) editor.
|
|
368
|
+
|
|
369
|
+
Always edits the PROJECT's ``plot_settings.yaml`` (seeding it from
|
|
370
|
+
the bundled default if absent), never the bundled package file. No
|
|
371
|
+
re-render is needed here — this dialog has no live preview; the
|
|
372
|
+
edited file is used on the next plot generation.
|
|
373
|
+
"""
|
|
374
|
+
if self._project_path is None:
|
|
375
|
+
messagebox.showinfo(
|
|
376
|
+
"No project",
|
|
377
|
+
"No project is loaded.",
|
|
378
|
+
parent=self,
|
|
379
|
+
)
|
|
380
|
+
return
|
|
381
|
+
|
|
382
|
+
from flextool.gui.dialogs.plot_settings_picker import PlotSettingsPicker
|
|
383
|
+
from flextool.gui.project_utils import seed_plot_settings
|
|
384
|
+
|
|
385
|
+
# No live preview in the batch dialog → no on_apply callback.
|
|
386
|
+
project_file = seed_plot_settings(self._project_path)
|
|
387
|
+
PlotSettingsPicker(self, project_file)
|
|
388
|
+
|
|
389
|
+
def _on_ok(self) -> None:
|
|
390
|
+
"""Save settings and close the dialog."""
|
|
391
|
+
# One section drives both single-scenario and comparison plots;
|
|
392
|
+
# write its values into both PlotSettings (still consumed by
|
|
393
|
+
# separate output commands). ``variant_durations`` is not edited
|
|
394
|
+
# here, so carry each object's existing values forward.
|
|
395
|
+
collected = self._section.collect()
|
|
396
|
+
self._settings.single_plot_settings = replace(
|
|
397
|
+
collected,
|
|
398
|
+
variant_durations=self._settings.single_plot_settings.variant_durations,
|
|
399
|
+
)
|
|
400
|
+
self._settings.comparison_plot_settings = replace(
|
|
401
|
+
collected,
|
|
402
|
+
variant_durations=self._settings.comparison_plot_settings.variant_durations,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
try:
|
|
406
|
+
save_project_settings(self._project_path, self._settings)
|
|
407
|
+
except OSError as exc:
|
|
408
|
+
logger.error("Failed to save plot settings: %s", exc)
|
|
409
|
+
messagebox.showerror(
|
|
410
|
+
"Save error",
|
|
411
|
+
f"Could not save settings:\n{exc}",
|
|
412
|
+
parent=self,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
self.grab_release()
|
|
416
|
+
self.destroy()
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
# ── Helpers ──────────────────────────────────────────────────────────
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _resolve_config_path(config_file: str) -> Path:
|
|
423
|
+
"""Resolve a config file string to an absolute ``Path``.
|
|
424
|
+
|
|
425
|
+
Absolute paths are returned as-is. A bare name like
|
|
426
|
+
``default_plots.yaml`` or the legacy
|
|
427
|
+
``templates/default_plots.yaml`` is resolved against the bundled
|
|
428
|
+
``schemas/`` package data. Any other relative path is
|
|
429
|
+
resolved against the user's CWD.
|
|
430
|
+
"""
|
|
431
|
+
p = Path(config_file)
|
|
432
|
+
if p.is_absolute():
|
|
433
|
+
return p
|
|
434
|
+
if p.name in {"default_plots.yaml", "default_plot_settings.yaml"}:
|
|
435
|
+
from flextool._resources import package_data_path
|
|
436
|
+
return package_data_path(f"schemas/{p.name}")
|
|
437
|
+
return Path.cwd() / p
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _parse_int(text: str, fallback: int) -> int:
|
|
441
|
+
"""Parse *text* as a non-negative integer, returning *fallback* on failure."""
|
|
442
|
+
try:
|
|
443
|
+
value = int(text)
|
|
444
|
+
return max(0, value)
|
|
445
|
+
except (ValueError, TypeError):
|
|
446
|
+
return fallback
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _register_int_validation(entry: ttk.Entry) -> None:
|
|
450
|
+
"""Register a validation callback that only allows digits."""
|
|
451
|
+
vcmd = (entry.winfo_toplevel().register(_validate_int), "%P")
|
|
452
|
+
entry.configure(validate="key", validatecommand=vcmd)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _validate_int(new_value: str) -> bool:
|
|
456
|
+
"""Return ``True`` if *new_value* is empty or consists entirely of digits."""
|
|
457
|
+
if new_value == "":
|
|
458
|
+
return True
|
|
459
|
+
return new_value.isdigit()
|