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.
Files changed (322) hide show
  1. flextool/__init__.py +41 -0
  2. flextool/_mem_sampler.py +193 -0
  3. flextool/_resources.py +43 -0
  4. flextool/calibrate/__init__.py +51 -0
  5. flextool/calibrate/__main__.py +11 -0
  6. flextool/calibrate/_cli.py +316 -0
  7. flextool/calibrate/_db_alt.py +166 -0
  8. flextool/calibrate/_final_outputs.py +110 -0
  9. flextool/calibrate/_guard.py +151 -0
  10. flextool/calibrate/_loop.py +558 -0
  11. flextool/calibrate/_readers.py +223 -0
  12. flextool/calibrate/_report.py +263 -0
  13. flextool/calibrate/_sizing.py +699 -0
  14. flextool/calibrate/_solve.py +134 -0
  15. flextool/calibrate/_solve_status.py +495 -0
  16. flextool/cli/__init__.py +9 -0
  17. flextool/cli/_console.py +51 -0
  18. flextool/cli/_timing.py +147 -0
  19. flextool/cli/cmd_execute_flextool_workflow.py +187 -0
  20. flextool/cli/cmd_export_to_tabular.py +56 -0
  21. flextool/cli/cmd_import_sensitivities.py +75 -0
  22. flextool/cli/cmd_migrate_database.py +13 -0
  23. flextool/cli/cmd_open_results_db.py +269 -0
  24. flextool/cli/cmd_read_matpower.py +66 -0
  25. flextool/cli/cmd_read_old_flextool.py +63 -0
  26. flextool/cli/cmd_read_self_describing_tabular_input.py +50 -0
  27. flextool/cli/cmd_read_tabular_input.py +81 -0
  28. flextool/cli/cmd_run_flextool.py +1095 -0
  29. flextool/cli/cmd_scenario_results.py +284 -0
  30. flextool/cli/cmd_solve_mps.py +169 -0
  31. flextool/cli/cmd_update_flextool.py +17 -0
  32. flextool/cli/cmd_write_outputs.py +125 -0
  33. flextool/common_utils/__init__.py +1 -0
  34. flextool/common_utils/plot_mem_shape.py +77 -0
  35. flextool/common_utils/precision.py +451 -0
  36. flextool/decomposition/__init__.py +0 -0
  37. flextool/decomposition/region_decomposition.py +128 -0
  38. flextool/decomposition/region_filter.py +1261 -0
  39. flextool/engine_polars/__init__.py +110 -0
  40. flextool/engine_polars/_axis_enums.py +742 -0
  41. flextool/engine_polars/_benders.py +3462 -0
  42. flextool/engine_polars/_block_layout.py +1479 -0
  43. flextool/engine_polars/_blocks.py +1515 -0
  44. flextool/engine_polars/_commodity_ladder.py +660 -0
  45. flextool/engine_polars/_cumulative_invest.py +1165 -0
  46. flextool/engine_polars/_db_loader.py +153 -0
  47. flextool/engine_polars/_db_reader.py +127 -0
  48. flextool/engine_polars/_dc_power_flow.py +445 -0
  49. flextool/engine_polars/_delay.py +442 -0
  50. flextool/engine_polars/_derived_arithmetic.py +432 -0
  51. flextool/engine_polars/_derived_block.py +990 -0
  52. flextool/engine_polars/_derived_branch.py +769 -0
  53. flextool/engine_polars/_derived_existing.py +1353 -0
  54. flextool/engine_polars/_derived_npv.py +1297 -0
  55. flextool/engine_polars/_derived_params.py +9850 -0
  56. flextool/engine_polars/_derived_profile.py +881 -0
  57. flextool/engine_polars/_derived_walks.py +276 -0
  58. flextool/engine_polars/_determinism.py +70 -0
  59. flextool/engine_polars/_direct_params.py +2186 -0
  60. flextool/engine_polars/_dump_csvs.py +1009 -0
  61. flextool/engine_polars/_emit_arc_unions.py +1631 -0
  62. flextool/engine_polars/_emit_calc_params.py +729 -0
  63. flextool/engine_polars/_emit_chain_params.py +709 -0
  64. flextool/engine_polars/_emit_co2_accumulators.py +400 -0
  65. flextool/engine_polars/_emit_dispatchers.py +690 -0
  66. flextool/engine_polars/_emit_energy_margin.py +125 -0
  67. flextool/engine_polars/_emit_energy_margin_adder.py +290 -0
  68. flextool/engine_polars/_emit_entity_annual.py +428 -0
  69. flextool/engine_polars/_emit_inflow_scaling.py +1420 -0
  70. flextool/engine_polars/_emit_leaf_sets.py +550 -0
  71. flextool/engine_polars/_emit_lp_scaling.py +665 -0
  72. flextool/engine_polars/_emit_mid_sets.py +859 -0
  73. flextool/engine_polars/_emit_pdt_params.py +759 -0
  74. flextool/engine_polars/_emit_per_solve.py +774 -0
  75. flextool/engine_polars/_emit_period_calc.py +504 -0
  76. flextool/engine_polars/_emit_period_params.py +2398 -0
  77. flextool/engine_polars/_emit_provider_io.py +141 -0
  78. flextool/engine_polars/_emit_reserve.py +574 -0
  79. flextool/engine_polars/_emit_solve_time.py +311 -0
  80. flextool/engine_polars/_emit_solve_writers.py +1249 -0
  81. flextool/engine_polars/_flex_data_accumulator.py +388 -0
  82. flextool/engine_polars/_flex_data_provider.py +478 -0
  83. flextool/engine_polars/_group_slack.py +1253 -0
  84. flextool/engine_polars/_inmemory_reader.py +140 -0
  85. flextool/engine_polars/_input_source.py +336 -0
  86. flextool/engine_polars/_invest_seeds.py +191 -0
  87. flextool/engine_polars/_native_input_writer.py +100 -0
  88. flextool/engine_polars/_native_run_model.py +1348 -0
  89. flextool/engine_polars/_orchestration.py +4314 -0
  90. flextool/engine_polars/_output_writer.py +439 -0
  91. flextool/engine_polars/_param_shapes.py +1595 -0
  92. flextool/engine_polars/_parquet_bundle.py +723 -0
  93. flextool/engine_polars/_pdt_join.py +167 -0
  94. flextool/engine_polars/_pdt_lookup.py +547 -0
  95. flextool/engine_polars/_per_solve_sets.py +335 -0
  96. flextool/engine_polars/_projection_params.py +2056 -0
  97. flextool/engine_polars/_provider_keys.py +173 -0
  98. flextool/engine_polars/_provider_translators.py +225 -0
  99. flextool/engine_polars/_recursive_solve.py +703 -0
  100. flextool/engine_polars/_region_filter.py +2508 -0
  101. flextool/engine_polars/_reserve.py +649 -0
  102. flextool/engine_polars/_solve_acceptance.py +331 -0
  103. flextool/engine_polars/_solve_config.py +1001 -0
  104. flextool/engine_polars/_solve_context.py +885 -0
  105. flextool/engine_polars/_solve_handoff.py +164 -0
  106. flextool/engine_polars/_solve_state.py +232 -0
  107. flextool/engine_polars/_solver_base.py +36 -0
  108. flextool/engine_polars/_solver_dispatch.py +511 -0
  109. flextool/engine_polars/_spinedb_reader.py +1165 -0
  110. flextool/engine_polars/_stochastic.py +593 -0
  111. flextool/engine_polars/_subprocess_solve.py +1838 -0
  112. flextool/engine_polars/_timeline.py +1416 -0
  113. flextool/engine_polars/_vectorize.py +438 -0
  114. flextool/engine_polars/_warm.py +858 -0
  115. flextool/engine_polars/autoscale/__init__.py +107 -0
  116. flextool/engine_polars/autoscale/_config.py +218 -0
  117. flextool/engine_polars/autoscale/_layer2.py +1253 -0
  118. flextool/engine_polars/autoscale/_layer2_types.py +584 -0
  119. flextool/engine_polars/autoscale/_quantity_types.py +621 -0
  120. flextool/engine_polars/autoscale/_report.py +336 -0
  121. flextool/engine_polars/chain.py +259 -0
  122. flextool/engine_polars/input.py +6638 -0
  123. flextool/engine_polars/model.py +4754 -0
  124. flextool/env_check.py +388 -0
  125. flextool/export_to_tabular/__init__.py +5 -0
  126. flextool/export_to_tabular/db_reader.py +224 -0
  127. flextool/export_to_tabular/excel_writer.py +3559 -0
  128. flextool/export_to_tabular/export_settings.yaml +377 -0
  129. flextool/export_to_tabular/export_to_excel.py +227 -0
  130. flextool/export_to_tabular/formatting.py +543 -0
  131. flextool/export_to_tabular/sheet_config.py +876 -0
  132. flextool/gui/__init__.py +0 -0
  133. flextool/gui/__main__.py +118 -0
  134. flextool/gui/calibrate_commands.py +184 -0
  135. flextool/gui/calibrate_jobs.py +424 -0
  136. flextool/gui/check_tree.py +142 -0
  137. flextool/gui/cli_format.py +83 -0
  138. flextool/gui/config_parser.py +68 -0
  139. flextool/gui/data_models.py +362 -0
  140. flextool/gui/db_editor_integration.py +202 -0
  141. flextool/gui/db_version_check.py +269 -0
  142. flextool/gui/dialogs/__init__.py +0 -0
  143. flextool/gui/dialogs/add_dialog.py +1098 -0
  144. flextool/gui/dialogs/calibrate_dialog.py +1259 -0
  145. flextool/gui/dialogs/file_picker.py +473 -0
  146. flextool/gui/dialogs/group_picker.py +299 -0
  147. flextool/gui/dialogs/migration_consent_dialog.py +106 -0
  148. flextool/gui/dialogs/migration_progress_dialog.py +237 -0
  149. flextool/gui/dialogs/plot_dialog.py +459 -0
  150. flextool/gui/dialogs/plot_settings_picker.py +2184 -0
  151. flextool/gui/dialogs/project_dialog.py +426 -0
  152. flextool/gui/dialogs/update_dialog.py +212 -0
  153. flextool/gui/downsampling.py +88 -0
  154. flextool/gui/error_handling.py +50 -0
  155. flextool/gui/execution_manager.py +1715 -0
  156. flextool/gui/execution_window.py +1377 -0
  157. flextool/gui/hover_tooltip.py +111 -0
  158. flextool/gui/input_sources.py +730 -0
  159. flextool/gui/main_window.py +6181 -0
  160. flextool/gui/network_graph.py +215 -0
  161. flextool/gui/output_actions.py +393 -0
  162. flextool/gui/output_log_window.py +159 -0
  163. flextool/gui/platform_utils.py +421 -0
  164. flextool/gui/plot_cache.py +88 -0
  165. flextool/gui/plot_canvas.py +543 -0
  166. flextool/gui/plot_config_reader.py +272 -0
  167. flextool/gui/project_utils.py +100 -0
  168. flextool/gui/result_viewer.py +4394 -0
  169. flextool/gui/scenario_key.py +162 -0
  170. flextool/gui/scenario_lists.py +516 -0
  171. flextool/gui/settings_io.py +360 -0
  172. flextool/gui/solve_reader.py +103 -0
  173. flextool/gui/tree_reorder.py +88 -0
  174. flextool/gui/ui_metrics.py +420 -0
  175. flextool/input_derivation/__init__.py +281 -0
  176. flextool/input_derivation/_commodity_ladder.py +375 -0
  177. flextool/input_derivation/_commodity_ladder_sets.py +70 -0
  178. flextool/input_derivation/_dc_power_flow.py +377 -0
  179. flextool/input_derivation/_method_constants.py +77 -0
  180. flextool/input_derivation/_process_method.py +258 -0
  181. flextool/input_derivation/_specs.py +1026 -0
  182. flextool/input_derivation/_validators.py +321 -0
  183. flextool/lean_parquet.py +159 -0
  184. flextool/model_builder/__init__.py +5 -0
  185. flextool/model_builder/build_model.py +589 -0
  186. flextool/model_builder/encoding.py +67 -0
  187. flextool/model_builder/names.py +34 -0
  188. flextool/model_builder/profiles.py +129 -0
  189. flextool/plot_outputs/__init__.py +14 -0
  190. flextool/plot_outputs/axis_helpers.py +355 -0
  191. flextool/plot_outputs/color_template.py +888 -0
  192. flextool/plot_outputs/config.py +171 -0
  193. flextool/plot_outputs/format_helpers.py +345 -0
  194. flextool/plot_outputs/legend_helpers.py +143 -0
  195. flextool/plot_outputs/orchestrator.py +1141 -0
  196. flextool/plot_outputs/perf.py +37 -0
  197. flextool/plot_outputs/plan.py +1787 -0
  198. flextool/plot_outputs/plot_bars.py +1510 -0
  199. flextool/plot_outputs/plot_bars_detail.py +753 -0
  200. flextool/plot_outputs/plot_lines.py +951 -0
  201. flextool/plot_outputs/shared_manifest.py +564 -0
  202. flextool/plot_outputs/subplot_helpers.py +137 -0
  203. flextool/process_inputs/__init__.py +188 -0
  204. flextool/process_inputs/import_old_excel_input.json +4159 -0
  205. flextool/process_inputs/read_matpower.py +451 -0
  206. flextool/process_inputs/read_old_flextool.py +1288 -0
  207. flextool/process_inputs/read_self_describing_excel.py +1423 -0
  208. flextool/process_inputs/read_tabular_with_specification.py +1114 -0
  209. flextool/process_inputs/write_old_flextool_to_db.py +3077 -0
  210. flextool/process_inputs/write_self_describing_to_db.py +977 -0
  211. flextool/process_inputs/write_to_input_db.py +269 -0
  212. flextool/process_outputs/__init__.py +7 -0
  213. flextool/process_outputs/_annualize.py +55 -0
  214. flextool/process_outputs/_inmemory_helpers.py +292 -0
  215. flextool/process_outputs/_output_meta.py +672 -0
  216. flextool/process_outputs/calc_capacity_flows.py +107 -0
  217. flextool/process_outputs/calc_connections.py +136 -0
  218. flextool/process_outputs/calc_costs.py +260 -0
  219. flextool/process_outputs/calc_group_flows.py +192 -0
  220. flextool/process_outputs/calc_slacks.py +103 -0
  221. flextool/process_outputs/calc_storage_vre.py +160 -0
  222. flextool/process_outputs/drop_levels.py +208 -0
  223. flextool/process_outputs/handoff_writers.py +1315 -0
  224. flextool/process_outputs/out_ancillary.py +544 -0
  225. flextool/process_outputs/out_capacity.py +179 -0
  226. flextool/process_outputs/out_costs.py +334 -0
  227. flextool/process_outputs/out_flowgroup.py +189 -0
  228. flextool/process_outputs/out_flows.py +301 -0
  229. flextool/process_outputs/out_group.py +475 -0
  230. flextool/process_outputs/out_node.py +190 -0
  231. flextool/process_outputs/persist_realized_slice.py +601 -0
  232. flextool/process_outputs/process_results.py +24 -0
  233. flextool/process_outputs/read_highs_solution.py +2256 -0
  234. flextool/process_outputs/read_parameters.py +1799 -0
  235. flextool/process_outputs/read_sets.py +1095 -0
  236. flextool/process_outputs/read_variables.py +553 -0
  237. flextool/process_outputs/solve_order.py +81 -0
  238. flextool/process_outputs/spinedb_replay.py +412 -0
  239. flextool/process_outputs/union_realized_slice.py +224 -0
  240. flextool/process_outputs/write_outputs.py +1286 -0
  241. flextool/process_outputs/write_spinedb.py +1267 -0
  242. flextool/representative_periods/__init__.py +5 -0
  243. flextool/representative_periods/clustering.py +165 -0
  244. flextool/representative_periods/force_include.py +563 -0
  245. flextool/representative_periods/netload.py +365 -0
  246. flextool/representative_periods/netload_inputs.py +345 -0
  247. flextool/representative_periods/netload_iterate.py +722 -0
  248. flextool/representative_periods/preprocess.py +948 -0
  249. flextool/representative_periods/scenario_stack.py +195 -0
  250. flextool/representative_periods/weights.py +124 -0
  251. flextool/scenario_comparison/__init__.py +13 -0
  252. flextool/scenario_comparison/config_builder.py +158 -0
  253. flextool/scenario_comparison/constants.py +20 -0
  254. flextool/scenario_comparison/data_models.py +222 -0
  255. flextool/scenario_comparison/db_reader.py +399 -0
  256. flextool/scenario_comparison/dispatch_data.py +1002 -0
  257. flextool/scenario_comparison/dispatch_mappings.py +205 -0
  258. flextool/scenario_comparison/dispatch_plots.py +691 -0
  259. flextool/scenario_comparison/input_entity_colors.py +319 -0
  260. flextool/scenario_comparison/orchestrator.py +453 -0
  261. flextool/scenario_comparison/plan_union.py +244 -0
  262. flextool/scenario_comparison/plot_settings_seed.py +205 -0
  263. flextool/schemas/AXIS_CONTRACT.md +71 -0
  264. flextool/schemas/canonical_databases/howto_aggregate_output.json +6225 -0
  265. flextool/schemas/canonical_databases/howto_connections.json +5606 -0
  266. flextool/schemas/canonical_databases/howto_demand.json +5518 -0
  267. flextool/schemas/canonical_databases/howto_hydro_reservoir.json +6239 -0
  268. flextool/schemas/canonical_databases/howto_hydro_reservoir_with_pump.json +5933 -0
  269. flextool/schemas/canonical_databases/howto_non_sync_and_curtailment.json +5794 -0
  270. flextool/schemas/canonical_databases/howto_ramp_and_start_up.json +5707 -0
  271. flextool/schemas/canonical_databases/howto_stochastics.json +6032 -0
  272. flextool/schemas/canonical_databases/templates_examples.json +13532 -0
  273. flextool/schemas/canonical_databases/templates_time_settings_only.json +5340 -0
  274. flextool/schemas/comparison_settings_template.json +197 -0
  275. flextool/schemas/default_plot_settings.yaml +260 -0
  276. flextool/schemas/default_plots.yaml +2293 -0
  277. flextool/schemas/flextool_axis_contract.json +303 -0
  278. flextool/schemas/flextool_axis_contract.schema.json +247 -0
  279. flextool/schemas/old_flextool_import_template.json +4443 -0
  280. flextool/schemas/output_info_template.json +48 -0
  281. flextool/schemas/output_settings_template.json +256 -0
  282. flextool/schemas/pre_v26/flextool_template_constant_default.json +2105 -0
  283. flextool/schemas/pre_v26/flextool_template_default_optional_output.json +2152 -0
  284. flextool/schemas/pre_v26/flextool_template_default_value.json +2094 -0
  285. flextool/schemas/pre_v26/flextool_template_drop_down.json +2080 -0
  286. flextool/schemas/pre_v26/flextool_template_lifetime_method.json +1990 -0
  287. flextool/schemas/pre_v26/flextool_template_optional_outputs.json +2094 -0
  288. flextool/schemas/pre_v26/flextool_template_output_node_flows.json +2105 -0
  289. flextool/schemas/pre_v26/flextool_template_results_master.json +493 -0
  290. flextool/schemas/pre_v26/flextool_template_rolling_start_remove.json +2087 -0
  291. flextool/schemas/pre_v26/flextool_template_rolling_window.json +2059 -0
  292. flextool/schemas/pre_v26/flextool_template_storage_binding_defaults.json +46 -0
  293. flextool/schemas/pre_v26/flextool_template_v2.json +1990 -0
  294. flextool/schemas/pre_v26/flextool_template_v25.json +3864 -0
  295. flextool/schemas/spinedb_results_schema.json +581 -0
  296. flextool/schemas/spinedb_schema.json +4636 -0
  297. flextool/solver_config/copt.opt.template +18 -0
  298. flextool/solver_config/cplex.opt.template +25 -0
  299. flextool/solver_config/gurobi.opt.template +18 -0
  300. flextool/solver_config/highs.opt.template +18 -0
  301. flextool/solver_config/xpress.opt.template +26 -0
  302. flextool/spinedb_backend/__init__.py +26 -0
  303. flextool/spinedb_backend/_axis_enums.py +1119 -0
  304. flextool/spinedb_backend/_backend.py +1139 -0
  305. flextool/update_flextool/__init__.py +12 -0
  306. flextool/update_flextool/canonical_databases.py +251 -0
  307. flextool/update_flextool/db_migration.py +7108 -0
  308. flextool/update_flextool/ensure_settings_db.py +138 -0
  309. flextool/update_flextool/export_database.py +103 -0
  310. flextool/update_flextool/extend_tests_fixture.py +772 -0
  311. flextool/update_flextool/generate_canonical.py +274 -0
  312. flextool/update_flextool/initialize_database.py +42 -0
  313. flextool/update_flextool/install_info.py +225 -0
  314. flextool/update_flextool/self_update.py +464 -0
  315. flextool/update_flextool/sync_master_json_template.py +125 -0
  316. flextool/update_flextool/test_fixtures.py +187 -0
  317. flextool-4.0.0.dist-info/METADATA +217 -0
  318. flextool-4.0.0.dist-info/RECORD +322 -0
  319. flextool-4.0.0.dist-info/WHEEL +5 -0
  320. flextool-4.0.0.dist-info/entry_points.txt +17 -0
  321. flextool-4.0.0.dist-info/licenses/LICENSE.txt +19 -0
  322. 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,5 @@
1
+ """FlexTool Spine DB to Excel exporter."""
2
+
3
+ from flextool.export_to_tabular.export_to_excel import export_to_excel
4
+
5
+ __all__ = ["export_to_excel"]
@@ -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)