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
flextool/env_check.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""Native-environment self-check and auto-remediation.
|
|
2
|
+
|
|
3
|
+
FlexTool's solve path loads compiled extensions — ``polars``, ``highspy``
|
|
4
|
+
(HiGHS, via ``polar_high``) — that can crash *natively* (not as a catchable
|
|
5
|
+
Python exception) when the installed wheel is wrong for the machine. Two
|
|
6
|
+
real cases we have hit:
|
|
7
|
+
|
|
8
|
+
* The default ``polars`` wheel on a CPU without the SIMD instruction set it
|
|
9
|
+
was built for: crashes on the first vectorised op. Cure: the
|
|
10
|
+
``polars-lts-cpu`` build.
|
|
11
|
+
* ``highspy`` ``1.14.0`` crashes on *import* on older Windows (Server 2019 /
|
|
12
|
+
Windows 10 1809-era), see ERGO-Code/HiGHS#2964 — a missing dependency in
|
|
13
|
+
that wheel; works on Windows 11 / Linux. Cure: pin ``highspy==1.13.1``.
|
|
14
|
+
|
|
15
|
+
Either way the symptom is a Windows access violation (``0xC0000005`` → exit
|
|
16
|
+
``3221225477``) or a POSIX ``SIGILL``/``SIGSEGV``. A native fault
|
|
17
|
+
terminates the process, so it cannot be caught with ``try/except``; the only
|
|
18
|
+
robust way to observe it is to run the risky imports in a **child process**
|
|
19
|
+
and inspect its exit code — which is what :func:`probe_solver_stack` does.
|
|
20
|
+
It also pinpoints *which* extension died (via flushed progress markers), so
|
|
21
|
+
the right remedy can be applied.
|
|
22
|
+
|
|
23
|
+
This module imports **stdlib only** so it stays importable in a broken
|
|
24
|
+
environment, and it never imports ``polars``/``highspy`` in-process.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import dataclasses
|
|
30
|
+
import hashlib
|
|
31
|
+
import importlib.metadata
|
|
32
|
+
import platform
|
|
33
|
+
import subprocess
|
|
34
|
+
import sys
|
|
35
|
+
|
|
36
|
+
# highspy 1.14.0 import-crashes on older Windows (HiGHS#2964); 1.13.1 is the
|
|
37
|
+
# last good release. Used as the downgrade target.
|
|
38
|
+
HIGHSPY_GOOD_VERSION = "1.13.1"
|
|
39
|
+
|
|
40
|
+
# Probe program: import each native extension in turn and print a *flushed*
|
|
41
|
+
# marker after each one survives. On a native crash the already-flushed
|
|
42
|
+
# markers remain in the captured pipe, so the last marker tells us how far
|
|
43
|
+
# we got — and therefore which component is the one that died. polars is
|
|
44
|
+
# exercised with a real SIMD op (a bare import can pass on an incompatible
|
|
45
|
+
# CPU and only fault later); highspy and polar_high are constructed.
|
|
46
|
+
_PROBE_CODE = (
|
|
47
|
+
"import sys\n"
|
|
48
|
+
"def mark(m):\n"
|
|
49
|
+
" sys.stdout.write('PROBE:' + m + '\\n'); sys.stdout.flush()\n"
|
|
50
|
+
"mark('begin')\n"
|
|
51
|
+
"import polars as pl\n"
|
|
52
|
+
"out = (pl.DataFrame({'a': list(range(1000)), 'b': list(range(1000))})\n"
|
|
53
|
+
" .sort('a', descending=True)\n"
|
|
54
|
+
" .group_by('a').agg(pl.col('b').sum())\n"
|
|
55
|
+
" .select(pl.col('b').sum())).to_series()[0]\n"
|
|
56
|
+
"assert out == 499500, out\n"
|
|
57
|
+
"mark('polars')\n"
|
|
58
|
+
"import highspy\n"
|
|
59
|
+
"highspy.Highs()\n"
|
|
60
|
+
"mark('highspy')\n"
|
|
61
|
+
"import polar_high\n"
|
|
62
|
+
"polar_high.Problem()\n"
|
|
63
|
+
"mark('polar_high')\n"
|
|
64
|
+
"mark('all_ok')\n"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Ordered probe checkpoints; the component that runs *after* a checkpoint is
|
|
68
|
+
# the suspect when the probe dies having last printed that checkpoint.
|
|
69
|
+
_CHECKPOINTS = ["begin", "polars", "highspy", "polar_high", "all_ok"]
|
|
70
|
+
_COMPONENT_AFTER = {
|
|
71
|
+
"begin": "polars",
|
|
72
|
+
"polars": "highspy",
|
|
73
|
+
"highspy": "polar_high",
|
|
74
|
+
"polar_high": None,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Probe outcome statuses.
|
|
78
|
+
OK = "ok" # whole stack imported and computed correctly
|
|
79
|
+
NATIVE_FAULT = "native_fault" # a process was killed by a native fault
|
|
80
|
+
ERROR = "error" # ordinary Python error (e.g. not installed)
|
|
81
|
+
TIMEOUT = "timeout" # probe did not finish in time
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclasses.dataclass
|
|
85
|
+
class StackProbe:
|
|
86
|
+
"""Outcome of running :func:`probe_solver_stack` in a child process."""
|
|
87
|
+
|
|
88
|
+
status: str
|
|
89
|
+
returncode: int | None
|
|
90
|
+
failed_component: str | None = None # "polars" / "highspy" / "polar_high"
|
|
91
|
+
stdout: str = ""
|
|
92
|
+
stderr: str = ""
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def ok(self) -> bool:
|
|
96
|
+
return self.status == OK
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def is_native_fault(self) -> bool:
|
|
100
|
+
return self.status == NATIVE_FAULT
|
|
101
|
+
|
|
102
|
+
def summary(self) -> str:
|
|
103
|
+
"""One-line, copy-pasteable description of the outcome."""
|
|
104
|
+
if self.status == OK:
|
|
105
|
+
return "solver stack: OK"
|
|
106
|
+
if self.status == NATIVE_FAULT:
|
|
107
|
+
comp = self.failed_component or "a solver library"
|
|
108
|
+
return (
|
|
109
|
+
f"solver stack: NATIVE CRASH in {comp} "
|
|
110
|
+
f"({describe_fault(self.returncode)}) — this build is "
|
|
111
|
+
f"incompatible with this computer"
|
|
112
|
+
)
|
|
113
|
+
if self.status == TIMEOUT:
|
|
114
|
+
return "solver stack: probe timed out"
|
|
115
|
+
return f"solver stack: error (exit {self.returncode})"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def is_native_fault(returncode: int | None, plat: str | None = None) -> bool:
|
|
119
|
+
"""True if *returncode* indicates a native crash (segfault / illegal
|
|
120
|
+
instruction / abort), as opposed to a normal Python exit.
|
|
121
|
+
|
|
122
|
+
``plat`` defaults to :data:`sys.platform`; it is a parameter so the
|
|
123
|
+
classifier can be unit-tested for both platform families on one host.
|
|
124
|
+
"""
|
|
125
|
+
if returncode is None or returncode == 0:
|
|
126
|
+
return False
|
|
127
|
+
plat = plat if plat is not None else sys.platform
|
|
128
|
+
|
|
129
|
+
if plat.startswith("win"):
|
|
130
|
+
# Windows exit codes are 32-bit DWORDs; NTSTATUS *fault* codes have
|
|
131
|
+
# the top two bits set (0xC0000000 range): 0xC0000005 access
|
|
132
|
+
# violation, 0xC000001D illegal instruction, 0xC00000FD stack
|
|
133
|
+
# overflow, 0xC0000409 stack buffer overrun, etc.
|
|
134
|
+
u = returncode & 0xFFFFFFFF
|
|
135
|
+
return u >= 0xC0000000
|
|
136
|
+
|
|
137
|
+
# POSIX: subprocess reports a signal-terminated child as ``-signal``.
|
|
138
|
+
# Treat the fatal "the code itself is broken / corrupted memory" signals
|
|
139
|
+
# as native faults; SIGILL=4, SIGABRT=6, SIGBUS=7, SIGFPE=8, SIGSEGV=11.
|
|
140
|
+
if returncode < 0:
|
|
141
|
+
return (-returncode) in (4, 6, 7, 8, 11)
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def describe_fault(returncode: int | None, plat: str | None = None) -> str:
|
|
146
|
+
"""Human-readable name for a native-fault *returncode* (best effort)."""
|
|
147
|
+
if returncode is None:
|
|
148
|
+
return "no exit code"
|
|
149
|
+
plat = plat if plat is not None else sys.platform
|
|
150
|
+
if plat.startswith("win"):
|
|
151
|
+
u = returncode & 0xFFFFFFFF
|
|
152
|
+
names = {
|
|
153
|
+
0xC0000005: "access violation (0xC0000005)",
|
|
154
|
+
0xC000001D: "illegal instruction (0xC000001D)",
|
|
155
|
+
0xC00000FD: "stack overflow (0xC00000FD)",
|
|
156
|
+
0xC0000409: "stack buffer overrun (0xC0000409)",
|
|
157
|
+
0xC0000094: "integer divide by zero (0xC0000094)",
|
|
158
|
+
}
|
|
159
|
+
return names.get(u, f"native fault (0x{u:08X})")
|
|
160
|
+
if returncode < 0:
|
|
161
|
+
sig = -returncode
|
|
162
|
+
names = {
|
|
163
|
+
4: "SIGILL (illegal instruction)",
|
|
164
|
+
6: "SIGABRT (abort)",
|
|
165
|
+
7: "SIGBUS (bus error)",
|
|
166
|
+
8: "SIGFPE (floating-point exception)",
|
|
167
|
+
11: "SIGSEGV (segmentation fault)",
|
|
168
|
+
}
|
|
169
|
+
return names.get(sig, f"signal {sig}")
|
|
170
|
+
return f"exit code {returncode}"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _classify_failed_component(stdout: str) -> str | None:
|
|
174
|
+
"""Given the probe's (possibly truncated) stdout, return the component
|
|
175
|
+
that was about to run when it died — the suspect for the crash."""
|
|
176
|
+
seen = [c for c in _CHECKPOINTS if f"PROBE:{c}" in stdout]
|
|
177
|
+
if not seen:
|
|
178
|
+
return None
|
|
179
|
+
return _COMPONENT_AFTER.get(seen[-1])
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def probe_solver_stack(python: str | None = None, timeout: float = 120.0) -> StackProbe:
|
|
183
|
+
"""Import the native solver stack in a child process and classify the
|
|
184
|
+
result, pinpointing which extension faulted.
|
|
185
|
+
|
|
186
|
+
Isolating the imports in a subprocess is what makes a native crash
|
|
187
|
+
*observable* instead of fatal to us: we read the child's exit code.
|
|
188
|
+
*python* defaults to the current interpreter (:data:`sys.executable`),
|
|
189
|
+
which is also the interpreter the GUI uses to launch solves.
|
|
190
|
+
"""
|
|
191
|
+
py = python or sys.executable
|
|
192
|
+
try:
|
|
193
|
+
completed = subprocess.run(
|
|
194
|
+
[py, "-c", _PROBE_CODE],
|
|
195
|
+
capture_output=True,
|
|
196
|
+
text=True,
|
|
197
|
+
timeout=timeout,
|
|
198
|
+
)
|
|
199
|
+
except subprocess.TimeoutExpired:
|
|
200
|
+
return StackProbe(TIMEOUT, None)
|
|
201
|
+
except OSError as exc:
|
|
202
|
+
return StackProbe(ERROR, None, stderr=str(exc))
|
|
203
|
+
|
|
204
|
+
rc = completed.returncode
|
|
205
|
+
out, err = completed.stdout.strip(), completed.stderr.strip()
|
|
206
|
+
if rc == 0 and "PROBE:all_ok" in completed.stdout:
|
|
207
|
+
return StackProbe(OK, rc, None, out, err)
|
|
208
|
+
if is_native_fault(rc):
|
|
209
|
+
return StackProbe(
|
|
210
|
+
NATIVE_FAULT, rc, _classify_failed_component(completed.stdout), out, err
|
|
211
|
+
)
|
|
212
|
+
return StackProbe(ERROR, rc, _classify_failed_component(completed.stdout), out, err)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _dist_version(name: str) -> str | None:
|
|
216
|
+
try:
|
|
217
|
+
return importlib.metadata.version(name)
|
|
218
|
+
except importlib.metadata.PackageNotFoundError:
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def installed_polars() -> tuple[str | None, str | None]:
|
|
223
|
+
"""Return ``(distribution_name, version)`` for whichever polars build is
|
|
224
|
+
installed, without importing polars. Either the standard ``polars`` or
|
|
225
|
+
the ``polars-lts-cpu`` distribution provides the ``polars`` import name.
|
|
226
|
+
"""
|
|
227
|
+
for dist in ("polars", "polars-lts-cpu"):
|
|
228
|
+
ver = _dist_version(dist)
|
|
229
|
+
if ver is not None:
|
|
230
|
+
return dist, ver
|
|
231
|
+
return None, None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def env_fingerprint() -> str:
|
|
235
|
+
"""Short, stable hash of the things that decide whether the probe needs
|
|
236
|
+
to re-run: the interpreter, its version, the machine/OS, and the
|
|
237
|
+
installed solver builds (polars + highspy). Swapping or downgrading
|
|
238
|
+
either build changes the fingerprint, so a fixed environment re-checks
|
|
239
|
+
itself exactly once after any remediation.
|
|
240
|
+
"""
|
|
241
|
+
pol_dist, pol_ver = installed_polars()
|
|
242
|
+
parts = [
|
|
243
|
+
sys.executable,
|
|
244
|
+
sys.version.split()[0],
|
|
245
|
+
platform.machine(),
|
|
246
|
+
platform.system(),
|
|
247
|
+
f"{pol_dist}=={pol_ver}",
|
|
248
|
+
f"highspy=={_dist_version('highspy')}",
|
|
249
|
+
]
|
|
250
|
+
return hashlib.sha256("|".join(str(p) for p in parts).encode()).hexdigest()[:16]
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def swap_to_lts_cpu_steps(python: str | None = None) -> list[list[str]]:
|
|
254
|
+
"""pip steps that replace the standard polars with ``polars-lts-cpu``.
|
|
255
|
+
Both distributions own the ``polars`` import name, so the standard build
|
|
256
|
+
must be uninstalled first — installing lts-cpu alone does not displace
|
|
257
|
+
it. Uninstalling a not-installed name is a no-op (pip warns, exits 0).
|
|
258
|
+
"""
|
|
259
|
+
py = python or sys.executable
|
|
260
|
+
return [
|
|
261
|
+
[py, "-m", "pip", "uninstall", "-y", "polars", "polars-lts-cpu"],
|
|
262
|
+
[py, "-m", "pip", "install", "polars-lts-cpu"],
|
|
263
|
+
]
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def downgrade_highspy_steps(python: str | None = None) -> list[list[str]]:
|
|
267
|
+
"""pip step that pins highspy to the last release without the older-
|
|
268
|
+
Windows import crash (HiGHS#2964)."""
|
|
269
|
+
py = python or sys.executable
|
|
270
|
+
return [[py, "-m", "pip", "install", f"highspy=={HIGHSPY_GOOD_VERSION}"]]
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _banner(message: str) -> str:
|
|
274
|
+
return (
|
|
275
|
+
"================================================================\n"
|
|
276
|
+
f" {message}\n"
|
|
277
|
+
" Re-installing the compatible version now...\n"
|
|
278
|
+
" (press Ctrl-C to stop)\n"
|
|
279
|
+
"================================================================"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# Per-component remediation. ``steps`` is a callable(python) -> list of argv.
|
|
284
|
+
_REMEDIATIONS = {
|
|
285
|
+
"polars": {
|
|
286
|
+
"steps": swap_to_lts_cpu_steps,
|
|
287
|
+
"banner": _banner(
|
|
288
|
+
"Your computer needs a different 'polars' build to run FlexTool."
|
|
289
|
+
),
|
|
290
|
+
},
|
|
291
|
+
"highspy": {
|
|
292
|
+
"steps": downgrade_highspy_steps,
|
|
293
|
+
"banner": _banner(
|
|
294
|
+
f"Your computer needs HiGHS solver 'highspy=={HIGHSPY_GOOD_VERSION}' "
|
|
295
|
+
"to run FlexTool."
|
|
296
|
+
),
|
|
297
|
+
},
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def has_remediation(component: str | None) -> bool:
|
|
302
|
+
return component in _REMEDIATIONS
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def remediation_steps(component: str | None, python: str | None = None) -> list[list[str]] | None:
|
|
306
|
+
spec = _REMEDIATIONS.get(component or "")
|
|
307
|
+
return spec["steps"](python) if spec else None
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def remediation_banner(component: str | None) -> str | None:
|
|
311
|
+
spec = _REMEDIATIONS.get(component or "")
|
|
312
|
+
return spec["banner"] if spec else None
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# Message for the cases a package swap cannot fix (a polar_high crash, a
|
|
316
|
+
# swap that did not help, or a native crash outside the solver stack):
|
|
317
|
+
# genuinely environment-level. The common Windows culprits are a venv
|
|
318
|
+
# built on Anaconda/conda (mixed native libraries), a missing Visual C++
|
|
319
|
+
# runtime, or running from a network/mapped drive under concurrency.
|
|
320
|
+
UNFIXABLE_HELP = (
|
|
321
|
+
"This is an environment-level problem a package install cannot fix. A "
|
|
322
|
+
"native crash like this usually means the Python environment itself is "
|
|
323
|
+
"unstable — most often a venv built on Anaconda/conda (mixing native "
|
|
324
|
+
"libraries), a missing Visual C++ runtime, or running from a network/"
|
|
325
|
+
"mapped drive. Rebuild the venv from a clean python.org Python on a "
|
|
326
|
+
"local disk (e.g. C:), not a network drive, and keep the project local too."
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _cpu_hint() -> str:
|
|
331
|
+
"""Best-effort CPU description for the diagnostics block (stdlib only)."""
|
|
332
|
+
proc = platform.processor() or "unknown"
|
|
333
|
+
avx2 = ""
|
|
334
|
+
if sys.platform.startswith("linux"):
|
|
335
|
+
try:
|
|
336
|
+
with open("/proc/cpuinfo", encoding="utf-8") as fh:
|
|
337
|
+
for line in fh:
|
|
338
|
+
if line.startswith("flags"):
|
|
339
|
+
avx2 = "; AVX2" if " avx2 " in f" {line} " else "; no AVX2"
|
|
340
|
+
break
|
|
341
|
+
except OSError:
|
|
342
|
+
pass
|
|
343
|
+
return f"{proc}{avx2}"
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def diagnostics_report(probe: StackProbe | None = None) -> str:
|
|
347
|
+
"""Multi-line, copy-pasteable environment report. Used by the
|
|
348
|
+
``__main__`` entry point, the self-update flow, and the GUI startup
|
|
349
|
+
check so a screenshot or paste is enough to triage remotely.
|
|
350
|
+
"""
|
|
351
|
+
pol_dist, pol_ver = installed_polars()
|
|
352
|
+
if probe is None:
|
|
353
|
+
probe = probe_solver_stack()
|
|
354
|
+
lines = [
|
|
355
|
+
"FlexTool environment check",
|
|
356
|
+
f" python : {sys.executable}",
|
|
357
|
+
f" version : {sys.version.split()[0]}",
|
|
358
|
+
f" platform : {platform.platform()}",
|
|
359
|
+
f" machine : {platform.machine()}",
|
|
360
|
+
f" cpu : {_cpu_hint()}",
|
|
361
|
+
f" polars build: {pol_dist} {pol_ver}",
|
|
362
|
+
f" highspy : {_dist_version('highspy')}",
|
|
363
|
+
f" {probe.summary()}",
|
|
364
|
+
]
|
|
365
|
+
if probe.stderr and not probe.ok:
|
|
366
|
+
lines.append(" --- probe stderr ---")
|
|
367
|
+
lines.extend(" " + ln for ln in probe.stderr.splitlines())
|
|
368
|
+
return "\n".join(lines)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def main(argv: list[str] | None = None) -> int:
|
|
372
|
+
"""``python -m flextool.env_check`` — print diagnostics, exit non-zero
|
|
373
|
+
if the solver stack cannot run here."""
|
|
374
|
+
probe = probe_solver_stack()
|
|
375
|
+
print(diagnostics_report(probe))
|
|
376
|
+
if probe.is_native_fault and has_remediation(probe.failed_component):
|
|
377
|
+
print("\nFix:")
|
|
378
|
+
for step in remediation_steps(probe.failed_component):
|
|
379
|
+
print(" " + " ".join(step))
|
|
380
|
+
return 1
|
|
381
|
+
if probe.is_native_fault:
|
|
382
|
+
print("\n" + UNFIXABLE_HELP)
|
|
383
|
+
return 1
|
|
384
|
+
return 0 if probe.ok else 2
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
if __name__ == "__main__":
|
|
388
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Read all data from a FlexTool Spine DB SQLite database into a DatabaseContents dataclass."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from spinedb_api import DatabaseMapping, from_database
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class DatabaseContents:
|
|
15
|
+
"""All data extracted from a FlexTool Spine DB."""
|
|
16
|
+
|
|
17
|
+
entity_classes: list[dict] = field(default_factory=list)
|
|
18
|
+
"""Each has 'name', 'dimension_name_list'."""
|
|
19
|
+
|
|
20
|
+
entities: dict[str, list[dict]] = field(default_factory=dict)
|
|
21
|
+
"""Keyed by entity_class_name -> list of entity dicts."""
|
|
22
|
+
|
|
23
|
+
parameter_definitions: dict[str, list] = field(default_factory=dict)
|
|
24
|
+
"""Keyed by entity_class_name -> list of param def dicts."""
|
|
25
|
+
|
|
26
|
+
parameter_values: dict[tuple, Any] = field(default_factory=dict)
|
|
27
|
+
"""(class_name, entity_byname, param_name, alt_name) -> parsed Python value."""
|
|
28
|
+
|
|
29
|
+
alternatives: list[str] = field(default_factory=list)
|
|
30
|
+
"""Alternative names."""
|
|
31
|
+
|
|
32
|
+
scenarios: list[dict] = field(default_factory=list)
|
|
33
|
+
"""Each: {'name': str, 'alternatives': [(alt_name, rank), ...]}."""
|
|
34
|
+
|
|
35
|
+
entity_alternatives: dict[tuple, bool] = field(default_factory=dict)
|
|
36
|
+
"""(class_name, entity_byname, alt_name) -> active (bool)."""
|
|
37
|
+
|
|
38
|
+
parameter_groups: dict[str, dict] = field(default_factory=dict)
|
|
39
|
+
"""group_name -> {'priority': int, 'color': str}."""
|
|
40
|
+
|
|
41
|
+
param_to_group: dict[tuple, str] = field(default_factory=dict)
|
|
42
|
+
"""(class_name, param_name) -> group_name."""
|
|
43
|
+
|
|
44
|
+
version: float | None = None
|
|
45
|
+
"""FlexTool DB version from model.version default."""
|
|
46
|
+
|
|
47
|
+
list_values: dict[str, list] = field(default_factory=dict)
|
|
48
|
+
"""value_list_name -> [allowed_values]."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _convert_numpy(value: Any) -> Any:
|
|
52
|
+
"""Convert numpy scalar types to native Python types for openpyxl compatibility."""
|
|
53
|
+
if isinstance(value, np.floating):
|
|
54
|
+
return float(value)
|
|
55
|
+
if isinstance(value, np.integer):
|
|
56
|
+
return int(value)
|
|
57
|
+
if isinstance(value, np.str_):
|
|
58
|
+
return str(value)
|
|
59
|
+
if isinstance(value, np.bool_):
|
|
60
|
+
return bool(value)
|
|
61
|
+
if isinstance(value, np.ndarray):
|
|
62
|
+
return value.tolist()
|
|
63
|
+
return value
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def read_database(db_url: str) -> DatabaseContents:
|
|
67
|
+
"""Read a FlexTool Spine DB and return all contents as a DatabaseContents dataclass.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
db_url: SQLAlchemy-style database URL, e.g. ``sqlite:///path/to/db.sqlite``.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
A fully populated :class:`DatabaseContents` instance.
|
|
74
|
+
"""
|
|
75
|
+
dc = DatabaseContents()
|
|
76
|
+
db = DatabaseMapping(db_url, create=False)
|
|
77
|
+
try:
|
|
78
|
+
db.fetch_all()
|
|
79
|
+
_read_entity_classes(db, dc)
|
|
80
|
+
_read_entities(db, dc)
|
|
81
|
+
_read_parameter_definitions(db, dc)
|
|
82
|
+
_read_parameter_values(db, dc)
|
|
83
|
+
_read_alternatives(db, dc)
|
|
84
|
+
_read_scenarios(db, dc)
|
|
85
|
+
_read_entity_alternatives(db, dc)
|
|
86
|
+
_read_parameter_groups(db, dc)
|
|
87
|
+
_read_param_to_group(dc)
|
|
88
|
+
_read_version(dc)
|
|
89
|
+
_read_list_values(db, dc)
|
|
90
|
+
finally:
|
|
91
|
+
db.close()
|
|
92
|
+
return dc
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _read_entity_classes(db: DatabaseMapping, dc: DatabaseContents) -> None:
|
|
96
|
+
"""Read entity classes: name and dimension_name_list."""
|
|
97
|
+
for item in db.get_entity_class_items():
|
|
98
|
+
dc.entity_classes.append(
|
|
99
|
+
{
|
|
100
|
+
"name": item["name"],
|
|
101
|
+
"dimension_name_list": item["dimension_name_list"],
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _read_entities(db: DatabaseMapping, dc: DatabaseContents) -> None:
|
|
107
|
+
"""Read entities grouped by entity_class_name."""
|
|
108
|
+
groups: dict[str, list[dict]] = defaultdict(list)
|
|
109
|
+
for item in db.get_entity_items():
|
|
110
|
+
entity_byname = item["entity_byname"] # always a tuple
|
|
111
|
+
groups[item["entity_class_name"]].append(
|
|
112
|
+
{
|
|
113
|
+
"name": item["name"],
|
|
114
|
+
"entity_byname": entity_byname,
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
dc.entities = dict(groups)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _read_parameter_definitions(db: DatabaseMapping, dc: DatabaseContents) -> None:
|
|
121
|
+
"""Read parameter definitions grouped by entity_class_name."""
|
|
122
|
+
groups: dict[str, list] = defaultdict(list)
|
|
123
|
+
for item in db.get_parameter_definition_items():
|
|
124
|
+
ext = item.extended()
|
|
125
|
+
default_value = None
|
|
126
|
+
if item["default_value"] is not None:
|
|
127
|
+
default_value = from_database(item["default_value"], item["default_type"])
|
|
128
|
+
default_value = _convert_numpy(default_value)
|
|
129
|
+
groups[ext["entity_class_name"]].append(
|
|
130
|
+
{
|
|
131
|
+
"name": item["name"],
|
|
132
|
+
"description": item["description"],
|
|
133
|
+
"parameter_type_list": ext.get("parameter_type_list"),
|
|
134
|
+
"parameter_value_list_name": ext.get("parameter_value_list_name"),
|
|
135
|
+
"default_value": default_value,
|
|
136
|
+
"default_type": item["default_type"],
|
|
137
|
+
"parameter_group_name": ext.get("parameter_group_name"),
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
dc.parameter_definitions = dict(groups)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _read_parameter_values(db: DatabaseMapping, dc: DatabaseContents) -> None:
|
|
144
|
+
"""Read parameter values keyed by (class, entity_byname, param, alt)."""
|
|
145
|
+
for pv in db.get_parameter_value_items():
|
|
146
|
+
value = from_database(pv["value"], pv["type"])
|
|
147
|
+
value = _convert_numpy(value)
|
|
148
|
+
key = (
|
|
149
|
+
pv["entity_class_name"],
|
|
150
|
+
pv["entity_byname"],
|
|
151
|
+
pv["parameter_name"],
|
|
152
|
+
pv["alternative_name"],
|
|
153
|
+
)
|
|
154
|
+
dc.parameter_values[key] = value
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _read_alternatives(db: DatabaseMapping, dc: DatabaseContents) -> None:
|
|
158
|
+
"""Read alternative names."""
|
|
159
|
+
dc.alternatives = [item["name"] for item in db.get_alternative_items()]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _read_scenarios(db: DatabaseMapping, dc: DatabaseContents) -> None:
|
|
163
|
+
"""Read scenarios with their ranked alternatives."""
|
|
164
|
+
scenario_alts: dict[str, list[tuple[str, int]]] = defaultdict(list)
|
|
165
|
+
for sa in db.get_scenario_alternative_items():
|
|
166
|
+
scenario_alts[sa["scenario_name"]].append(
|
|
167
|
+
(sa["alternative_name"], sa["rank"])
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
for sc in db.get_scenario_items():
|
|
171
|
+
name = sc["name"]
|
|
172
|
+
alts = sorted(scenario_alts.get(name, []), key=lambda x: x[1])
|
|
173
|
+
dc.scenarios.append({"name": name, "alternatives": alts})
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _read_entity_alternatives(db: DatabaseMapping, dc: DatabaseContents) -> None:
|
|
177
|
+
"""Read entity alternatives: (class, entity_byname, alt) -> active."""
|
|
178
|
+
for ea in db.get_entity_alternative_items():
|
|
179
|
+
key = (
|
|
180
|
+
ea["entity_class_name"],
|
|
181
|
+
ea["entity_byname"],
|
|
182
|
+
ea["alternative_name"],
|
|
183
|
+
)
|
|
184
|
+
dc.entity_alternatives[key] = bool(ea["active"])
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _read_parameter_groups(db: DatabaseMapping, dc: DatabaseContents) -> None:
|
|
188
|
+
"""Read parameter groups: group_name -> {priority, color}."""
|
|
189
|
+
for pg in db.get_parameter_group_items():
|
|
190
|
+
d = pg._asdict()
|
|
191
|
+
dc.parameter_groups[d["name"]] = {
|
|
192
|
+
"priority": d.get("priority"),
|
|
193
|
+
"color": d.get("color"),
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _read_param_to_group(dc: DatabaseContents) -> None:
|
|
198
|
+
"""Build param_to_group from parameter definitions that have parameter_group_name."""
|
|
199
|
+
for class_name, pdefs in dc.parameter_definitions.items():
|
|
200
|
+
for pdef in pdefs:
|
|
201
|
+
group_name = pdef.get("parameter_group_name")
|
|
202
|
+
if group_name:
|
|
203
|
+
dc.param_to_group[(class_name, pdef["name"])] = group_name
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _read_version(dc: DatabaseContents) -> None:
|
|
207
|
+
"""Extract FlexTool DB version from model.version default_value."""
|
|
208
|
+
model_defs = dc.parameter_definitions.get("model", [])
|
|
209
|
+
for pdef in model_defs:
|
|
210
|
+
if pdef["name"] == "version":
|
|
211
|
+
dv = pdef["default_value"]
|
|
212
|
+
if dv is not None:
|
|
213
|
+
dc.version = float(dv)
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _read_list_values(db: DatabaseMapping, dc: DatabaseContents) -> None:
|
|
218
|
+
"""Read parameter value list contents: list_name -> [values]."""
|
|
219
|
+
groups: dict[str, list] = defaultdict(list)
|
|
220
|
+
for lv in db.get_list_value_items():
|
|
221
|
+
val = from_database(lv["value"], lv["type"])
|
|
222
|
+
val = _convert_numpy(val)
|
|
223
|
+
groups[lv["parameter_value_list_name"]].append(val)
|
|
224
|
+
dc.list_values = dict(groups)
|