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,272 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from flextool.plot_outputs.config import _is_new_format_entry
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
GROUP_NAMES: dict[str, str] = {
|
|
14
|
+
"0": "Infeasibilities",
|
|
15
|
+
"1": "Curtailments",
|
|
16
|
+
"2": "Flows",
|
|
17
|
+
"3": "Costs",
|
|
18
|
+
"4": "Capacities",
|
|
19
|
+
"5": "Emissions",
|
|
20
|
+
"6": "NodeGroups",
|
|
21
|
+
"7": "Nodes",
|
|
22
|
+
"8": "Unit/Conn details",
|
|
23
|
+
"9": "Extras",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class PlotVariant:
|
|
29
|
+
"""A single variant (letter) within a plot entry."""
|
|
30
|
+
|
|
31
|
+
letter: str # e.g., "d", "t", "g", "a", "tf"
|
|
32
|
+
result_key: str # e.g., "node_slack_up_dt_e"
|
|
33
|
+
sub_config: str # e.g., "default", "chunks"
|
|
34
|
+
full_name: str # e.g., "Loss of load (upward slack)"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class PlotEntry:
|
|
39
|
+
"""A second-level entry in the plot tree (e.g., "0.0")."""
|
|
40
|
+
|
|
41
|
+
number: str # e.g., "0.0"
|
|
42
|
+
short_name: str # e.g., "Loss of load (upwar..."
|
|
43
|
+
full_name: str # e.g., "Loss of load (upward slack)"
|
|
44
|
+
variants: list[PlotVariant] = field(default_factory=list)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class PlotGroup:
|
|
49
|
+
"""A first-level group in the plot tree (e.g., "0")."""
|
|
50
|
+
|
|
51
|
+
number: str # e.g., "0"
|
|
52
|
+
name: str # e.g., "Infeasibilities"
|
|
53
|
+
entries: list[PlotEntry] = field(default_factory=list)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class PlotConfigData:
|
|
58
|
+
"""Parsed plot config: tree groups plus top-level metadata.
|
|
59
|
+
|
|
60
|
+
Attributes
|
|
61
|
+
----------
|
|
62
|
+
groups:
|
|
63
|
+
Plot tree as a list of :class:`PlotGroup` objects.
|
|
64
|
+
default_durations:
|
|
65
|
+
Map of variant letter -> default time-range duration. Values are
|
|
66
|
+
either an ``int`` (number of timesteps) or the string ``"all"``
|
|
67
|
+
(resolved at view time to the available data length). Empty if
|
|
68
|
+
the YAML does not define a ``default_duration`` key.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
groups: list[PlotGroup] = field(default_factory=list)
|
|
72
|
+
default_durations: dict[str, int | str] = field(default_factory=dict)
|
|
73
|
+
|
|
74
|
+
# Allow legacy callers that treat the result as a list of groups
|
|
75
|
+
# (iteration, len, indexing, equality with []) to keep working.
|
|
76
|
+
def __iter__(self):
|
|
77
|
+
return iter(self.groups)
|
|
78
|
+
|
|
79
|
+
def __len__(self) -> int:
|
|
80
|
+
return len(self.groups)
|
|
81
|
+
|
|
82
|
+
def __getitem__(self, index):
|
|
83
|
+
return self.groups[index]
|
|
84
|
+
|
|
85
|
+
def __eq__(self, other: object) -> bool:
|
|
86
|
+
if isinstance(other, PlotConfigData):
|
|
87
|
+
return (
|
|
88
|
+
self.groups == other.groups
|
|
89
|
+
and self.default_durations == other.default_durations
|
|
90
|
+
)
|
|
91
|
+
if isinstance(other, list):
|
|
92
|
+
return self.groups == other
|
|
93
|
+
return NotImplemented
|
|
94
|
+
|
|
95
|
+
def __bool__(self) -> bool:
|
|
96
|
+
return bool(self.groups) or bool(self.default_durations)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _derive_variant_letter(result_key: str, sub_config: str,
|
|
101
|
+
sub_value: dict | None = None) -> str:
|
|
102
|
+
"""Derive variant letter from result_key suffix and sub_config name.
|
|
103
|
+
|
|
104
|
+
The variant letter determines the display order and label for each
|
|
105
|
+
plot variant (p=period, h=hourly, w=weekly, a=aggregated).
|
|
106
|
+
|
|
107
|
+
If *sub_value* contains an explicit ``variant`` key, that value is
|
|
108
|
+
used directly.
|
|
109
|
+
"""
|
|
110
|
+
# Explicit override
|
|
111
|
+
if sub_value and 'variant' in sub_value:
|
|
112
|
+
return str(sub_value['variant'])
|
|
113
|
+
if sub_config == 'chunks':
|
|
114
|
+
return 'w'
|
|
115
|
+
if sub_config == 'sum_periods':
|
|
116
|
+
return 'a'
|
|
117
|
+
if sub_config == 'lines':
|
|
118
|
+
return 'h' # lines are always time-based
|
|
119
|
+
# Default: derive from result_key data type
|
|
120
|
+
is_dt = '_dt_' in result_key or result_key.endswith('_dt') or '_gdt_' in result_key
|
|
121
|
+
if is_dt:
|
|
122
|
+
return 'h'
|
|
123
|
+
return 'p'
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _extract_plot_items(
|
|
127
|
+
plots: dict,
|
|
128
|
+
) -> list[tuple[str, str, str, str, str, str]]:
|
|
129
|
+
"""Walk the YAML *plots* dict and yield parsed plot items.
|
|
130
|
+
|
|
131
|
+
Each entry under ``plots:`` is an entry-name key with ``group``/``order``
|
|
132
|
+
fields and nested result_keys.
|
|
133
|
+
|
|
134
|
+
Each item is ``(group, number, variant, human_name, result_key, sub_config)``.
|
|
135
|
+
Entries without ``map_dimensions_for_plots`` are skipped.
|
|
136
|
+
"""
|
|
137
|
+
items: list[tuple[str, str, str, str, str, str]] = []
|
|
138
|
+
for key, value in plots.items():
|
|
139
|
+
if not isinstance(value, dict):
|
|
140
|
+
continue
|
|
141
|
+
if not _is_new_format_entry(value):
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
entry_name = key
|
|
145
|
+
group_num = str(value['group'])
|
|
146
|
+
order_num = str(value['order'])
|
|
147
|
+
number = f"{group_num}.{order_num}"
|
|
148
|
+
|
|
149
|
+
for result_key, rk_value in value.items():
|
|
150
|
+
if result_key in ('group', 'order') or not isinstance(rk_value, dict):
|
|
151
|
+
continue
|
|
152
|
+
for sub_config, sub_value in rk_value.items():
|
|
153
|
+
if not isinstance(sub_value, dict):
|
|
154
|
+
continue
|
|
155
|
+
if 'map_dimensions_for_plots' not in sub_value:
|
|
156
|
+
continue
|
|
157
|
+
variant_letter = _derive_variant_letter(
|
|
158
|
+
result_key, sub_config, sub_value,
|
|
159
|
+
)
|
|
160
|
+
# Use per-variant plot_name if specified, otherwise entry name
|
|
161
|
+
variant_name = sub_value.get('plot_name', entry_name)
|
|
162
|
+
items.append(
|
|
163
|
+
(group_num, number, variant_letter, variant_name,
|
|
164
|
+
result_key, sub_config)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return items
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _parse_default_durations(data: dict) -> dict[str, int | str]:
|
|
171
|
+
"""Extract ``default_duration`` mapping from a parsed YAML doc.
|
|
172
|
+
|
|
173
|
+
Values are either an ``int`` (number of timesteps) or the literal
|
|
174
|
+
string ``"all"`` (resolve at view time to the available data length).
|
|
175
|
+
Anything else is silently dropped.
|
|
176
|
+
"""
|
|
177
|
+
raw = data.get("default_duration")
|
|
178
|
+
if not isinstance(raw, dict):
|
|
179
|
+
return {}
|
|
180
|
+
cleaned: dict[str, int | str] = {}
|
|
181
|
+
for letter, value in raw.items():
|
|
182
|
+
key = str(letter)
|
|
183
|
+
if isinstance(value, bool):
|
|
184
|
+
continue # reject bools (int subclass)
|
|
185
|
+
if isinstance(value, (int, float)):
|
|
186
|
+
cleaned[key] = int(value)
|
|
187
|
+
elif isinstance(value, str) and value.strip().lower() == "all":
|
|
188
|
+
cleaned[key] = "all"
|
|
189
|
+
return cleaned
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def parse_plot_config(config_path: Path) -> PlotConfigData:
|
|
193
|
+
"""Parse a plot config YAML and return the tree structure plus defaults.
|
|
194
|
+
|
|
195
|
+
Returns a :class:`PlotConfigData` with two attributes:
|
|
196
|
+
|
|
197
|
+
* ``groups`` — list of :class:`PlotGroup` objects sorted by group number.
|
|
198
|
+
Each group contains :class:`PlotEntry` objects sorted by entry number,
|
|
199
|
+
and each entry contains :class:`PlotVariant` objects for available
|
|
200
|
+
variant letters. Entries without ``map_dimensions_for_plots`` are
|
|
201
|
+
excluded.
|
|
202
|
+
* ``default_durations`` — mapping of variant letter to default time-range
|
|
203
|
+
duration (``int`` or the sentinel string ``"all"``).
|
|
204
|
+
|
|
205
|
+
The returned object is iterable / indexable / comparable to a list of
|
|
206
|
+
:class:`PlotGroup`, so legacy callers keep working.
|
|
207
|
+
"""
|
|
208
|
+
if not config_path.is_file():
|
|
209
|
+
logger.warning("Plot config file not found: %s", config_path)
|
|
210
|
+
return PlotConfigData()
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
214
|
+
data = yaml.safe_load(f)
|
|
215
|
+
except (yaml.YAMLError, OSError) as exc:
|
|
216
|
+
logger.error("Failed to read plot config %s: %s", config_path, exc)
|
|
217
|
+
return PlotConfigData()
|
|
218
|
+
|
|
219
|
+
if not isinstance(data, dict):
|
|
220
|
+
return PlotConfigData()
|
|
221
|
+
|
|
222
|
+
default_durations = _parse_default_durations(data)
|
|
223
|
+
|
|
224
|
+
plots = data.get("plots")
|
|
225
|
+
if not isinstance(plots, dict):
|
|
226
|
+
return PlotConfigData(default_durations=default_durations)
|
|
227
|
+
|
|
228
|
+
items = _extract_plot_items(plots)
|
|
229
|
+
|
|
230
|
+
# Group by entry number, collecting variants
|
|
231
|
+
# Use dict to preserve insertion order; entries keyed by number
|
|
232
|
+
entry_map: dict[str, PlotEntry] = {}
|
|
233
|
+
for group, number, variant, human_name, result_key, sub_config in items:
|
|
234
|
+
if number not in entry_map:
|
|
235
|
+
entry_map[number] = PlotEntry(
|
|
236
|
+
number=number,
|
|
237
|
+
short_name=human_name,
|
|
238
|
+
full_name=human_name,
|
|
239
|
+
variants=[],
|
|
240
|
+
)
|
|
241
|
+
entry = entry_map[number]
|
|
242
|
+
# Update full_name if this variant has a longer/better name
|
|
243
|
+
# (keep the first one encountered for consistency)
|
|
244
|
+
pv = PlotVariant(
|
|
245
|
+
letter=variant,
|
|
246
|
+
result_key=result_key,
|
|
247
|
+
sub_config=sub_config,
|
|
248
|
+
full_name=human_name,
|
|
249
|
+
)
|
|
250
|
+
# Avoid duplicate variant letters for the same entry
|
|
251
|
+
existing_letters = {v.letter for v in entry.variants}
|
|
252
|
+
if variant not in existing_letters:
|
|
253
|
+
entry.variants.append(pv)
|
|
254
|
+
|
|
255
|
+
# Build group structure
|
|
256
|
+
group_map: dict[str, PlotGroup] = {}
|
|
257
|
+
for number, entry in entry_map.items():
|
|
258
|
+
group_num = number.split(".")[0]
|
|
259
|
+
if group_num not in group_map:
|
|
260
|
+
group_map[group_num] = PlotGroup(
|
|
261
|
+
number=group_num,
|
|
262
|
+
name=GROUP_NAMES.get(group_num, f"Group {group_num}"),
|
|
263
|
+
entries=[],
|
|
264
|
+
)
|
|
265
|
+
group_map[group_num].entries.append(entry)
|
|
266
|
+
|
|
267
|
+
# Sort groups by number, entries by number within each group
|
|
268
|
+
groups = sorted(group_map.values(), key=lambda g: int(g.number))
|
|
269
|
+
for group in groups:
|
|
270
|
+
group.entries.sort(key=lambda e: [int(x) for x in e.number.split(".")])
|
|
271
|
+
|
|
272
|
+
return PlotConfigData(groups=groups, default_durations=default_durations)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
# Subdirectories created inside every new project
|
|
7
|
+
PROJECT_SUBDIRS = [
|
|
8
|
+
"input_sources",
|
|
9
|
+
"converted",
|
|
10
|
+
"intermediate",
|
|
11
|
+
"work",
|
|
12
|
+
"output_plots",
|
|
13
|
+
"output_parquet",
|
|
14
|
+
"output_csv",
|
|
15
|
+
"output_excel",
|
|
16
|
+
"output_plot_comparisons",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_projects_dir() -> Path:
|
|
21
|
+
"""Return the path to the top-level ``projects/`` directory.
|
|
22
|
+
|
|
23
|
+
Resolved against the user's current working directory. In an
|
|
24
|
+
editable install with the GUI launched from the repo root this is
|
|
25
|
+
the historical ``<repo>/projects/``; in a wheel install the user is
|
|
26
|
+
expected to launch ``flextool-gui`` from their workspace, where
|
|
27
|
+
``./projects/`` is created on demand.
|
|
28
|
+
"""
|
|
29
|
+
return Path.cwd() / "projects"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def seed_plot_settings(project_path: Path) -> Path:
|
|
33
|
+
"""Ensure ``<project_path>/plot_settings.yaml`` exists.
|
|
34
|
+
|
|
35
|
+
Seeds the per-project plot color template from the bundled default so
|
|
36
|
+
the project owns an editable copy. Never clobbers an existing file
|
|
37
|
+
(preserves user edits). Returns the path to the project file.
|
|
38
|
+
"""
|
|
39
|
+
plot_settings_path = Path(project_path) / "plot_settings.yaml"
|
|
40
|
+
if not plot_settings_path.exists():
|
|
41
|
+
from flextool._resources import package_data_path
|
|
42
|
+
bundled = package_data_path("schemas/default_plot_settings.yaml")
|
|
43
|
+
shutil.copy2(bundled, plot_settings_path)
|
|
44
|
+
return plot_settings_path
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def create_project(name: str) -> Path:
|
|
48
|
+
"""Create a new project directory with all required subdirectories.
|
|
49
|
+
|
|
50
|
+
Returns the path to the newly created project directory.
|
|
51
|
+
"""
|
|
52
|
+
projects_dir = get_projects_dir()
|
|
53
|
+
project_path = projects_dir / name
|
|
54
|
+
project_path.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
|
|
56
|
+
for subdir in PROJECT_SUBDIRS:
|
|
57
|
+
(project_path / subdir).mkdir(exist_ok=True)
|
|
58
|
+
|
|
59
|
+
# Seed the per-project plot color template from the bundled default so
|
|
60
|
+
# the project owns an editable copy.
|
|
61
|
+
seed_plot_settings(project_path)
|
|
62
|
+
|
|
63
|
+
return project_path
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def list_projects() -> list[str]:
|
|
67
|
+
"""Return a sorted list of project directory names.
|
|
68
|
+
|
|
69
|
+
Returns an empty list if the projects directory does not exist.
|
|
70
|
+
"""
|
|
71
|
+
projects_dir = get_projects_dir()
|
|
72
|
+
if not projects_dir.exists():
|
|
73
|
+
return []
|
|
74
|
+
return sorted(
|
|
75
|
+
entry.name
|
|
76
|
+
for entry in projects_dir.iterdir()
|
|
77
|
+
if entry.is_dir()
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def rename_project(old_name: str, new_name: str) -> Path:
|
|
82
|
+
"""Rename a project directory.
|
|
83
|
+
|
|
84
|
+
Returns the path to the renamed project directory.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
FileNotFoundError: If the source project does not exist.
|
|
88
|
+
FileExistsError: If a project with the new name already exists.
|
|
89
|
+
"""
|
|
90
|
+
projects_dir = get_projects_dir()
|
|
91
|
+
old_path = projects_dir / old_name
|
|
92
|
+
new_path = projects_dir / new_name
|
|
93
|
+
|
|
94
|
+
if not old_path.exists():
|
|
95
|
+
raise FileNotFoundError(f"Project '{old_name}' does not exist.")
|
|
96
|
+
if new_path.exists():
|
|
97
|
+
raise FileExistsError(f"Project '{new_name}' already exists.")
|
|
98
|
+
|
|
99
|
+
old_path.rename(new_path)
|
|
100
|
+
return new_path
|