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,319 @@
|
|
|
1
|
+
"""Upfront seeding of input-DB entity colors into ``plot_settings.yaml``.
|
|
2
|
+
|
|
3
|
+
The dispatch pipeline already seeds the *dispatch* entities it discovers
|
|
4
|
+
(:mod:`flextool.scenario_comparison.config_builder` /
|
|
5
|
+
:mod:`flextool.scenario_comparison.orchestrator`). This module covers the
|
|
6
|
+
*regular* (non-dispatch) plot pipeline: when a GUI scenario-execution batch
|
|
7
|
+
starts, do one upfront fetch of ALL entities in the relevant entity classes
|
|
8
|
+
from each project INPUT DB and additively seed their default-palette colors
|
|
9
|
+
into the project's ``plot_settings.yaml``. The normal time-series / stack
|
|
10
|
+
plots then get a complete, editable color list, and the user can reorder /
|
|
11
|
+
recolor while the scenarios run.
|
|
12
|
+
|
|
13
|
+
Design (decided with the user):
|
|
14
|
+
|
|
15
|
+
* **Upfront, at job start** — source is the INPUT DB, not the output parquet.
|
|
16
|
+
* **One DB fetch of ALL entities** in the relevant classes per distinct input
|
|
17
|
+
DB — never per scenario / per alternative (DB fetches are slow; one pass
|
|
18
|
+
gets them all). If a batch references multiple distinct input DBs, each
|
|
19
|
+
distinct one is fetched once and the results unioned.
|
|
20
|
+
* **Single writer in the GUI process** — scenarios run as parallel
|
|
21
|
+
subprocesses, so this seeding must happen once, in the GUI thread, before
|
|
22
|
+
(or while) those subprocesses run. This module does not write concurrently.
|
|
23
|
+
|
|
24
|
+
The relevant FlexTool entity classes — verified against the model schema
|
|
25
|
+
(``flextool/schemas/spinedb_schema.json``) and the ``entities`` subsections of
|
|
26
|
+
``schemas/default_plot_settings.yaml`` — are ``unit``, ``connection``,
|
|
27
|
+
``node`` and ``group``: exactly the classes whose entity names appear as plot
|
|
28
|
+
legend labels.
|
|
29
|
+
|
|
30
|
+
This is the GUI-execution path only (the color-editing workflow is GUI). The
|
|
31
|
+
headless CLI path is intentionally out of scope.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import logging
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
from spinedb_api import DatabaseMapping, from_database
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
# ``flowGroup.flow_aggregator`` enum values that request dispatch bands — i.e.
|
|
44
|
+
# the flowGroup aggregates the flows of its members into a dispatch-plot band.
|
|
45
|
+
# ``none`` / ``standalone_aggregator_only`` do NOT draw dispatch bands and so
|
|
46
|
+
# are not dispatch aggregators for the picker's flowGroup display filter.
|
|
47
|
+
_DISPATCH_AGGREGATOR_VALUES = frozenset({"dispatch_plots_only", "both"})
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ``plot_settings.yaml`` ``entities`` subsections that plot legend labels map
|
|
51
|
+
# to. v58 split: ``flowGroup`` is now its own top-level SpineDB class read
|
|
52
|
+
# directly; ``nodeGroup`` is a ``group`` with ``group__node`` membership (see
|
|
53
|
+
# :func:`fetch_entities_by_class`). The rest map 1:1 to top-level classes.
|
|
54
|
+
RELEVANT_ENTITY_CLASSES = ("nodeGroup", "flowGroup", "unit", "connection", "node")
|
|
55
|
+
|
|
56
|
+
# SpineDB membership relationship class that makes a ``group`` a nodeGroup
|
|
57
|
+
# (a collection of nodes — the dispatch subject). A group with no
|
|
58
|
+
# ``group__node`` membership has no nodes to colour and is dropped.
|
|
59
|
+
_NODEGROUP_MEMBERSHIP_CLASSES = (
|
|
60
|
+
"group__node",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def fetch_entities_by_class(db_url: str) -> dict[str, list[str]]:
|
|
65
|
+
"""Open one input DB and return entity names grouped by plot entity class.
|
|
66
|
+
|
|
67
|
+
Opens a single :class:`~spinedb_api.DatabaseMapping` (``create=False``),
|
|
68
|
+
reads ALL entity items in ONE pass (units / connections / nodes / groups /
|
|
69
|
+
flowGroups plus the ``group__node`` membership class), classifies each
|
|
70
|
+
``group`` as a ``nodeGroup`` by its membership, reads ``flowGroup``
|
|
71
|
+
directly, and closes the mapping. Returns ``{class -> sorted unique
|
|
72
|
+
names}`` for the classes in :data:`RELEVANT_ENTITY_CLASSES` that have at
|
|
73
|
+
least one entity. Never queries per scenario / alternative — one fetch
|
|
74
|
+
returns everything.
|
|
75
|
+
|
|
76
|
+
Classification (v58): ``flowGroup`` is read directly from the real
|
|
77
|
+
top-level ``flowGroup`` class; a ``group`` with
|
|
78
|
+
:data:`_NODEGROUP_MEMBERSHIP_CLASSES` (``group__node``) membership is a
|
|
79
|
+
**nodeGroup**. A dual-role entity legitimately appearing in both buckets
|
|
80
|
+
is correct and left as-is (no collision-precedence logic).
|
|
81
|
+
|
|
82
|
+
The base pattern mirrors
|
|
83
|
+
:func:`flextool.export_to_tabular.db_reader._read_entities`
|
|
84
|
+
(``db.get_entity_items()`` grouped by ``entity_class_name``).
|
|
85
|
+
"""
|
|
86
|
+
groups: set[str] = set()
|
|
87
|
+
units: set[str] = set()
|
|
88
|
+
connections: set[str] = set()
|
|
89
|
+
nodes: set[str] = set()
|
|
90
|
+
flow_group_names: set[str] = set()
|
|
91
|
+
node_group_names: set[str] = set()
|
|
92
|
+
|
|
93
|
+
db = DatabaseMapping(db_url, create=False)
|
|
94
|
+
try:
|
|
95
|
+
for item in db.get_entity_items():
|
|
96
|
+
cls = item["entity_class_name"]
|
|
97
|
+
if cls == "group":
|
|
98
|
+
groups.add(item["name"])
|
|
99
|
+
elif cls == "flowGroup":
|
|
100
|
+
flow_group_names.add(item["name"]) # v58 real top-level class
|
|
101
|
+
elif cls == "unit":
|
|
102
|
+
units.add(item["name"])
|
|
103
|
+
elif cls == "connection":
|
|
104
|
+
connections.add(item["name"])
|
|
105
|
+
elif cls == "node":
|
|
106
|
+
nodes.add(item["name"])
|
|
107
|
+
elif cls in _NODEGROUP_MEMBERSHIP_CLASSES:
|
|
108
|
+
byname = item.get("entity_byname")
|
|
109
|
+
if byname:
|
|
110
|
+
node_group_names.add(byname[0]) # group is the 1st member
|
|
111
|
+
finally:
|
|
112
|
+
db.close()
|
|
113
|
+
|
|
114
|
+
by_class: dict[str, list[str]] = {
|
|
115
|
+
# A group lands in nodeGroup if it has group__node members.
|
|
116
|
+
"nodeGroup": sorted(g for g in groups if g in node_group_names),
|
|
117
|
+
# flowGroup is the real top-level class read directly.
|
|
118
|
+
"flowGroup": sorted(flow_group_names),
|
|
119
|
+
"unit": sorted(units),
|
|
120
|
+
"connection": sorted(connections),
|
|
121
|
+
"node": sorted(nodes),
|
|
122
|
+
}
|
|
123
|
+
return {cls: names for cls, names in by_class.items() if names}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _decode_flow_aggregator_value(item) -> str | None:
|
|
127
|
+
"""Best-effort decode of a ``flow_aggregator`` parameter value to a string.
|
|
128
|
+
|
|
129
|
+
``flow_aggregator`` is a single-valued enum method, so its stored value is a
|
|
130
|
+
plain string (spinedb JSON-encodes it as ``b'"both"'`` with ``type is
|
|
131
|
+
None``). Prefer the already-decoded ``parsed_value`` when the mapping
|
|
132
|
+
provides one, else decode the raw ``(value, type)`` via
|
|
133
|
+
:func:`~spinedb_api.from_database`. Defensively unwrap a ``{"type": ...,
|
|
134
|
+
"data"/"value": ...}`` JSON wrapper and a bytes payload so a comparison
|
|
135
|
+
always runs against a bare ``str``. Returns ``None`` (never raises) when the
|
|
136
|
+
value is missing / unparseable / not a string.
|
|
137
|
+
"""
|
|
138
|
+
value = None
|
|
139
|
+
# ``parsed_value`` (when present) is already decoded to a Python object.
|
|
140
|
+
try:
|
|
141
|
+
value = item["parsed_value"]
|
|
142
|
+
except (KeyError, TypeError, IndexError):
|
|
143
|
+
value = None
|
|
144
|
+
if value is None:
|
|
145
|
+
try:
|
|
146
|
+
value = from_database(item["value"], item["type"])
|
|
147
|
+
except Exception:
|
|
148
|
+
return None
|
|
149
|
+
# Defensively unwrap a dict / bytes payload down to a plain string.
|
|
150
|
+
if isinstance(value, dict):
|
|
151
|
+
value = value.get("data", value.get("value"))
|
|
152
|
+
if isinstance(value, bytes):
|
|
153
|
+
try:
|
|
154
|
+
value = value.decode("utf-8")
|
|
155
|
+
except Exception:
|
|
156
|
+
return None
|
|
157
|
+
return value if isinstance(value, str) else None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def fetch_flow_aggregator_flowgroups(db_url: str) -> set[str]:
|
|
161
|
+
"""Return flowGroup names whose ``flow_aggregator`` requests dispatch bands.
|
|
162
|
+
|
|
163
|
+
Opens one :class:`~spinedb_api.DatabaseMapping` (``create=False``), reads the
|
|
164
|
+
``flowGroup`` class' ``flow_aggregator`` parameter values (via
|
|
165
|
+
:meth:`DatabaseMapping.find_parameter_values`, the same filtered read
|
|
166
|
+
:mod:`flextool.input_derivation._validators` uses), decodes each value
|
|
167
|
+
(:func:`_decode_flow_aggregator_value`) and collects the flowGroup names
|
|
168
|
+
whose value is ``dispatch_plots_only`` or ``both`` — i.e. the dispatch
|
|
169
|
+
aggregators. Non-aggregators (``none`` / ``standalone_aggregator_only`` /
|
|
170
|
+
absent) are excluded.
|
|
171
|
+
|
|
172
|
+
Robust by construction: a missing class / parameter, an unparseable value,
|
|
173
|
+
an un-openable or already-closed DB all contribute nothing and NEVER raise —
|
|
174
|
+
the caller gets whatever could be read (possibly an empty set).
|
|
175
|
+
"""
|
|
176
|
+
result: set[str] = set()
|
|
177
|
+
try:
|
|
178
|
+
db = DatabaseMapping(db_url, create=False)
|
|
179
|
+
except Exception as exc: # un-openable / missing DB
|
|
180
|
+
logger.warning("Cannot open %s for flow_aggregator read: %s", db_url, exc)
|
|
181
|
+
return result
|
|
182
|
+
try:
|
|
183
|
+
try:
|
|
184
|
+
items = db.find_parameter_values(
|
|
185
|
+
entity_class_name="flowGroup",
|
|
186
|
+
parameter_definition_name="flow_aggregator",
|
|
187
|
+
)
|
|
188
|
+
except Exception as exc: # missing class / parameter
|
|
189
|
+
logger.warning(
|
|
190
|
+
"Cannot read flow_aggregator from %s: %s", db_url, exc,
|
|
191
|
+
)
|
|
192
|
+
items = []
|
|
193
|
+
for pv in items:
|
|
194
|
+
if _decode_flow_aggregator_value(pv) not in _DISPATCH_AGGREGATOR_VALUES:
|
|
195
|
+
continue
|
|
196
|
+
try:
|
|
197
|
+
byname = pv["entity_byname"]
|
|
198
|
+
except (KeyError, TypeError):
|
|
199
|
+
byname = None
|
|
200
|
+
if byname:
|
|
201
|
+
result.add(byname[0]) # flowGroup is the single dimension
|
|
202
|
+
finally:
|
|
203
|
+
try:
|
|
204
|
+
db.close()
|
|
205
|
+
except Exception:
|
|
206
|
+
pass
|
|
207
|
+
return result
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _url_to_path(db_url: str) -> Path | None:
|
|
211
|
+
"""Return the on-disk path for a ``sqlite:///`` URL, else ``None``.
|
|
212
|
+
|
|
213
|
+
Non-sqlite URLs (which FlexTool does not currently use for input DBs)
|
|
214
|
+
return ``None`` and are skipped by the existence check.
|
|
215
|
+
"""
|
|
216
|
+
prefix = "sqlite:///"
|
|
217
|
+
if db_url.startswith(prefix):
|
|
218
|
+
return Path(db_url[len(prefix):])
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def seed_input_entity_colors(
|
|
223
|
+
project_path: Path,
|
|
224
|
+
input_db_urls: list[str],
|
|
225
|
+
) -> bool:
|
|
226
|
+
"""Fetch input-DB entities and seed (add + prune) their colors in settings.
|
|
227
|
+
|
|
228
|
+
For each DISTINCT *input_db_url* whose backing sqlite file exists on disk,
|
|
229
|
+
fetch all entities in the relevant classes (one DB open each), union the
|
|
230
|
+
results across DBs, assign default palette colors with the same logic the
|
|
231
|
+
dispatch seeding uses
|
|
232
|
+
(:func:`flextool.scenario_comparison.config_builder.assign_palette_colors`),
|
|
233
|
+
and write them into the project ``plot_settings.yaml`` via the structured
|
|
234
|
+
writer
|
|
235
|
+
(:func:`flextool.scenario_comparison.plot_settings_seed.seed_colors_into_plot_settings`).
|
|
236
|
+
Because the input DB is the AUTHORITATIVE full entity set, this seed passes
|
|
237
|
+
``prune_entities`` (the full per-class DB name set) so entities no longer
|
|
238
|
+
in the DB are REMOVED as well as new ones added.
|
|
239
|
+
|
|
240
|
+
The project file is resolved via
|
|
241
|
+
:func:`flextool.plot_outputs.color_template.resolve_plot_settings_path`;
|
|
242
|
+
if the resolver returns the bundled default (the project has no own file
|
|
243
|
+
yet) the project copy is seeded first via
|
|
244
|
+
:func:`flextool.gui.project_utils.seed_plot_settings` so the packaged file
|
|
245
|
+
is never written. When the file changes, the color-template cache is
|
|
246
|
+
cleared so any reader in this same process sees the new colors.
|
|
247
|
+
|
|
248
|
+
Returns ``True`` when the project file was modified, ``False`` otherwise
|
|
249
|
+
(no new entities / nothing to seed — the idempotent case).
|
|
250
|
+
|
|
251
|
+
URLs whose backing file does not exist are skipped (e.g. xlsx sources that
|
|
252
|
+
point at a not-yet-created ``intermediate/<stem>.sqlite``). This is a
|
|
253
|
+
known limitation: such sources are seeded later, on their next batch, once
|
|
254
|
+
the intermediate DB has been built.
|
|
255
|
+
"""
|
|
256
|
+
from flextool.plot_outputs.color_template import (
|
|
257
|
+
_clear_cache,
|
|
258
|
+
_default_path,
|
|
259
|
+
resolve_plot_settings_path,
|
|
260
|
+
)
|
|
261
|
+
from flextool.scenario_comparison.config_builder import assign_palette_colors
|
|
262
|
+
from flextool.scenario_comparison.plot_settings_seed import (
|
|
263
|
+
seed_colors_into_plot_settings,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Distinct, existing-on-disk input DBs only.
|
|
267
|
+
seen: set[str] = set()
|
|
268
|
+
distinct_urls: list[str] = []
|
|
269
|
+
for url in input_db_urls:
|
|
270
|
+
if not url or url in seen:
|
|
271
|
+
continue
|
|
272
|
+
seen.add(url)
|
|
273
|
+
path = _url_to_path(url)
|
|
274
|
+
if path is None or not path.is_file():
|
|
275
|
+
# xlsx source -> intermediate/<stem>.sqlite not yet created; skip
|
|
276
|
+
# (known limitation, see docstring).
|
|
277
|
+
continue
|
|
278
|
+
distinct_urls.append(url)
|
|
279
|
+
|
|
280
|
+
if not distinct_urls:
|
|
281
|
+
return False
|
|
282
|
+
|
|
283
|
+
# Union all entities across the distinct input DBs (one fetch each).
|
|
284
|
+
union: dict[str, set[str]] = {cls: set() for cls in RELEVANT_ENTITY_CLASSES}
|
|
285
|
+
for url in distinct_urls:
|
|
286
|
+
per_db = fetch_entities_by_class(url)
|
|
287
|
+
for cls, names in per_db.items():
|
|
288
|
+
union[cls].update(names)
|
|
289
|
+
|
|
290
|
+
entity_colors: dict[str, dict[str, str]] = {}
|
|
291
|
+
prune_entities: dict[str, set[str]] = {}
|
|
292
|
+
for cls in RELEVANT_ENTITY_CLASSES:
|
|
293
|
+
names = sorted(union[cls])
|
|
294
|
+
if names:
|
|
295
|
+
entity_colors[cls] = assign_palette_colors(names)
|
|
296
|
+
# The input DB is the AUTHORITATIVE full entity set per class: prune
|
|
297
|
+
# any entity in the file that the DB no longer has. Pass the full
|
|
298
|
+
# (possibly empty) lowercased name set for every relevant class so a
|
|
299
|
+
# class that lost all its entities is fully cleared.
|
|
300
|
+
prune_entities[cls] = {n.lower() for n in names}
|
|
301
|
+
|
|
302
|
+
if not any(entity_colors.values()):
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
# Resolve to the project's own file; if the resolver handed back the
|
|
306
|
+
# bundled default, seed the project copy first (never write the package
|
|
307
|
+
# file).
|
|
308
|
+
target = resolve_plot_settings_path(project_path)
|
|
309
|
+
if target == Path(_default_path()):
|
|
310
|
+
from flextool.gui.project_utils import seed_plot_settings
|
|
311
|
+
target = seed_plot_settings(project_path)
|
|
312
|
+
|
|
313
|
+
changed = seed_colors_into_plot_settings(
|
|
314
|
+
target, entity_colors, {}, prune_entities=prune_entities
|
|
315
|
+
)
|
|
316
|
+
if changed:
|
|
317
|
+
_clear_cache()
|
|
318
|
+
logger.info("Seeded input-DB entity colors into %s", target)
|
|
319
|
+
return changed
|