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,420 @@
|
|
|
1
|
+
"""Central font metrics + scaling helpers for the FlexTool GUI.
|
|
2
|
+
|
|
3
|
+
All windows and dialogs should read sizes through ``get_metrics()`` so a
|
|
4
|
+
single ``setup_fonts()`` call at startup controls the whole UI.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
import re as _re
|
|
10
|
+
import tkinter as tk
|
|
11
|
+
import tkinter.font as tkfont
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_GEOMETRY_RE = _re.compile(
|
|
15
|
+
r"^(?P<w>\d+)x(?P<h>\d+)"
|
|
16
|
+
r"(?:(?P<xs>[+-])(?P<x>\d+)(?P<ys>[+-])(?P<y>\d+))?$"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def clamp_geometry(
|
|
21
|
+
saved: str,
|
|
22
|
+
screen_w: int,
|
|
23
|
+
screen_h: int,
|
|
24
|
+
*,
|
|
25
|
+
min_w: int,
|
|
26
|
+
min_h: int,
|
|
27
|
+
margin: int = 40,
|
|
28
|
+
) -> str | None:
|
|
29
|
+
"""Sanitise a saved Tk geometry string against the current screen.
|
|
30
|
+
|
|
31
|
+
Returns ``None`` if *saved* is empty or malformed. Otherwise:
|
|
32
|
+
|
|
33
|
+
* Width/height are clamped to ``[min_w, screen_w - margin]`` and
|
|
34
|
+
``[min_h, screen_h - margin]``.
|
|
35
|
+
* If an ``+X+Y`` offset is present, it is kept only when the window
|
|
36
|
+
would fit fully on-screen; otherwise the offset is dropped and the
|
|
37
|
+
window is placed at ``+0+0`` (the WM picks the position).
|
|
38
|
+
|
|
39
|
+
The ``margin`` reserves space for taskbars / docks.
|
|
40
|
+
"""
|
|
41
|
+
if not saved:
|
|
42
|
+
return None
|
|
43
|
+
m = _GEOMETRY_RE.match(saved.strip())
|
|
44
|
+
if not m:
|
|
45
|
+
return None
|
|
46
|
+
orig_w = int(m["w"])
|
|
47
|
+
orig_h = int(m["h"])
|
|
48
|
+
w = max(min_w, min(orig_w, max(min_w, screen_w - margin)))
|
|
49
|
+
h = max(min_h, min(orig_h, max(min_h, screen_h - margin)))
|
|
50
|
+
clamped_size = (w != orig_w) or (h != orig_h)
|
|
51
|
+
if m["x"] is None:
|
|
52
|
+
return f"{w}x{h}"
|
|
53
|
+
x = int(m["x"])
|
|
54
|
+
y = int(m["y"])
|
|
55
|
+
if m["xs"] == "-":
|
|
56
|
+
x = -x
|
|
57
|
+
if m["ys"] == "-":
|
|
58
|
+
y = -y
|
|
59
|
+
# Drop the offset if the width/height had to be clamped or the window
|
|
60
|
+
# would run off-screen — saved offsets from a different resolution
|
|
61
|
+
# are not trustworthy in those cases.
|
|
62
|
+
if (
|
|
63
|
+
clamped_size
|
|
64
|
+
or x < 0
|
|
65
|
+
or y < 0
|
|
66
|
+
or x + w > screen_w
|
|
67
|
+
or y + h > screen_h - margin
|
|
68
|
+
):
|
|
69
|
+
return f"{w}x{h}"
|
|
70
|
+
return f"{w}x{h}+{x}+{y}"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def clamp_sash(
|
|
74
|
+
saved_px: int,
|
|
75
|
+
pane_total: int,
|
|
76
|
+
*,
|
|
77
|
+
min_px: int,
|
|
78
|
+
max_frac: float = 0.85,
|
|
79
|
+
) -> int:
|
|
80
|
+
"""Keep a saved sash position inside the current pane.
|
|
81
|
+
|
|
82
|
+
Returns 0 when *saved_px* is 0 or ``pane_total`` is too small to honour
|
|
83
|
+
``min_px`` — callers should treat 0 as "leave default".
|
|
84
|
+
"""
|
|
85
|
+
if saved_px <= 0 or pane_total <= 0:
|
|
86
|
+
return 0
|
|
87
|
+
upper = int(pane_total * max_frac)
|
|
88
|
+
if upper <= min_px:
|
|
89
|
+
return 0
|
|
90
|
+
return max(min_px, min(saved_px, upper))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def rescale_pixels(saved_px: int, saved_cw: int, current_cw: int) -> int:
|
|
94
|
+
"""Rescale a pixel value saved under one font metric to the current one.
|
|
95
|
+
|
|
96
|
+
Returns ``saved_px`` unchanged when ``saved_cw`` is 0 (unknown), when
|
|
97
|
+
the ratio is within 10%, or when either cw is non-positive. Otherwise
|
|
98
|
+
returns ``round(saved_px * current_cw / saved_cw)``.
|
|
99
|
+
|
|
100
|
+
Used to make saved sash positions and (optionally) window dimensions
|
|
101
|
+
survive a DPI change between sessions.
|
|
102
|
+
"""
|
|
103
|
+
if saved_px <= 0 or saved_cw <= 0 or current_cw <= 0:
|
|
104
|
+
return saved_px
|
|
105
|
+
ratio = current_cw / saved_cw
|
|
106
|
+
if 0.90 <= ratio <= 1.10:
|
|
107
|
+
return saved_px
|
|
108
|
+
return round(saved_px * ratio)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def rescale_geometry(saved: str, saved_cw: int, current_cw: int) -> str:
|
|
112
|
+
"""Apply rescale_pixels to W and H in a Tk geometry string."""
|
|
113
|
+
if not saved or saved_cw <= 0 or current_cw <= 0 or saved_cw == current_cw:
|
|
114
|
+
return saved
|
|
115
|
+
m = _GEOMETRY_RE.match(saved.strip())
|
|
116
|
+
if not m:
|
|
117
|
+
return saved
|
|
118
|
+
new_w = rescale_pixels(int(m["w"]), saved_cw, current_cw)
|
|
119
|
+
new_h = rescale_pixels(int(m["h"]), saved_cw, current_cw)
|
|
120
|
+
if m["x"] is None:
|
|
121
|
+
return f"{new_w}x{new_h}"
|
|
122
|
+
xs = m["xs"]
|
|
123
|
+
ys = m["ys"]
|
|
124
|
+
return f"{new_w}x{new_h}{xs}{m['x']}{ys}{m['y']}"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def monitor_signature(window: tk.Misc | None = None) -> str:
|
|
128
|
+
"""Stable key identifying the current monitor *configuration*.
|
|
129
|
+
|
|
130
|
+
Used to remember window placements per monitor layout: a window
|
|
131
|
+
dragged onto the 4K while docked, then the laptop undocked, should not
|
|
132
|
+
have its docked offset reused on the single internal panel. The
|
|
133
|
+
signature changes whenever a monitor is added/removed/moved/resized, so
|
|
134
|
+
each configuration keeps its own saved geometry.
|
|
135
|
+
|
|
136
|
+
Built from the sorted ``WxH+X+Y`` rectangles reported by ``screeninfo``
|
|
137
|
+
(order-independent). Falls back to ``virtual:<W>x<H>`` from the Tk
|
|
138
|
+
virtual-desktop size when ``screeninfo`` is unavailable, and finally to
|
|
139
|
+
``"virtual:unknown"`` so callers always get a usable, hashable key.
|
|
140
|
+
"""
|
|
141
|
+
try:
|
|
142
|
+
from screeninfo import get_monitors
|
|
143
|
+
monitors = list(get_monitors())
|
|
144
|
+
if monitors:
|
|
145
|
+
parts = sorted(
|
|
146
|
+
f"{int(m.width)}x{int(m.height)}+{int(m.x)}+{int(m.y)}"
|
|
147
|
+
for m in monitors
|
|
148
|
+
)
|
|
149
|
+
return "|".join(parts)
|
|
150
|
+
except Exception:
|
|
151
|
+
pass
|
|
152
|
+
if window is not None:
|
|
153
|
+
try:
|
|
154
|
+
return f"virtual:{window.winfo_screenwidth()}x{window.winfo_screenheight()}"
|
|
155
|
+
except tk.TclError:
|
|
156
|
+
pass
|
|
157
|
+
return "virtual:unknown"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def primary_monitor_bounds() -> tuple[int, int, int, int] | None:
|
|
161
|
+
"""Bounds ``(x, y, w, h)`` of the primary monitor, or ``None``.
|
|
162
|
+
|
|
163
|
+
Used to place a window that has no prior position to detect a "current"
|
|
164
|
+
monitor from (e.g. the main window at first launch). Prefers the monitor
|
|
165
|
+
``screeninfo`` flags ``is_primary``; otherwise the first enumerated one.
|
|
166
|
+
Returns ``None`` when ``screeninfo`` is unavailable or enumerates none,
|
|
167
|
+
so callers fall back to virtual-desktop sizing.
|
|
168
|
+
"""
|
|
169
|
+
try:
|
|
170
|
+
from screeninfo import get_monitors
|
|
171
|
+
monitors = list(get_monitors())
|
|
172
|
+
except Exception:
|
|
173
|
+
return None
|
|
174
|
+
if not monitors:
|
|
175
|
+
return None
|
|
176
|
+
chosen = next((m for m in monitors if getattr(m, "is_primary", False)), monitors[0])
|
|
177
|
+
if chosen.width <= 1 or chosen.height <= 1:
|
|
178
|
+
return None
|
|
179
|
+
return (int(chosen.x), int(chosen.y), int(chosen.width), int(chosen.height))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _monitor_bounds_screeninfo(
|
|
183
|
+
window: tk.Misc,
|
|
184
|
+
) -> tuple[int, int, int, int] | None:
|
|
185
|
+
"""Per-monitor rectangle via the ``screeninfo`` package (flicker-free).
|
|
186
|
+
|
|
187
|
+
Picks the monitor whose rectangle contains the centre of *window*; if
|
|
188
|
+
no monitor claims the centre (e.g. the window straddles a gap), falls
|
|
189
|
+
back to the primary monitor, then the first enumerated one. Returns
|
|
190
|
+
``None`` when ``screeninfo`` is missing or enumerates no monitors, so
|
|
191
|
+
the caller can drop to the maximise-probe.
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
from screeninfo import get_monitors
|
|
195
|
+
except Exception:
|
|
196
|
+
return None
|
|
197
|
+
try:
|
|
198
|
+
monitors = list(get_monitors())
|
|
199
|
+
except Exception:
|
|
200
|
+
# screeninfo raises on headless / unusual display backends.
|
|
201
|
+
return None
|
|
202
|
+
if not monitors:
|
|
203
|
+
return None
|
|
204
|
+
try:
|
|
205
|
+
window.update_idletasks()
|
|
206
|
+
cx = window.winfo_x() + window.winfo_width() // 2
|
|
207
|
+
cy = window.winfo_y() + window.winfo_height() // 2
|
|
208
|
+
except tk.TclError:
|
|
209
|
+
cx = cy = None
|
|
210
|
+
chosen = None
|
|
211
|
+
if cx is not None:
|
|
212
|
+
for m in monitors:
|
|
213
|
+
if m.x <= cx < m.x + m.width and m.y <= cy < m.y + m.height:
|
|
214
|
+
chosen = m
|
|
215
|
+
break
|
|
216
|
+
if chosen is None:
|
|
217
|
+
chosen = next((m for m in monitors if getattr(m, "is_primary", False)), None)
|
|
218
|
+
if chosen is None:
|
|
219
|
+
chosen = monitors[0]
|
|
220
|
+
if chosen.width <= 1 or chosen.height <= 1:
|
|
221
|
+
return None
|
|
222
|
+
return (int(chosen.x), int(chosen.y), int(chosen.width), int(chosen.height))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def current_monitor_bounds(window: tk.Misc) -> tuple[int, int, int, int] | None:
|
|
226
|
+
"""Best-effort bounds ``(x, y, w, h)`` of the monitor showing *window*.
|
|
227
|
+
|
|
228
|
+
Tk exposes no per-monitor geometry: ``winfo_screenwidth`` /
|
|
229
|
+
``winfo_screenheight`` report the FULL virtual desktop spanning *every*
|
|
230
|
+
monitor, so sizing a window to those values stretches it across all
|
|
231
|
+
screens (the dual-monitor "window covers both displays" bug).
|
|
232
|
+
|
|
233
|
+
Prefers the ``screeninfo`` package (flicker-free, also yields the
|
|
234
|
+
monitor's ``x``/``y`` origin). When it is unavailable or enumerates no
|
|
235
|
+
monitors, falls back to momentarily maximising *window* — the window
|
|
236
|
+
manager snaps a maximised window to the *current* monitor's usable area
|
|
237
|
+
— then reading the resulting offset/size and restoring normal state.
|
|
238
|
+
|
|
239
|
+
*window* should already be positioned (e.g. via ``geometry("+x+y")``) on
|
|
240
|
+
the target monitor before calling, so both paths pick the right display.
|
|
241
|
+
Returns ``None`` when every approach fails, so callers fall back to
|
|
242
|
+
virtual-desktop sizing (no worse than before).
|
|
243
|
+
|
|
244
|
+
Note: ``screeninfo`` reports the full monitor rectangle, not the WM work
|
|
245
|
+
area, so callers should still reserve taskbar/dock space themselves; the
|
|
246
|
+
maximise-probe path already excludes it.
|
|
247
|
+
"""
|
|
248
|
+
bounds = _monitor_bounds_screeninfo(window)
|
|
249
|
+
if bounds is not None:
|
|
250
|
+
return bounds
|
|
251
|
+
|
|
252
|
+
def _set_maximised(on: bool) -> bool:
|
|
253
|
+
# Windows / macOS use state('zoomed'); most Linux WMs honour the
|
|
254
|
+
# '-zoomed' attribute. Try both; report whether either took.
|
|
255
|
+
try:
|
|
256
|
+
window.tk.call("wm", "state", window._w, "zoomed" if on else "normal")
|
|
257
|
+
return True
|
|
258
|
+
except tk.TclError:
|
|
259
|
+
pass
|
|
260
|
+
try:
|
|
261
|
+
window.tk.call("wm", "attributes", window._w, "-zoomed", "1" if on else "0")
|
|
262
|
+
return True
|
|
263
|
+
except tk.TclError:
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
window.update_idletasks()
|
|
268
|
+
if not _set_maximised(True):
|
|
269
|
+
return None
|
|
270
|
+
window.update_idletasks()
|
|
271
|
+
x = window.winfo_x()
|
|
272
|
+
y = window.winfo_y()
|
|
273
|
+
w = window.winfo_width()
|
|
274
|
+
h = window.winfo_height()
|
|
275
|
+
_set_maximised(False)
|
|
276
|
+
window.update_idletasks()
|
|
277
|
+
if w <= 1 or h <= 1:
|
|
278
|
+
return None
|
|
279
|
+
return (x, y, w, h)
|
|
280
|
+
except tk.TclError:
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def resolve_saved_geometry(
|
|
285
|
+
geom_map: dict[str, str],
|
|
286
|
+
signature: str,
|
|
287
|
+
saved_cw: int,
|
|
288
|
+
current_cw: int,
|
|
289
|
+
screen_w: int,
|
|
290
|
+
screen_h: int,
|
|
291
|
+
*,
|
|
292
|
+
min_w: int,
|
|
293
|
+
min_h: int,
|
|
294
|
+
) -> str | None:
|
|
295
|
+
"""Pick the geometry saved for *signature* and make it safe to apply.
|
|
296
|
+
|
|
297
|
+
Looks up *geom_map* by the current monitor *signature*, falling back to
|
|
298
|
+
a ``"legacy"`` entry migrated from the old single-string format. The
|
|
299
|
+
result is rescaled for any font/DPI change (``rescale_geometry``) and
|
|
300
|
+
clamped to the current screen (``clamp_geometry``). Returns ``None``
|
|
301
|
+
when nothing usable is saved, so callers keep the default placement.
|
|
302
|
+
"""
|
|
303
|
+
if not geom_map:
|
|
304
|
+
return None
|
|
305
|
+
saved = geom_map.get(signature) or geom_map.get("legacy", "")
|
|
306
|
+
if not saved:
|
|
307
|
+
return None
|
|
308
|
+
geom = rescale_geometry(saved, saved_cw, current_cw)
|
|
309
|
+
return clamp_geometry(geom, screen_w, screen_h, min_w=min_w, min_h=min_h)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@dataclass(frozen=True)
|
|
313
|
+
class FontMetrics:
|
|
314
|
+
"""Snapshot of derived font metrics. Cheap to recompute, immutable."""
|
|
315
|
+
cw: int # char width — TkDefaultFont.measure("0")
|
|
316
|
+
lh: int # line height — TkDefaultFont.metrics("linespace")
|
|
317
|
+
em: int # alias for cw, exposed for readability in spacing code
|
|
318
|
+
row_height: int # treeview row height (max(24, int(lh * 1.25)))
|
|
319
|
+
bold_font: tkfont.Font # body font with weight=bold
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def setup_fonts(root: tk.Misc, *, body_pt: int = 10, code_pt: int = 10) -> None:
|
|
323
|
+
"""Configure the six standard Tk named fonts with role-aware sizes.
|
|
324
|
+
|
|
325
|
+
Sets:
|
|
326
|
+
TkDefaultFont / TkTextFont / TkMenuFont = body_pt
|
|
327
|
+
TkHeadingFont = body_pt + 2, bold
|
|
328
|
+
TkTooltipFont = body_pt - 1
|
|
329
|
+
TkFixedFont = code_pt
|
|
330
|
+
|
|
331
|
+
All sizes are POINTS (positive numbers), so tk scaling DPI-scales them.
|
|
332
|
+
|
|
333
|
+
Idempotent: safe to call again to restyle the whole UI at runtime.
|
|
334
|
+
"""
|
|
335
|
+
# body / text / menu — same size, regular weight
|
|
336
|
+
for name in ("TkDefaultFont", "TkTextFont", "TkMenuFont"):
|
|
337
|
+
try:
|
|
338
|
+
font = tkfont.nametofont(name)
|
|
339
|
+
font.configure(size=body_pt, weight="normal")
|
|
340
|
+
except tk.TclError:
|
|
341
|
+
pass
|
|
342
|
+
|
|
343
|
+
# heading — one point larger, bold. Bold weight already reads as a
|
|
344
|
+
# heading next to the regular body; +2 looked oversized on Windows
|
|
345
|
+
# (esp. the "Input sources" / "File outputs" section labels), so +1 is
|
|
346
|
+
# enough lift without dominating the surrounding sv_ttk widgets.
|
|
347
|
+
try:
|
|
348
|
+
heading = tkfont.nametofont("TkHeadingFont")
|
|
349
|
+
heading.configure(size=body_pt + 1, weight="bold")
|
|
350
|
+
except tk.TclError:
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
# tooltip — one point smaller, regular
|
|
354
|
+
try:
|
|
355
|
+
tip = tkfont.nametofont("TkTooltipFont")
|
|
356
|
+
tip.configure(size=max(body_pt - 1, 6), weight="normal")
|
|
357
|
+
except tk.TclError:
|
|
358
|
+
pass
|
|
359
|
+
|
|
360
|
+
# monospace / code
|
|
361
|
+
try:
|
|
362
|
+
fixed = tkfont.nametofont("TkFixedFont")
|
|
363
|
+
# keep the platform-default family; only override size
|
|
364
|
+
fixed.configure(size=code_pt)
|
|
365
|
+
except tk.TclError:
|
|
366
|
+
pass
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def monitor_dpi(widget: tk.Misc) -> float:
|
|
370
|
+
"""Return the current monitor's effective DPI as Tk sees it.
|
|
371
|
+
|
|
372
|
+
``winfo_fpixels('1i')`` returns the pixels-per-inch Tk uses for sizing
|
|
373
|
+
fonts and ttk widgets; it reflects the current value of ``tk scaling``,
|
|
374
|
+
not the OS DPI on the monitor the window happens to be on. Wrapping it
|
|
375
|
+
keeps callers from sprinkling magic strings.
|
|
376
|
+
|
|
377
|
+
Note: for a true per-monitor DPI on Windows, the code path would be
|
|
378
|
+
Win32 ``MonitorFromWindow`` + ``GetDpiForMonitor``; that is out of
|
|
379
|
+
scope here. Users can set ``FLEXTOOL_DPI=...`` at startup as an
|
|
380
|
+
override when auto-detection picks the wrong monitor.
|
|
381
|
+
"""
|
|
382
|
+
try:
|
|
383
|
+
return float(widget.tk.call("winfo", "fpixels", widget._w, "1i"))
|
|
384
|
+
except (tk.TclError, AttributeError):
|
|
385
|
+
return 96.0
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def get_metrics(root: tk.Misc | None = None) -> FontMetrics:
|
|
389
|
+
"""Compute current font metrics from TkDefaultFont. Safe to call any time."""
|
|
390
|
+
default = tkfont.nametofont("TkDefaultFont")
|
|
391
|
+
cw = default.measure("0")
|
|
392
|
+
lh = default.metrics("linespace")
|
|
393
|
+
bold = default.copy()
|
|
394
|
+
bold.configure(weight="bold")
|
|
395
|
+
# Treeview row height must track the font the tree ACTUALLY renders cell
|
|
396
|
+
# text with — under sv_ttk that's SunValleyBodyFont, whose linespace runs
|
|
397
|
+
# ~20% tighter than TkDefaultFont's at the same visual size. Deriving the
|
|
398
|
+
# row height from TkDefaultFont (the old code) over-padded every row, so
|
|
399
|
+
# rows looked sparse at all DPIs. A flat 1.3× of the real linespace gives
|
|
400
|
+
# a comfortable click target without the gaps; the old max(24, …) px floor
|
|
401
|
+
# is dropped because a fixed pixel doesn't scale and only inflated rows at
|
|
402
|
+
# 96 DPI.
|
|
403
|
+
tree_lh = lh
|
|
404
|
+
try:
|
|
405
|
+
import tkinter.ttk as ttk
|
|
406
|
+
# lookup() returns a Tcl_Obj, not a str; str() yields the clean font
|
|
407
|
+
# name ("SunValleyBodyFont"), whereas passing the Tcl_Obj straight to
|
|
408
|
+
# nametofont raises "named font … does not already exist".
|
|
409
|
+
tree_font_name = str(ttk.Style().lookup("Treeview", "font") or "")
|
|
410
|
+
if tree_font_name:
|
|
411
|
+
tree_lh = tkfont.nametofont(tree_font_name).metrics("linespace")
|
|
412
|
+
except (tk.TclError, RuntimeError):
|
|
413
|
+
pass
|
|
414
|
+
return FontMetrics(
|
|
415
|
+
cw=cw,
|
|
416
|
+
lh=lh,
|
|
417
|
+
em=cw,
|
|
418
|
+
row_height=max(18, round(tree_lh * 1.3)),
|
|
419
|
+
bold_font=bold,
|
|
420
|
+
)
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""SpineDBBackend output → :class:`FlexDataProvider`.
|
|
2
|
+
|
|
3
|
+
The :mod:`input_derivation` tier sits between
|
|
4
|
+
:mod:`flextool.spinedb_backend` (raw EAV → polars materialisation) and
|
|
5
|
+
the in-cascade :class:`flextool.engine_polars._flex_data_provider.FlexDataProvider`
|
|
6
|
+
that the rest of the engine consumes.
|
|
7
|
+
|
|
8
|
+
Architecture
|
|
9
|
+
------------
|
|
10
|
+
|
|
11
|
+
::
|
|
12
|
+
|
|
13
|
+
SpineDB → SpineDBBackend → input_derivation → FlexDataProvider → cascade
|
|
14
|
+
|
|
15
|
+
A derivation in this package reads its raw inputs **through the
|
|
16
|
+
:class:`SpineDBBackend`** (or, for derivations that compose other
|
|
17
|
+
derivation outputs, the Provider) and emits one or more canonical-
|
|
18
|
+
schema :class:`polars.DataFrame` objects to the Provider via
|
|
19
|
+
``provider.put(key, frame)``.
|
|
20
|
+
|
|
21
|
+
**No disk writes.** The derivation tier is purely in-memory; CSV
|
|
22
|
+
emission is the Provider's job (``snapshot_processed_inputs`` /
|
|
23
|
+
``--csv-dump``).
|
|
24
|
+
|
|
25
|
+
Hard rules
|
|
26
|
+
----------
|
|
27
|
+
|
|
28
|
+
* No disk writes anywhere in this package.
|
|
29
|
+
* No disk-fallback reads. ``provider`` and ``backend`` are required
|
|
30
|
+
keyword arguments; the derivation fails loudly if either is missing.
|
|
31
|
+
* Each derivation populates the Provider via ``provider.put(...)``;
|
|
32
|
+
return values are reserved for derivation-internal carriers (e.g.
|
|
33
|
+
``ct_method_overrides`` from DC power flow, consumed by process
|
|
34
|
+
method).
|
|
35
|
+
"""
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import logging
|
|
39
|
+
import os
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
from typing import TYPE_CHECKING
|
|
42
|
+
|
|
43
|
+
import spinedb_api as api
|
|
44
|
+
from spinedb_api import DatabaseMapping
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
from flextool.engine_polars._flex_data_provider import FlexDataProvider
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = ["run"]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _provider_key(filename: str) -> str:
|
|
54
|
+
"""Canonical Provider key for ``input/<stem>.csv`` style paths.
|
|
55
|
+
|
|
56
|
+
Matches the convention in :mod:`flextool.engine_polars._emit_provider_io`:
|
|
57
|
+
``"<parent_dir_name>/<stem>"``.
|
|
58
|
+
"""
|
|
59
|
+
p = Path(filename)
|
|
60
|
+
return f"{p.parent.name}/{p.stem}" if p.parent.name else p.stem
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def run(
|
|
64
|
+
backend_or_db_url,
|
|
65
|
+
provider: "FlexDataProvider",
|
|
66
|
+
logger: "logging.Logger | None" = None,
|
|
67
|
+
*,
|
|
68
|
+
scenario_name: "str | None" = None,
|
|
69
|
+
work_folder: "Path | None" = None,
|
|
70
|
+
precision_digits: int = 0,
|
|
71
|
+
memory_recorder=None,
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Populate *provider* with the canonical ``input/`` + derivation
|
|
74
|
+
frames produced from the Spine database.
|
|
75
|
+
|
|
76
|
+
Pipeline
|
|
77
|
+
--------
|
|
78
|
+
|
|
79
|
+
1. **SpineDBBackend spec loops** — three loops over the canonical
|
|
80
|
+
spec lists (``_DEFAULT_VALUES_SPECS``, ``_ENTITY_SPECS``,
|
|
81
|
+
``_PARAMETER_SPECS``) materialise ~120 EAV frames into the
|
|
82
|
+
Provider under their canonical ``input/<stem>`` key.
|
|
83
|
+
2. **Validators** (timeline timestep duration, ladder methods,
|
|
84
|
+
capacity margin storage exclusion, group output memberships).
|
|
85
|
+
3. **DB-driven derivations** (DC power flow, process method,
|
|
86
|
+
commodity ladder cumulative + annual + sets).
|
|
87
|
+
4. **Native preprocessing emitters** — the dozen modules under
|
|
88
|
+
:mod:`flextool.engine_polars._emit_*` that produce the
|
|
89
|
+
``solve_data/*.csv`` artefacts the cascade consumes pre-solve.
|
|
90
|
+
Each is invoked directly with ``provider=`` so its frames flow
|
|
91
|
+
into the Provider and disk emission is skipped.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
backend_or_db_url
|
|
96
|
+
Either a Spine DB URL string (``sqlite:///...``) or a
|
|
97
|
+
pre-constructed :class:`SpineDBBackend`. The URL form opens an
|
|
98
|
+
ephemeral DatabaseMapping for the duration of the call.
|
|
99
|
+
provider
|
|
100
|
+
Cascade-input :class:`FlexDataProvider`. All frames land here.
|
|
101
|
+
logger
|
|
102
|
+
Optional logger. Defaults to ``logging.getLogger(__name__)``.
|
|
103
|
+
scenario_name
|
|
104
|
+
Scenario filter applied to the DB mapping. ``None`` runs
|
|
105
|
+
without a filter.
|
|
106
|
+
work_folder
|
|
107
|
+
Workdir for the (still-disk) batches that have not yet been
|
|
108
|
+
ported to Provider-only operation. When ``None`` defaults to
|
|
109
|
+
:func:`pathlib.Path.cwd`. Becomes optional in a later step
|
|
110
|
+
once every writer is Provider-only.
|
|
111
|
+
precision_digits
|
|
112
|
+
Float precision forwarded to ``SpineDBBackend.parameter_values``.
|
|
113
|
+
"""
|
|
114
|
+
from flextool.spinedb_backend import SpineDBBackend
|
|
115
|
+
from flextool.input_derivation._specs import (
|
|
116
|
+
_ENTITY_SPECS,
|
|
117
|
+
_PARAMETER_SPECS,
|
|
118
|
+
_DEFAULT_VALUES_SPECS,
|
|
119
|
+
)
|
|
120
|
+
from flextool.input_derivation._validators import (
|
|
121
|
+
validate_timeline_timestep_duration,
|
|
122
|
+
validate_ladder_methods,
|
|
123
|
+
validate_group_output_memberships,
|
|
124
|
+
validate_capacity_margin_groups,
|
|
125
|
+
validate_connection_node_memberships,
|
|
126
|
+
)
|
|
127
|
+
from flextool.input_derivation._dc_power_flow import derive_dc_power_flow
|
|
128
|
+
from flextool.input_derivation._process_method import derive_process_method
|
|
129
|
+
from flextool.input_derivation._commodity_ladder import (
|
|
130
|
+
derive_commodity_ladder_cumulative,
|
|
131
|
+
derive_commodity_ladder_annual,
|
|
132
|
+
)
|
|
133
|
+
from flextool.input_derivation._commodity_ladder_sets import (
|
|
134
|
+
derive_commodity_ladder_sets,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if logger is None:
|
|
138
|
+
logger = logging.getLogger(__name__)
|
|
139
|
+
if provider is None:
|
|
140
|
+
raise TypeError("input_derivation.run requires a FlexDataProvider")
|
|
141
|
+
|
|
142
|
+
def _mem(label: str, user_label: str) -> None:
|
|
143
|
+
"""Emit a phase checkpoint when a recorder was supplied; no-op
|
|
144
|
+
otherwise. Lets the user follow input-pipeline phase progress
|
|
145
|
+
with section-delta accounting matching the cascade/build
|
|
146
|
+
checkpoints emitted by :mod:`_orchestration`.
|
|
147
|
+
|
|
148
|
+
The recorder's log lines are user-visible by default; full
|
|
149
|
+
tracemalloc-peak diagnostics and CSV emission are opt-in via
|
|
150
|
+
``FLEXTOOL_MEMORY_DIAGNOSTICS=1`` (handled inside the recorder).
|
|
151
|
+
"""
|
|
152
|
+
if memory_recorder is not None:
|
|
153
|
+
memory_recorder.checkpoint(label, logger, user_label=user_label)
|
|
154
|
+
|
|
155
|
+
wf = work_folder if work_folder is not None else Path.cwd()
|
|
156
|
+
|
|
157
|
+
def _do(db) -> None:
|
|
158
|
+
# Step 1 — SpineDBBackend spec loops.
|
|
159
|
+
backend = SpineDBBackend.__new__(SpineDBBackend)
|
|
160
|
+
backend._db = db # type: ignore[attr-defined]
|
|
161
|
+
backend._api = api # type: ignore[attr-defined]
|
|
162
|
+
backend._precision_digits = precision_digits # type: ignore[attr-defined]
|
|
163
|
+
backend._scenario_name = scenario_name # type: ignore[attr-defined]
|
|
164
|
+
|
|
165
|
+
for spec in _DEFAULT_VALUES_SPECS:
|
|
166
|
+
frame = backend.parameter_defaults(
|
|
167
|
+
cl_pars=spec["cl_pars"],
|
|
168
|
+
header=spec["header"],
|
|
169
|
+
filter_in_type=spec.get("filter_in_type"),
|
|
170
|
+
only_value=spec.get("only_value", False),
|
|
171
|
+
)
|
|
172
|
+
provider.put(_provider_key(spec["filename"]), frame)
|
|
173
|
+
|
|
174
|
+
for spec in _ENTITY_SPECS:
|
|
175
|
+
frame = backend.entities(
|
|
176
|
+
classes=spec.classes,
|
|
177
|
+
header=spec.header,
|
|
178
|
+
entity_dimens=spec.entity_dimens,
|
|
179
|
+
)
|
|
180
|
+
provider.put(_provider_key(spec.filename), frame)
|
|
181
|
+
|
|
182
|
+
validate_timeline_timestep_duration(db)
|
|
183
|
+
|
|
184
|
+
for spec in _PARAMETER_SPECS:
|
|
185
|
+
kwargs = {k: v for k, v in spec.items() if k != "filename"}
|
|
186
|
+
frame = backend.parameter_values(**kwargs)
|
|
187
|
+
provider.put(_provider_key(spec["filename"]), frame)
|
|
188
|
+
_mem("spine_db_read_end", "Spine DB loaded")
|
|
189
|
+
|
|
190
|
+
# Step 2 — DB-driven derivations.
|
|
191
|
+
ct_method_overrides = derive_dc_power_flow(backend, provider, logger)
|
|
192
|
+
derive_process_method(
|
|
193
|
+
backend, provider, logger,
|
|
194
|
+
ct_method_overrides=ct_method_overrides,
|
|
195
|
+
)
|
|
196
|
+
validate_ladder_methods(db, logger)
|
|
197
|
+
derive_commodity_ladder_cumulative(backend, provider, logger)
|
|
198
|
+
derive_commodity_ladder_annual(backend, provider, logger)
|
|
199
|
+
derive_commodity_ladder_sets(backend, provider)
|
|
200
|
+
_mem("db_derivations_end", "DB-driven derivations done")
|
|
201
|
+
|
|
202
|
+
# Step 3 — write_input-time native preprocessing emitters.
|
|
203
|
+
# Each emit_* threads the Provider directly so frames flow into
|
|
204
|
+
# memory rather than disk.
|
|
205
|
+
from flextool.engine_polars import (
|
|
206
|
+
_emit_leaf_sets as _leaf,
|
|
207
|
+
_emit_mid_sets as _mid,
|
|
208
|
+
_emit_calc_params as _calc,
|
|
209
|
+
_emit_arc_unions as _arc,
|
|
210
|
+
_emit_dispatchers as _disp,
|
|
211
|
+
)
|
|
212
|
+
input_dir = wf / "input"
|
|
213
|
+
solve_data_dir = wf / "solve_data"
|
|
214
|
+
os.makedirs(solve_data_dir, exist_ok=True)
|
|
215
|
+
_leaf.emit_period_param_sets(input_dir, solve_data_dir, provider=provider)
|
|
216
|
+
_leaf.emit_invest_method_sets(input_dir, solve_data_dir, provider=provider)
|
|
217
|
+
_leaf.emit_co2_method_sets(input_dir, solve_data_dir, provider=provider)
|
|
218
|
+
_leaf.emit_optional_yes(input_dir, solve_data_dir, provider=provider)
|
|
219
|
+
_leaf.emit_reserve_upDown_group(input_dir, solve_data_dir, provider=provider)
|
|
220
|
+
_leaf.emit_group_loss_share(input_dir, solve_data_dir, provider=provider)
|
|
221
|
+
_mid.emit_node_type_sets(input_dir, solve_data_dir, provider=provider)
|
|
222
|
+
_mid.emit_entity_lifetime_method(input_dir, solve_data_dir, provider=provider)
|
|
223
|
+
_mid.emit_process_ct_method(input_dir, solve_data_dir, provider=provider)
|
|
224
|
+
_mid.emit_process_startup_method(input_dir, solve_data_dir, provider=provider)
|
|
225
|
+
_mid.emit_node_inflow_method(input_dir, solve_data_dir, provider=provider)
|
|
226
|
+
_mid.emit_node_storage_binding_method(input_dir, solve_data_dir, provider=provider)
|
|
227
|
+
_mid.emit_node_penalty_method(input_dir, solve_data_dir, provider=provider)
|
|
228
|
+
_mid.emit_process_group_inside_group_nonsync(input_dir, solve_data_dir, provider=provider)
|
|
229
|
+
_mid.emit_process__sink_nonSync(input_dir, solve_data_dir, provider=provider)
|
|
230
|
+
_mid.emit_group_entity(input_dir, solve_data_dir, provider=provider)
|
|
231
|
+
_mid.emit_process_delayed__duration(input_dir, solve_data_dir, provider=provider)
|
|
232
|
+
_calc.emit_entity_total_caps(input_dir, solve_data_dir, provider=provider)
|
|
233
|
+
_calc.emit_process_method_projections(input_dir, provider=provider)
|
|
234
|
+
_calc.emit_process_VRE(input_dir, provider=provider)
|
|
235
|
+
_calc.emit_process_arc_method_joins(input_dir, provider=provider)
|
|
236
|
+
_calc.emit_process_profile_method_joins(input_dir, provider=provider)
|
|
237
|
+
_mid.emit_reserve_partitions(input_dir, solve_data_dir, provider=provider)
|
|
238
|
+
_mid.emit_commodity_node_co2(input_dir, solve_data_dir, provider=provider)
|
|
239
|
+
_mid.emit_process_coeff_zero_sets(input_dir, solve_data_dir, provider=provider)
|
|
240
|
+
_leaf.emit_def_optional_yes(input_dir, solve_data_dir, provider=provider)
|
|
241
|
+
_leaf.emit_process_delayed(input_dir, solve_data_dir, provider=provider)
|
|
242
|
+
_leaf.emit_simple_setof_projections(input_dir, solve_data_dir, provider=provider)
|
|
243
|
+
# emit_period_solve depends on solve_data outputs from
|
|
244
|
+
# emit_simple_setof_projections, so must run after.
|
|
245
|
+
_leaf.emit_period_solve(solve_data_dir, provider=provider)
|
|
246
|
+
_leaf.emit_time_set(input_dir, solve_data_dir, provider=provider)
|
|
247
|
+
_leaf.emit_enable_optional_outputs(solve_data_dir, provider=provider)
|
|
248
|
+
_leaf.emit_node_state_subsets(solve_data_dir, provider=provider)
|
|
249
|
+
_mid.emit_invest_total_sets(input_dir, solve_data_dir, provider=provider)
|
|
250
|
+
_mid.emit_ci_ladder_cumulative(input_dir, solve_data_dir, provider=provider)
|
|
251
|
+
_disp.emit_process_arc_unions(input_dir, solve_data_dir, provider=provider)
|
|
252
|
+
_arc.emit_param_in_use_sets(input_dir, solve_data_dir, provider=provider)
|
|
253
|
+
|
|
254
|
+
_mem("preprocessing_writers_end", "Preprocessing writers done")
|
|
255
|
+
|
|
256
|
+
# Step 4 — validators that need DB access.
|
|
257
|
+
validate_capacity_margin_groups(db, logger)
|
|
258
|
+
validate_group_output_memberships(db, logger)
|
|
259
|
+
validate_connection_node_memberships(db, logger)
|
|
260
|
+
|
|
261
|
+
# Accept either a backend, a DatabaseMapping or a URL string.
|
|
262
|
+
if isinstance(backend_or_db_url, str):
|
|
263
|
+
scen_config = (
|
|
264
|
+
api.filters.scenario_filter.scenario_filter_config(scenario_name)
|
|
265
|
+
if scenario_name else None
|
|
266
|
+
)
|
|
267
|
+
with DatabaseMapping(backend_or_db_url) as db:
|
|
268
|
+
# Hot-load the DB: fetching upfront is faster than repeated
|
|
269
|
+
# parameter_value lookups.
|
|
270
|
+
db.fetch_all("entity")
|
|
271
|
+
db.fetch_all("parameter_value")
|
|
272
|
+
if scen_config is not None:
|
|
273
|
+
api.filters.scenario_filter.scenario_filter_from_dict(db, scen_config)
|
|
274
|
+
os.makedirs(wf / "input", exist_ok=True)
|
|
275
|
+
_do(db)
|
|
276
|
+
else:
|
|
277
|
+
# Accept a SpineDBBackend (with ._db) or a DatabaseMapping
|
|
278
|
+
# directly; both expose the find_* API we need.
|
|
279
|
+
db = getattr(backend_or_db_url, "_db", backend_or_db_url)
|
|
280
|
+
os.makedirs(wf / "input", exist_ok=True)
|
|
281
|
+
_do(db)
|