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,453 @@
|
|
|
1
|
+
"""Top-level orchestration: ties together data loading, config, and plotting."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
from flextool.lean_parquet import write_lean_parquet
|
|
12
|
+
|
|
13
|
+
from flextool.scenario_comparison.config_builder import (
|
|
14
|
+
assign_palette_colors,
|
|
15
|
+
discover_dispatch_entities,
|
|
16
|
+
)
|
|
17
|
+
from flextool.scenario_comparison.data_models import DispatchMappings
|
|
18
|
+
from flextool.scenario_comparison.db_reader import get_scenario_results
|
|
19
|
+
from flextool.scenario_comparison.dispatch_mappings import (
|
|
20
|
+
combine_dispatch_mappings,
|
|
21
|
+
data_scenario_tags,
|
|
22
|
+
)
|
|
23
|
+
from flextool.scenario_comparison.dispatch_plots import create_dispatch_plots
|
|
24
|
+
from flextool.scenario_comparison.plan_union import (
|
|
25
|
+
derive_comparison_config,
|
|
26
|
+
has_comparison_view,
|
|
27
|
+
)
|
|
28
|
+
from flextool.plot_outputs.config import _is_single_config, flatten_new_format
|
|
29
|
+
from flextool.plot_outputs.orchestrator import plot_dict_of_dataframes
|
|
30
|
+
from flextool.plot_outputs.color_template import resolve_plot_settings_path
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _seed_dispatch_colors_into_plot_settings(
|
|
34
|
+
project_path: Path,
|
|
35
|
+
color_path: Path,
|
|
36
|
+
mappings: DispatchMappings,
|
|
37
|
+
dispatch_scenarios: list[str],
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Auto-seed discovered dispatch entity / scenario colors additively.
|
|
40
|
+
|
|
41
|
+
Relocates ``config_builder``'s color-seeding intent from ``config.yaml``
|
|
42
|
+
to the project ``plot_settings.yaml``: discover the run's group / unit /
|
|
43
|
+
connection / scenario names, assign each a default palette color, and
|
|
44
|
+
splice the genuinely-new ones into the project settings file without
|
|
45
|
+
disturbing existing content.
|
|
46
|
+
|
|
47
|
+
When *color_path* is the bundled fallback (the project has no own
|
|
48
|
+
``plot_settings.yaml`` yet), seed the project copy first via
|
|
49
|
+
:func:`flextool.gui.project_utils.seed_plot_settings` so we never write
|
|
50
|
+
the packaged default. Non-fatal: any failure is logged and the run
|
|
51
|
+
continues.
|
|
52
|
+
"""
|
|
53
|
+
from flextool.plot_outputs.color_template import (
|
|
54
|
+
_clear_cache,
|
|
55
|
+
_default_path,
|
|
56
|
+
)
|
|
57
|
+
from flextool.scenario_comparison.plot_settings_seed import (
|
|
58
|
+
seed_colors_into_plot_settings,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
discovered = discover_dispatch_entities(mappings, dispatch_scenarios)
|
|
63
|
+
entity_colors: dict[str, dict[str, str]] = {}
|
|
64
|
+
for cls in ("nodeGroup", "flowGroup", "unit", "connection"):
|
|
65
|
+
names = discovered.get(cls) or []
|
|
66
|
+
if names:
|
|
67
|
+
entity_colors[cls] = assign_palette_colors(names)
|
|
68
|
+
scenario_colors = assign_palette_colors(
|
|
69
|
+
discovered.get("scenarios") or [], use_tab10=True
|
|
70
|
+
)
|
|
71
|
+
if not any(entity_colors.values()) and not scenario_colors:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
# Resolve to the project's own file; if the resolver handed back the
|
|
75
|
+
# bundled default, seed the project copy first (never write the
|
|
76
|
+
# package file).
|
|
77
|
+
target = Path(color_path)
|
|
78
|
+
if target == Path(_default_path()):
|
|
79
|
+
from flextool.gui.project_utils import seed_plot_settings
|
|
80
|
+
target = seed_plot_settings(project_path)
|
|
81
|
+
|
|
82
|
+
changed = seed_colors_into_plot_settings(
|
|
83
|
+
target, entity_colors, scenario_colors
|
|
84
|
+
)
|
|
85
|
+
if changed:
|
|
86
|
+
# The settings file changed on disk — drop the mtime-keyed cache
|
|
87
|
+
# so downstream readers in this same process see the new colors.
|
|
88
|
+
_clear_cache()
|
|
89
|
+
print(f"Seeded dispatch colors into {target}")
|
|
90
|
+
except Exception as exc: # noqa: BLE001 - seeding must never abort a run
|
|
91
|
+
import logging as _logging
|
|
92
|
+
_logging.warning(
|
|
93
|
+
"Auto-seeding plot_settings.yaml failed (non-fatal): %s", exc
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def derive_project_root(scenario_folders: dict[str, str] | None) -> Path | None:
|
|
98
|
+
"""Derive the common project root from the scenarios' ``output_location``s.
|
|
99
|
+
|
|
100
|
+
Each scenario's folder (the per-scenario ``output_location`` read from the
|
|
101
|
+
DB, or the parquet base dir in ``--parquet-base-dir`` mode) is, under the
|
|
102
|
+
project-rooted output layout, the project folder itself. The comparison
|
|
103
|
+
run's project root is therefore the common ancestor of those folders.
|
|
104
|
+
|
|
105
|
+
"Sensible common project root" predicate
|
|
106
|
+
----------------------------------------
|
|
107
|
+
A sensible project root must be one the scenarios genuinely *share*, never
|
|
108
|
+
a far-up directory like ``/`` or ``/home`` that merely happens to be a
|
|
109
|
+
common ancestor of folders in different projects. The only such guaranteed
|
|
110
|
+
value is a folder that **all** scenarios report identically: under FlexTool's
|
|
111
|
+
project-rooted output layout every scenario of one project reports the same
|
|
112
|
+
``output_location`` (the project folder), and the ``--parquet-base-dir``
|
|
113
|
+
mode maps every scenario to the same base dir
|
|
114
|
+
(see :func:`build_scenario_folders_from_dir`). So:
|
|
115
|
+
|
|
116
|
+
* if every non-empty folder normalizes to the **same** absolute path, that
|
|
117
|
+
shared folder is the project root;
|
|
118
|
+
* otherwise the scenarios diverge (different projects, or a one-off
|
|
119
|
+
cross-project comparison) and there is **no** sensible common root —
|
|
120
|
+
taking ``commonpath`` here would pick a sibling-parent / ``/home`` / ``/``
|
|
121
|
+
ancestor that belongs to no single project, so we deliberately decline.
|
|
122
|
+
|
|
123
|
+
Returns the resolved project-root :class:`~pathlib.Path`, or ``None`` when
|
|
124
|
+
there is no sensible common root (empty/missing locations, or divergent
|
|
125
|
+
folders) — the caller then falls back to the CWD-relative default. Pure
|
|
126
|
+
path logic; no filesystem access, deterministic.
|
|
127
|
+
"""
|
|
128
|
+
if not scenario_folders:
|
|
129
|
+
return None
|
|
130
|
+
folders = [
|
|
131
|
+
os.path.normpath(os.path.abspath(str(f)))
|
|
132
|
+
for f in scenario_folders.values()
|
|
133
|
+
if f
|
|
134
|
+
]
|
|
135
|
+
if not folders:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
unique = set(folders)
|
|
139
|
+
if len(unique) == 1:
|
|
140
|
+
return Path(next(iter(unique)))
|
|
141
|
+
|
|
142
|
+
# Divergent folders: no folder is shared by all scenarios, so any common
|
|
143
|
+
# ancestor would be a far-up directory belonging to no single project.
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _derive_comparison_settings(plots_flat: dict) -> dict:
|
|
148
|
+
"""Map each leaf config dict through ``derive_comparison_config``.
|
|
149
|
+
|
|
150
|
+
Drops entries with no comparison view — those lacking ``scenario_rule``
|
|
151
|
+
*and* an explicit ``comparison_overrides.map_dimensions_for_plots`` (see
|
|
152
|
+
:func:`~flextool.scenario_comparison.plan_union.has_comparison_view`).
|
|
153
|
+
Produces the same flat-dict shape that ``plot_dict_of_dataframes`` and
|
|
154
|
+
``compute_all_plot_plans`` expect.
|
|
155
|
+
"""
|
|
156
|
+
out: dict = {}
|
|
157
|
+
for rk, cfg_dict in plots_flat.items():
|
|
158
|
+
if not isinstance(cfg_dict, dict):
|
|
159
|
+
continue
|
|
160
|
+
if _is_single_config(cfg_dict):
|
|
161
|
+
# Direct config (no sub_configs)
|
|
162
|
+
if not has_comparison_view(cfg_dict):
|
|
163
|
+
continue
|
|
164
|
+
try:
|
|
165
|
+
out[rk] = derive_comparison_config(cfg_dict)
|
|
166
|
+
except (ValueError, TypeError):
|
|
167
|
+
continue
|
|
168
|
+
continue
|
|
169
|
+
# Named sub-configs
|
|
170
|
+
new_sub: dict = {}
|
|
171
|
+
for sub, leaf in cfg_dict.items():
|
|
172
|
+
if not isinstance(leaf, dict):
|
|
173
|
+
continue
|
|
174
|
+
if not has_comparison_view(leaf):
|
|
175
|
+
continue
|
|
176
|
+
try:
|
|
177
|
+
new_sub[sub] = derive_comparison_config(leaf)
|
|
178
|
+
except (ValueError, TypeError):
|
|
179
|
+
continue
|
|
180
|
+
if new_sub:
|
|
181
|
+
out[rk] = new_sub
|
|
182
|
+
return out
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def run(
|
|
186
|
+
db_url: str | None,
|
|
187
|
+
parquet_subdir: str,
|
|
188
|
+
plot_dir: str,
|
|
189
|
+
output_config_path: str,
|
|
190
|
+
active_configs: list[str],
|
|
191
|
+
plot_rows: list[int],
|
|
192
|
+
write_to_xlsx: bool,
|
|
193
|
+
write_dispatch_xlsx: bool,
|
|
194
|
+
write_to_ods: bool,
|
|
195
|
+
show_plots: bool,
|
|
196
|
+
dispatch_plots: bool,
|
|
197
|
+
plot_file_format: str = 'png',
|
|
198
|
+
scenario_folders: dict[str, str] | None = None,
|
|
199
|
+
excel_dir: str | None = None,
|
|
200
|
+
shared_legend: bool = True,
|
|
201
|
+
only_first_file: bool = False,
|
|
202
|
+
comparison_parquet_dir: str | None = None,
|
|
203
|
+
debug: bool = False,
|
|
204
|
+
plot_dir_is_default: bool = False,
|
|
205
|
+
) -> None:
|
|
206
|
+
"""Run the full scenario-comparison pipeline.
|
|
207
|
+
|
|
208
|
+
Takes already-resolved parameters (CLI > settings DB > defaults)
|
|
209
|
+
and orchestrates: load data → build config → generate plots → write Excel.
|
|
210
|
+
|
|
211
|
+
When *scenario_folders* is provided the database is not queried and
|
|
212
|
+
*db_url* may be ``None``.
|
|
213
|
+
|
|
214
|
+
*plot_dir_is_default* is ``True`` when the caller did not explicitly set
|
|
215
|
+
``--plot-dir`` (it carries the bare CWD-relative default). In that case the
|
|
216
|
+
plot output dir is re-rooted at ``<project_root>/output_plot_comparisons``
|
|
217
|
+
when the participating scenarios share a sensible common project root (see
|
|
218
|
+
:func:`derive_project_root`). An explicit ``--plot-dir`` always wins.
|
|
219
|
+
"""
|
|
220
|
+
with open(output_config_path, 'r', encoding='utf-8') as f:
|
|
221
|
+
settings = yaml.safe_load(f)
|
|
222
|
+
|
|
223
|
+
scenario_folders, results = get_scenario_results(
|
|
224
|
+
db_url=db_url, parquet_subdir=parquet_subdir,
|
|
225
|
+
scenario_folders=scenario_folders,
|
|
226
|
+
)
|
|
227
|
+
combined_dfs = results.to_dict()
|
|
228
|
+
|
|
229
|
+
# Derive the project root from the participating scenarios' output
|
|
230
|
+
# locations. When sensible (see ``derive_project_root``) it roots both the
|
|
231
|
+
# ``plot_settings.yaml`` lookup and — when ``--plot-dir`` was left at its
|
|
232
|
+
# default — the comparison plot output dir, giving Toolbox/GUI parity with
|
|
233
|
+
# the per-project ``plot_settings.yaml``. ``None`` means no sensible common
|
|
234
|
+
# root → fall back to the CWD-relative behaviour unchanged.
|
|
235
|
+
project_root = derive_project_root(scenario_folders)
|
|
236
|
+
|
|
237
|
+
# Re-root the default plot dir at ``<project_root>/output_plot_comparisons``
|
|
238
|
+
# only when the user did not pass an explicit ``--plot-dir`` and a sensible
|
|
239
|
+
# project root exists. An explicit ``--plot-dir`` always wins.
|
|
240
|
+
if plot_dir_is_default and project_root is not None:
|
|
241
|
+
plot_dir = str(project_root / "output_plot_comparisons")
|
|
242
|
+
|
|
243
|
+
os.makedirs(plot_dir, exist_ok=True)
|
|
244
|
+
|
|
245
|
+
# The plot color template comes from the project's ``plot_settings.yaml``
|
|
246
|
+
# when present, else the bundled default. Prefer the derived project root;
|
|
247
|
+
# only when there is no sensible common root do we fall back to the
|
|
248
|
+
# ``plot_dir`` parent (CWD-relative legacy behaviour).
|
|
249
|
+
settings_root = project_root if project_root is not None else Path(plot_dir).parent
|
|
250
|
+
color_path = resolve_plot_settings_path(settings_root)
|
|
251
|
+
|
|
252
|
+
scenarios = list(scenario_folders.keys())
|
|
253
|
+
|
|
254
|
+
# Load and combine dispatch mappings across all scenarios
|
|
255
|
+
if scenario_folders:
|
|
256
|
+
mappings = combine_dispatch_mappings(scenario_folders, parquet_subdir)
|
|
257
|
+
combined_mapping_dfs = {
|
|
258
|
+
k: v for k, v in vars(mappings).items() if v is not None
|
|
259
|
+
}
|
|
260
|
+
else:
|
|
261
|
+
mappings = DispatchMappings()
|
|
262
|
+
combined_mapping_dfs = {}
|
|
263
|
+
|
|
264
|
+
# Folder names (``scenarios``) drive paths, _metadata.json and the
|
|
265
|
+
# viewer-set match, but dispatch slicing keys off the scenario tag
|
|
266
|
+
# embedded in the data — which differs when a folder carries the GUI
|
|
267
|
+
# run-index suffix (folder S2_Dry_1 → tag S2_Dry). Resolve the tags
|
|
268
|
+
# from the data and use them for every dispatch slice.
|
|
269
|
+
dispatch_scenarios = data_scenario_tags(mappings) or scenarios
|
|
270
|
+
|
|
271
|
+
# Derive group_node_df for summary plots (needs 'scenario' as column)
|
|
272
|
+
group_node_combined = combined_mapping_dfs.get('group_node')
|
|
273
|
+
if group_node_combined is not None and not group_node_combined.empty:
|
|
274
|
+
group_node_combined.reset_index()
|
|
275
|
+
|
|
276
|
+
# Additively seed discovered entity / scenario colors into the project
|
|
277
|
+
# ``plot_settings.yaml`` (the durable, comment-preserving colors file the
|
|
278
|
+
# renderers read). This is the sole color/order/scenario source now; the
|
|
279
|
+
# legacy ``config.yaml`` system has been removed.
|
|
280
|
+
if dispatch_plots:
|
|
281
|
+
_seed_dispatch_colors_into_plot_settings(
|
|
282
|
+
settings_root, color_path, mappings, dispatch_scenarios
|
|
283
|
+
)
|
|
284
|
+
# Seeding may have created the project ``plot_settings.yaml`` (when
|
|
285
|
+
# only the bundled default existed); re-resolve so the renderers and
|
|
286
|
+
# the viewer plan use the freshly-seeded project file.
|
|
287
|
+
color_path = resolve_plot_settings_path(settings_root)
|
|
288
|
+
|
|
289
|
+
# Flatten new-format entries to flat result_key mapping for downstream use.
|
|
290
|
+
# Then derive comparison-mode configs from each leaf (single rules +
|
|
291
|
+
# scenario_rule), since the merged ``default_plots.yaml`` carries
|
|
292
|
+
# single-mode rules. Entries without ``scenario_rule`` have no
|
|
293
|
+
# comparison rendering and are dropped here.
|
|
294
|
+
settings['plots'] = _derive_comparison_settings(
|
|
295
|
+
flatten_new_format(settings.get('plots', {}))
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# If shared_legend is disabled, replace 'shared' legend with 'right' in all plot configs
|
|
299
|
+
if not shared_legend:
|
|
300
|
+
for result_name, config_dict in settings['plots'].items():
|
|
301
|
+
if not isinstance(config_dict, dict):
|
|
302
|
+
continue
|
|
303
|
+
if _is_single_config(config_dict):
|
|
304
|
+
# Single config: the dict itself is the plot settings
|
|
305
|
+
if config_dict.get('legend') == 'shared':
|
|
306
|
+
config_dict['legend'] = 'right'
|
|
307
|
+
else:
|
|
308
|
+
# Named configs: iterate sub-dicts
|
|
309
|
+
for config_name, plot_cfg in config_dict.items():
|
|
310
|
+
if isinstance(plot_cfg, dict) and plot_cfg.get('legend') == 'shared':
|
|
311
|
+
plot_cfg['legend'] = 'right'
|
|
312
|
+
|
|
313
|
+
# Load timeline breaks from all scenario parquet dirs.
|
|
314
|
+
# For --parquet-base-dir mode (subdir=''): parquets at base_dir/name/
|
|
315
|
+
# For DB mode (subdir='output_parquet'): parquets at folder/subdir/name/
|
|
316
|
+
from flextool.plot_outputs.format_helpers import load_timeline_breaks
|
|
317
|
+
break_dirs = []
|
|
318
|
+
for name, folder in scenario_folders.items():
|
|
319
|
+
if parquet_subdir:
|
|
320
|
+
break_dirs.append(os.path.join(folder, parquet_subdir, name))
|
|
321
|
+
else:
|
|
322
|
+
break_dirs.append(os.path.join(folder, name))
|
|
323
|
+
break_times = load_timeline_breaks(*break_dirs)
|
|
324
|
+
|
|
325
|
+
# Write combined comparison parquets if output directory specified
|
|
326
|
+
if comparison_parquet_dir:
|
|
327
|
+
os.makedirs(comparison_parquet_dir, exist_ok=True)
|
|
328
|
+
for name, df in combined_dfs.items():
|
|
329
|
+
if not df.empty:
|
|
330
|
+
write_lean_parquet(df, os.path.join(comparison_parquet_dir, f"{name}.parquet"))
|
|
331
|
+
# Also write a metadata file with the scenario list
|
|
332
|
+
import json
|
|
333
|
+
meta = {"scenarios": scenarios}
|
|
334
|
+
with open(os.path.join(comparison_parquet_dir, "_metadata.json"), "w") as f:
|
|
335
|
+
json.dump(meta, f)
|
|
336
|
+
if break_times:
|
|
337
|
+
# Save break times so the viewer can load them
|
|
338
|
+
bt_df = pd.DataFrame({"break_time": list(break_times)})
|
|
339
|
+
write_lean_parquet(bt_df, os.path.join(comparison_parquet_dir, "timeline_breaks.parquet"), index=False)
|
|
340
|
+
print(f"Wrote comparison parquets to: {comparison_parquet_dir}")
|
|
341
|
+
|
|
342
|
+
# Compute and save dispatch metadata (cross-scenario ylims) for the viewer
|
|
343
|
+
if combined_mapping_dfs and comparison_parquet_dir:
|
|
344
|
+
try:
|
|
345
|
+
from flextool.scenario_comparison.dispatch_plots import compute_dispatch_metadata
|
|
346
|
+
# Use same timeline as dispatch plots
|
|
347
|
+
if plot_rows and len(plot_rows) >= 2:
|
|
348
|
+
meta_timeline = (int(plot_rows[0]), int(plot_rows[1]) + 1)
|
|
349
|
+
else:
|
|
350
|
+
meta_timeline = (0, 168)
|
|
351
|
+
dispatch_meta = compute_dispatch_metadata(
|
|
352
|
+
results, mappings, dispatch_scenarios, meta_timeline,
|
|
353
|
+
)
|
|
354
|
+
import json as _json
|
|
355
|
+
meta_path = os.path.join(comparison_parquet_dir, "_dispatch_metadata.json")
|
|
356
|
+
with open(meta_path, "w") as f:
|
|
357
|
+
_json.dump(dispatch_meta, f, indent=2)
|
|
358
|
+
print(f"Wrote dispatch metadata to: {meta_path}")
|
|
359
|
+
except Exception as exc:
|
|
360
|
+
import logging as _logging
|
|
361
|
+
_logging.warning("Dispatch metadata computation failed (non-fatal): %s", exc)
|
|
362
|
+
|
|
363
|
+
# Compute plot plans for the viewer (always in comparison parquet dir)
|
|
364
|
+
if comparison_parquet_dir:
|
|
365
|
+
try:
|
|
366
|
+
from flextool.plot_outputs.orchestrator import compute_all_plot_plans
|
|
367
|
+
compute_all_plot_plans(
|
|
368
|
+
combined_dfs, settings.get('plots', {}), comparison_parquet_dir,
|
|
369
|
+
active_settings=active_configs, plot_rows=plot_rows,
|
|
370
|
+
break_times=break_times,
|
|
371
|
+
strip_scenario_level=False,
|
|
372
|
+
color_path=color_path,
|
|
373
|
+
)
|
|
374
|
+
print("Computed plot plans for viewer")
|
|
375
|
+
except Exception as exc:
|
|
376
|
+
import logging as _logging
|
|
377
|
+
_logging.warning("Plot plan computation failed (non-fatal): %s", exc)
|
|
378
|
+
|
|
379
|
+
# Generate comparison plots from the merged settings derived above.
|
|
380
|
+
plot_dict_of_dataframes(
|
|
381
|
+
combined_dfs, plot_dir, settings['plots'],
|
|
382
|
+
active_settings=active_configs, plot_rows=plot_rows,
|
|
383
|
+
delete_existing_plots=True, plot_file_format=plot_file_format,
|
|
384
|
+
only_first_file=only_first_file,
|
|
385
|
+
break_times=break_times,
|
|
386
|
+
color_path=color_path,
|
|
387
|
+
)
|
|
388
|
+
print(f'\nPlotted comparison of {len(scenario_folders)} scenarios to folder: {plot_dir}')
|
|
389
|
+
|
|
390
|
+
# Generate dispatch plots
|
|
391
|
+
if dispatch_plots:
|
|
392
|
+
if combined_mapping_dfs:
|
|
393
|
+
print("\nGenerating dispatch plots...")
|
|
394
|
+
create_dispatch_plots(
|
|
395
|
+
results, mappings, plot_dir,
|
|
396
|
+
scenarios=dispatch_scenarios,
|
|
397
|
+
show_plot=show_plots,
|
|
398
|
+
write_xlsx=write_dispatch_xlsx,
|
|
399
|
+
break_times=break_times,
|
|
400
|
+
plot_rows=plot_rows,
|
|
401
|
+
debug=debug,
|
|
402
|
+
settings_path=color_path,
|
|
403
|
+
)
|
|
404
|
+
else:
|
|
405
|
+
print("Warning: Cannot generate dispatch plots - missing dispatch mappings")
|
|
406
|
+
|
|
407
|
+
# Write to Excel (combined results)
|
|
408
|
+
if write_to_xlsx:
|
|
409
|
+
# Load rename/export config: use the comparison YAML's rename section
|
|
410
|
+
# if present, otherwise fall back to default_plots.yaml.
|
|
411
|
+
rename_raw = settings.get('rename')
|
|
412
|
+
if not rename_raw:
|
|
413
|
+
try:
|
|
414
|
+
default_plots_path = os.path.join(
|
|
415
|
+
os.path.dirname(output_config_path), 'default_plots.yaml',
|
|
416
|
+
)
|
|
417
|
+
with open(default_plots_path, 'r', encoding='utf-8') as f_dp:
|
|
418
|
+
rename_raw = yaml.safe_load(f_dp).get('rename', {})
|
|
419
|
+
except Exception:
|
|
420
|
+
rename_raw = {}
|
|
421
|
+
|
|
422
|
+
def _parse_rename(entry):
|
|
423
|
+
if isinstance(entry, list) and len(entry) >= 2:
|
|
424
|
+
return str(entry[0]), bool(entry[1])
|
|
425
|
+
return str(entry), True
|
|
426
|
+
|
|
427
|
+
filename = 'compare_' + str(len(scenario_folders)) + '_scens.xlsx'
|
|
428
|
+
target_dir = excel_dir if excel_dir is not None else plot_dir
|
|
429
|
+
excel_path = os.path.join(target_dir, filename)
|
|
430
|
+
# Build list of (sheet_name, df) sorted alphabetically
|
|
431
|
+
sheets: list[tuple[str, pd.DataFrame]] = []
|
|
432
|
+
used_names: set[str] = set()
|
|
433
|
+
for name, df in combined_dfs.items():
|
|
434
|
+
display_name, export = _parse_rename(rename_raw.get(name, name))
|
|
435
|
+
if not export:
|
|
436
|
+
continue
|
|
437
|
+
if (not df.empty) & (len(df) > 0):
|
|
438
|
+
sheet_name = display_name[:31]
|
|
439
|
+
if sheet_name in used_names:
|
|
440
|
+
suffix = 1
|
|
441
|
+
while f"{sheet_name[:28]}_{suffix}" in used_names:
|
|
442
|
+
suffix += 1
|
|
443
|
+
sheet_name = f"{sheet_name[:28]}_{suffix}"
|
|
444
|
+
used_names.add(sheet_name)
|
|
445
|
+
sheets.append((sheet_name, df))
|
|
446
|
+
sheets.sort(key=lambda x: x[0].lower())
|
|
447
|
+
|
|
448
|
+
with pd.ExcelWriter(excel_path, engine='xlsxwriter') as writer:
|
|
449
|
+
for sheet_name, df in sheets:
|
|
450
|
+
df.to_excel(writer, sheet_name=sheet_name)
|
|
451
|
+
print(f'\nWrote comparison of {len(scenario_folders)} scenarios to xlsx file: {excel_path}')
|
|
452
|
+
|
|
453
|
+
print('\nDone!')
|