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,511 @@
|
|
|
1
|
+
"""Multi-solver dispatch helpers (Phases 2 + 3 of the FlexTool multi-solver port).
|
|
2
|
+
|
|
3
|
+
This module owns:
|
|
4
|
+
|
|
5
|
+
* :func:`build_solver_options` — translate :class:`SolverConfig` into
|
|
6
|
+
the option dict polar-high's ``solve()`` consumes (Phase 2).
|
|
7
|
+
* :func:`run_one_solve` — dispatch a single :class:`polar_high.Problem`
|
|
8
|
+
to either ``Problem.solve()`` (in-process HiGHS, default — preserves
|
|
9
|
+
streaming + ``Solution.highs``) or
|
|
10
|
+
:func:`flextool.engine_polars._subprocess_solve.solve_via_subprocess`
|
|
11
|
+
(every cold path — HiGHS via ``cmd_solve_mps`` and commercial solvers
|
|
12
|
+
via their CLI binaries; both return real ``polar_high.Solution``
|
|
13
|
+
objects with the HiGHS instance read back from the MPS).
|
|
14
|
+
* :class:`FlexToolUserError` — surface user-facing errors from the
|
|
15
|
+
commercial path with actionable hints.
|
|
16
|
+
|
|
17
|
+
See ``specs/flextool-multi-solver-handoff.md`` Step 3 for the design
|
|
18
|
+
rationale and the canonical _PARAM_MAP table.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import TYPE_CHECKING, Any, Mapping
|
|
25
|
+
|
|
26
|
+
from flextool.engine_polars._solve_config import SolverConfig
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
from polar_high import Problem
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Effective HiGHS-options resolver (Batch C.1)
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _parse_highs_opt_file(path: Path | None) -> dict[str, str]:
|
|
40
|
+
"""Parse a HiGHS-style ``key=value`` options file into a flat dict.
|
|
41
|
+
|
|
42
|
+
HiGHS' ``highs.opt`` syntax is one ``key = value`` line per option
|
|
43
|
+
with optional surrounding whitespace; lines starting with ``#`` and
|
|
44
|
+
blank lines are comments and skipped. Unknown / malformed lines
|
|
45
|
+
are also skipped (HiGHS itself tolerates them) so the floor never
|
|
46
|
+
fails the engine; the user sees the misparse in HiGHS' own warning
|
|
47
|
+
output when it loads the file.
|
|
48
|
+
|
|
49
|
+
Returns an empty dict when *path* is None or does not exist — that
|
|
50
|
+
is the steady state for in-process engine runs (the file is read
|
|
51
|
+
by HiGHS on the CLI path only). The resolver still calls this
|
|
52
|
+
helper so a future change to wire the file through
|
|
53
|
+
``set_solver_options`` is a one-call edit.
|
|
54
|
+
"""
|
|
55
|
+
if path is None or not path.is_file():
|
|
56
|
+
return {}
|
|
57
|
+
options: dict[str, str] = {}
|
|
58
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
59
|
+
stripped = line.strip()
|
|
60
|
+
if not stripped or stripped.startswith("#"):
|
|
61
|
+
continue
|
|
62
|
+
if "=" not in stripped:
|
|
63
|
+
continue
|
|
64
|
+
key, _, val = stripped.partition("=")
|
|
65
|
+
options[key.strip()] = val.strip()
|
|
66
|
+
return options
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _resolve_effective_highs_options(
|
|
70
|
+
*,
|
|
71
|
+
solver_arguments_map: Mapping[str, Any] | None,
|
|
72
|
+
highs_opt_path: Path | None,
|
|
73
|
+
cli_overrides: Mapping[str, Any] | None,
|
|
74
|
+
baseline: Mapping[str, Any] | None = None,
|
|
75
|
+
) -> dict[str, Any]:
|
|
76
|
+
"""Resolve the effective HiGHS solver options for one solve.
|
|
77
|
+
|
|
78
|
+
Precedence (lowest → highest):
|
|
79
|
+
|
|
80
|
+
1. ``baseline`` — engine-pinned defaults from
|
|
81
|
+
:func:`flextool.engine_polars._orchestration._baseline_highs_options`
|
|
82
|
+
(Curtis-Reid simplex scale + the four determinism keys from
|
|
83
|
+
:data:`flextool.engine_polars._determinism.DETERMINISM_OPTIONS`).
|
|
84
|
+
Each higher layer may overwrite these — operator intent wins.
|
|
85
|
+
2. ``highs.opt`` — floor parsed from ``solver_config/highs.opt``
|
|
86
|
+
via :func:`_parse_highs_opt_file`. Project-level defaults the
|
|
87
|
+
user has committed to disk.
|
|
88
|
+
3. ``solver_arguments`` — the 1d-map authored on the active
|
|
89
|
+
solve's ``solver_arguments`` parameter (Batch C.1+).
|
|
90
|
+
4. ``cli_overrides`` — keys injected by CLI flags
|
|
91
|
+
(``--highs-threads``, ``--solver-time-limit``, …) via
|
|
92
|
+
:func:`flextool.engine_polars._orchestration._finalise_highs_options`.
|
|
93
|
+
Highest precedence; the operator's command-line intent is
|
|
94
|
+
authoritative.
|
|
95
|
+
|
|
96
|
+
Empty / ``None`` layers are skipped cleanly.
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
solver_arguments_map
|
|
101
|
+
The 1d-map dict authored on the solve's ``solver_arguments``
|
|
102
|
+
parameter. ``None`` and ``{}`` are equivalent.
|
|
103
|
+
highs_opt_path
|
|
104
|
+
Path to ``solver_config/highs.opt`` (or any equivalent).
|
|
105
|
+
``None`` skips this layer.
|
|
106
|
+
cli_overrides
|
|
107
|
+
Dict of HiGHS option-keys → values to apply at the top of the
|
|
108
|
+
precedence chain. ``None`` and ``{}`` are equivalent.
|
|
109
|
+
baseline
|
|
110
|
+
Optional engine-pinned floor below all other layers. When
|
|
111
|
+
``None`` an empty dict is used (callers that want the
|
|
112
|
+
determinism + scale floor pass it explicitly).
|
|
113
|
+
|
|
114
|
+
Returns
|
|
115
|
+
-------
|
|
116
|
+
dict[str, Any]
|
|
117
|
+
The final option dict ready to forward to
|
|
118
|
+
``polar_high.Problem.set_solver_options``.
|
|
119
|
+
"""
|
|
120
|
+
options: dict[str, Any] = dict(baseline) if baseline else {}
|
|
121
|
+
options.update(_parse_highs_opt_file(highs_opt_path))
|
|
122
|
+
if solver_arguments_map:
|
|
123
|
+
for key, value in solver_arguments_map.items():
|
|
124
|
+
options[str(key)] = value
|
|
125
|
+
if cli_overrides:
|
|
126
|
+
for key, value in cli_overrides.items():
|
|
127
|
+
options[str(key)] = value
|
|
128
|
+
return options
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
# User-facing error type
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class FlexToolUserError(Exception):
|
|
137
|
+
"""Raised for user-actionable misconfiguration of the multi-solver
|
|
138
|
+
dispatch.
|
|
139
|
+
|
|
140
|
+
Carries a message intended for direct surfacing to the user — installer
|
|
141
|
+
hint, license hint, or "switch the solver" hint. ``__cause__`` carries
|
|
142
|
+
the underlying polar-high exception for debugging.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# Per-solver native parameter names for the three "convenience" knobs
|
|
147
|
+
# normalised by FlexTool. Anything outside these three goes through
|
|
148
|
+
# untranslated via ``SolverConfig.options``.
|
|
149
|
+
#
|
|
150
|
+
# Source: ``specs/flextool-multi-solver-handoff.md`` lines 109-136.
|
|
151
|
+
_PARAM_MAP: dict[str, dict[str, str]] = {
|
|
152
|
+
"highs": {"time_limit": "time_limit", "mip_gap": "mip_rel_gap", "threads": "threads"},
|
|
153
|
+
"gurobi": {"time_limit": "TimeLimit", "mip_gap": "MIPGap", "threads": "Threads"},
|
|
154
|
+
"cplex": {"time_limit": "timelimit", "mip_gap": "mip.tolerances.mipgap", "threads": "threads"},
|
|
155
|
+
"xpress": {"time_limit": "maxtime", "mip_gap": "miprelstop", "threads": "threads"},
|
|
156
|
+
"copt": {"time_limit": "TimeLimit", "mip_gap": "RelGap", "threads": "Threads"},
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def build_solver_options(solver_config: SolverConfig) -> dict[str, Any]:
|
|
161
|
+
"""Translate a :class:`SolverConfig` into the raw options dict that
|
|
162
|
+
polar-high's ``solve()`` consumes.
|
|
163
|
+
|
|
164
|
+
1. The three convenience knobs (``time_limit`` / ``mip_gap`` /
|
|
165
|
+
``threads``), when set on *solver_config*, are translated to the
|
|
166
|
+
chosen solver's native parameter name via :data:`_PARAM_MAP`.
|
|
167
|
+
``None`` values are skipped (no override).
|
|
168
|
+
2. The raw ``solver_config.options`` dict is merged on top of the
|
|
169
|
+
translated knobs — **raw options win** on key collisions. The
|
|
170
|
+
user knows what they're doing; if they hand-write
|
|
171
|
+
``solver_arguments = {"TimeLimit": 30}`` and also set
|
|
172
|
+
``solver_time_limit = 60``, the raw value (30) wins.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
solver_config
|
|
177
|
+
Solve-level config built by
|
|
178
|
+
:meth:`flextool.engine_polars._solve_config.SolveConfig.load_from_db`.
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
dict[str, Any]
|
|
183
|
+
Option dict ready to ``**unpack`` into
|
|
184
|
+
:func:`polar_high.solvers.solve`. Empty dict when no
|
|
185
|
+
convenience knobs are set and ``options`` is empty.
|
|
186
|
+
|
|
187
|
+
Raises
|
|
188
|
+
------
|
|
189
|
+
ValueError
|
|
190
|
+
If ``solver_config.name`` is not in :data:`_PARAM_MAP` AND at
|
|
191
|
+
least one convenience knob is set. Raw-options-only with an
|
|
192
|
+
unknown solver passes through silently so users can plug a
|
|
193
|
+
future solver via ``solver_arguments`` before
|
|
194
|
+
:data:`_PARAM_MAP` is updated.
|
|
195
|
+
"""
|
|
196
|
+
has_convenience_knob = (
|
|
197
|
+
solver_config.time_limit is not None
|
|
198
|
+
or solver_config.mip_gap is not None
|
|
199
|
+
or solver_config.threads is not None
|
|
200
|
+
)
|
|
201
|
+
mapping = _PARAM_MAP.get(solver_config.name)
|
|
202
|
+
if mapping is None and has_convenience_knob:
|
|
203
|
+
available = ", ".join(sorted(_PARAM_MAP.keys()))
|
|
204
|
+
raise ValueError(
|
|
205
|
+
f"unknown solver {solver_config.name!r}, expected one of: "
|
|
206
|
+
f"{available}"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
opts: dict[str, Any] = {}
|
|
210
|
+
if mapping is not None:
|
|
211
|
+
if solver_config.time_limit is not None:
|
|
212
|
+
opts[mapping["time_limit"]] = solver_config.time_limit
|
|
213
|
+
if solver_config.mip_gap is not None:
|
|
214
|
+
opts[mapping["mip_gap"]] = solver_config.mip_gap
|
|
215
|
+
if solver_config.threads is not None:
|
|
216
|
+
opts[mapping["threads"]] = solver_config.threads
|
|
217
|
+
|
|
218
|
+
# Raw options take precedence — see docstring.
|
|
219
|
+
opts.update(solver_config.options)
|
|
220
|
+
return opts
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
# Single-solve dispatch (Phase 3)
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def run_one_solve(
|
|
229
|
+
problem: "Problem",
|
|
230
|
+
solver_config: SolverConfig,
|
|
231
|
+
logger: logging.Logger | None = None,
|
|
232
|
+
*,
|
|
233
|
+
save_memory: bool = False,
|
|
234
|
+
solve_name: str | None = None,
|
|
235
|
+
work_folder: "Path | None" = None,
|
|
236
|
+
):
|
|
237
|
+
"""Dispatch *problem* to the chosen solver.
|
|
238
|
+
|
|
239
|
+
The default HiGHS path is byte-identical to the pre-Phase-3 code: a
|
|
240
|
+
direct call to ``problem.solve(keep_solver=True)`` preserves
|
|
241
|
+
streaming, the live ``Solution.highs`` (consumed by the output
|
|
242
|
+
writer adapter), and the established option-resolution chain.
|
|
243
|
+
|
|
244
|
+
The commercial path (gurobi / cplex / xpress / copt) routes through
|
|
245
|
+
:func:`flextool.engine_polars._subprocess_solve.solve_via_subprocess`,
|
|
246
|
+
which spawns the solver's CLI binary against an MPS written by
|
|
247
|
+
``Problem.write_mps`` and reads the .sol back through a read-only
|
|
248
|
+
``highspy.Highs`` populated via ``setSolution``. Downstream consumers
|
|
249
|
+
(``input.py``, ``_emit_co2_accumulators.py``,
|
|
250
|
+
``process_outputs/read_parameters.py``) see a uniform
|
|
251
|
+
:class:`polar_high.Solution` shape regardless of which solver ran.
|
|
252
|
+
|
|
253
|
+
Parameters
|
|
254
|
+
----------
|
|
255
|
+
problem
|
|
256
|
+
The :class:`polar_high.Problem` to solve.
|
|
257
|
+
solver_config
|
|
258
|
+
Resolved per-solve configuration (defaults to HiGHS/direct when
|
|
259
|
+
no ``solver_*`` parameter is authored on the solve).
|
|
260
|
+
logger
|
|
261
|
+
Optional logger. When provided, the commercial-path error
|
|
262
|
+
messages are also logged at ERROR level before being raised.
|
|
263
|
+
save_memory
|
|
264
|
+
When True on the HiGHS path, divert to
|
|
265
|
+
:func:`flextool.engine_polars._subprocess_solve.solve_via_subprocess`:
|
|
266
|
+
polar-high writes the LP to a temp MPS file via
|
|
267
|
+
``Problem.write_mps(release=True)`` (a direct polars→MPS writer
|
|
268
|
+
that never builds a ``highspy.Highs`` instance, peaking at
|
|
269
|
+
~2-3 GB on the largest LPs vs ~45 GB for HiGHS' own
|
|
270
|
+
``writeModel``), then a ``flextool.cli.cmd_solve_mps``
|
|
271
|
+
subprocess solves the MPS in a clean address space. The
|
|
272
|
+
parent reads the solution back via a read-only ``highspy.Highs``
|
|
273
|
+
and wraps it as a ``polar_high.Solution`` identical in shape
|
|
274
|
+
to the in-process return. Trades file I/O for HiGHS' active-
|
|
275
|
+
solve memory living outside the parent process. Also disables
|
|
276
|
+
warm-LP reuse for the cascade — the Problem is in ``_released``
|
|
277
|
+
state after the write. Silently ignored on the commercial path.
|
|
278
|
+
solve_name
|
|
279
|
+
Used by the subprocess path to name MPS/options/solution files.
|
|
280
|
+
Defaults to ``"solve"`` when omitted.
|
|
281
|
+
work_folder
|
|
282
|
+
When provided, the subprocess path keeps its MPS/options/sol
|
|
283
|
+
files under ``<work_folder>/solve_data/subprocess/`` for post-
|
|
284
|
+
mortem inspection. ``None`` uses a self-cleaning tempdir.
|
|
285
|
+
Ignored on the in-process path.
|
|
286
|
+
|
|
287
|
+
Returns
|
|
288
|
+
-------
|
|
289
|
+
polar_high.Solution
|
|
290
|
+
The native polar-high Solution. On the cold/subprocess paths
|
|
291
|
+
the contained ``highs`` instance is a read-only
|
|
292
|
+
``highspy.Highs`` reconstructed from the MPS with the primal /
|
|
293
|
+
dual injected via ``setSolution``.
|
|
294
|
+
|
|
295
|
+
Raises
|
|
296
|
+
------
|
|
297
|
+
FlexToolUserError
|
|
298
|
+
If the requested solver's Python wrapper is not installed, the
|
|
299
|
+
license check fails, or the solver returns a model-level error.
|
|
300
|
+
"""
|
|
301
|
+
if solver_config.name == "highs":
|
|
302
|
+
# Default path: keep ``Problem.solve()`` (preserves streaming +
|
|
303
|
+
# ``Solution.highs`` for the output writer adapter). Forward
|
|
304
|
+
# ``solver_arguments`` and the convenience-knob translations so
|
|
305
|
+
# HiGHS-side users get the same surface as commercial users —
|
|
306
|
+
# ``problem.solve(options=...)`` accepts a dict and routes each
|
|
307
|
+
# key to HiGHS via ``setOptionValue`` (polar-high engine.py).
|
|
308
|
+
highs_options = build_solver_options(solver_config) or None
|
|
309
|
+
if save_memory:
|
|
310
|
+
# Subprocess path: write MPS via Problem.write_mps directly
|
|
311
|
+
# from polars frames, spawn flextool.cli.cmd_solve_mps, read
|
|
312
|
+
# solution back. The effective options are forwarded to the
|
|
313
|
+
# subprocess through the .opt file written by solve_via_
|
|
314
|
+
# subprocess (write_mps itself runs no solver and takes no
|
|
315
|
+
# options). Source: convenience-knob-translated options
|
|
316
|
+
# when present, else whatever was already stored on the
|
|
317
|
+
# Problem via ``set_solver_options`` upstream (autoscale
|
|
318
|
+
# Layer 3 may have mutated those).
|
|
319
|
+
from flextool.engine_polars._subprocess_solve import (
|
|
320
|
+
solve_via_subprocess,
|
|
321
|
+
)
|
|
322
|
+
effective_opts = (
|
|
323
|
+
highs_options if highs_options is not None
|
|
324
|
+
else dict(getattr(problem, "_solver_options", {}) or {})
|
|
325
|
+
)
|
|
326
|
+
return solve_via_subprocess(
|
|
327
|
+
problem,
|
|
328
|
+
"highs",
|
|
329
|
+
effective_opts,
|
|
330
|
+
solve_name=solve_name or "solve",
|
|
331
|
+
logger=logger,
|
|
332
|
+
work_folder=work_folder,
|
|
333
|
+
)
|
|
334
|
+
return problem.solve(
|
|
335
|
+
options=highs_options,
|
|
336
|
+
keep_solver=True,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# Commercial path. Always subprocess: write MPS via the cheap
|
|
340
|
+
# ``Problem.write_mps`` (polars→MPS, no LpView), spawn the solver's
|
|
341
|
+
# CLI binary, parse the .sol. The in-process commercial-solver
|
|
342
|
+
# dispatch was retired alongside the in-process cold-HiGHS path —
|
|
343
|
+
# the goal is a single hard memory bound (~2-3 GB for write_mps on
|
|
344
|
+
# the largest LPs) for every cold solve regardless of solver.
|
|
345
|
+
#
|
|
346
|
+
# Convenience-knob translations are honoured but most commercial
|
|
347
|
+
# knobs (and the raw ``solver_options`` dict) are not yet plumbed
|
|
348
|
+
# into the CLI scripts — see :mod:`_subprocess_solve` for the
|
|
349
|
+
# current contract. ``time_limit`` flows through as the subprocess
|
|
350
|
+
# timeout.
|
|
351
|
+
from flextool.engine_polars._subprocess_solve import (
|
|
352
|
+
_BINARY_NAMES,
|
|
353
|
+
solve_via_subprocess,
|
|
354
|
+
)
|
|
355
|
+
if solver_config.name not in _BINARY_NAMES and solver_config.name != "highs":
|
|
356
|
+
# Unknown solver name — fail clean before we try anything else.
|
|
357
|
+
from polar_high.solvers import available_solvers
|
|
358
|
+
msg = (
|
|
359
|
+
f"Unknown solver {solver_config.name!r}. Supported solvers: "
|
|
360
|
+
f"highs, gurobi, cplex, xpress, copt. Installed wrappers: "
|
|
361
|
+
f"{available_solvers}."
|
|
362
|
+
)
|
|
363
|
+
if logger is not None:
|
|
364
|
+
logger.error(msg)
|
|
365
|
+
raise FlexToolUserError(msg)
|
|
366
|
+
options = build_solver_options(solver_config)
|
|
367
|
+
try:
|
|
368
|
+
return solve_via_subprocess(
|
|
369
|
+
problem,
|
|
370
|
+
solver_config.name,
|
|
371
|
+
options,
|
|
372
|
+
solve_name=solve_name or "solve",
|
|
373
|
+
logger=logger,
|
|
374
|
+
work_folder=work_folder,
|
|
375
|
+
)
|
|
376
|
+
except RuntimeError as e:
|
|
377
|
+
msg = str(e)
|
|
378
|
+
if msg.startswith("LICENSE: "):
|
|
379
|
+
user_msg = (
|
|
380
|
+
f"Solver {solver_config.name!r} subprocess failed a "
|
|
381
|
+
f"license check. Details: {msg[len('LICENSE: '):]}. "
|
|
382
|
+
f"See docs/solvers/{solver_config.name}.md#licensing "
|
|
383
|
+
f"for help."
|
|
384
|
+
)
|
|
385
|
+
if logger is not None:
|
|
386
|
+
logger.error(user_msg)
|
|
387
|
+
raise FlexToolUserError(user_msg) from e
|
|
388
|
+
if "was not found on $PATH" in msg:
|
|
389
|
+
user_msg = (
|
|
390
|
+
f"Solver {solver_config.name!r} CLI binary is not "
|
|
391
|
+
f"installed on this system. {msg} See "
|
|
392
|
+
f"docs/solvers/{solver_config.name}.md for installation "
|
|
393
|
+
f"instructions."
|
|
394
|
+
)
|
|
395
|
+
if logger is not None:
|
|
396
|
+
logger.error(user_msg)
|
|
397
|
+
raise FlexToolUserError(user_msg) from e
|
|
398
|
+
if logger is not None:
|
|
399
|
+
logger.error(msg)
|
|
400
|
+
raise FlexToolUserError(
|
|
401
|
+
f"Solver {solver_config.name!r} subprocess returned an "
|
|
402
|
+
f"error: {msg}. This is usually a model issue (numerics, "
|
|
403
|
+
f"scaling, infeasibility) or a CLI invocation problem."
|
|
404
|
+
) from e
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
_LICENSE_PROBE_CACHE: dict[str, str] | None = None
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def probe_solver_licenses() -> dict[str, str]:
|
|
411
|
+
"""Return ``{solver_name: status}`` for every solver in
|
|
412
|
+
``polar_high.solvers.available_solvers``.
|
|
413
|
+
|
|
414
|
+
Status values:
|
|
415
|
+
|
|
416
|
+
- ``"installed"`` — wrapper installed, license check passed (with
|
|
417
|
+
either a free test license or a commercial one), trivial solve
|
|
418
|
+
completed. Neutral wording — does not imply the user holds a
|
|
419
|
+
full commercial entitlement; a free trial license counts too.
|
|
420
|
+
- ``"no-license"`` — wrapper installed but the solver refused on
|
|
421
|
+
license grounds (commercial trial expired, no licence file, etc.).
|
|
422
|
+
- ``"not-installed"`` — Python wrapper isn't on this system.
|
|
423
|
+
- ``"probe-failed"`` — any other exception during the probe; the
|
|
424
|
+
solver may or may not be functional on a real problem.
|
|
425
|
+
|
|
426
|
+
The probe runs a 1-variable, 0-constraint LP through
|
|
427
|
+
``polar_high.solvers.solve(...)`` per solver. Solver chatter on
|
|
428
|
+
stdout is suppressed. Result cached at module level so repeat
|
|
429
|
+
cascade runs in the same Python process don't re-probe.
|
|
430
|
+
|
|
431
|
+
Used by ``_orchestration.run_chain_from_db`` to print one INFO line
|
|
432
|
+
per cascade, giving users a quick "is gurobi actually working on
|
|
433
|
+
this machine" hint.
|
|
434
|
+
"""
|
|
435
|
+
global _LICENSE_PROBE_CACHE
|
|
436
|
+
if _LICENSE_PROBE_CACHE is not None:
|
|
437
|
+
return _LICENSE_PROBE_CACHE
|
|
438
|
+
import io
|
|
439
|
+
import os
|
|
440
|
+
from contextlib import redirect_stdout, redirect_stderr
|
|
441
|
+
# Tests set FLEXTOOL_SKIP_SOLVER_PROBE=1 in conftest to avoid the
|
|
442
|
+
# FICO Xpress Community LicenseWarning (and other startup chatter)
|
|
443
|
+
# that the probe triggers via xpress.problem(). Skip silently and
|
|
444
|
+
# cache an empty dict so callers' .items() iteration is a no-op.
|
|
445
|
+
if os.environ.get("FLEXTOOL_SKIP_SOLVER_PROBE"):
|
|
446
|
+
_LICENSE_PROBE_CACHE = {}
|
|
447
|
+
return _LICENSE_PROBE_CACHE
|
|
448
|
+
import polars as pl
|
|
449
|
+
from polar_high import Problem
|
|
450
|
+
from polar_high.solvers import (
|
|
451
|
+
LicenseError,
|
|
452
|
+
SolverError,
|
|
453
|
+
SolverNotAvailableError,
|
|
454
|
+
available_solvers,
|
|
455
|
+
)
|
|
456
|
+
from polar_high.solvers import solve as polar_solve
|
|
457
|
+
|
|
458
|
+
statuses: dict[str, str] = {}
|
|
459
|
+
# HiGHS / Xpress write probe chatter via C-level handles that
|
|
460
|
+
# ``redirect_stdout`` alone can't catch. Redirect the underlying
|
|
461
|
+
# file descriptors for the duration of each probe so the startup
|
|
462
|
+
# log stays clean.
|
|
463
|
+
devnull_fd = os.open(os.devnull, os.O_WRONLY)
|
|
464
|
+
saved_stdout_fd = os.dup(1)
|
|
465
|
+
saved_stderr_fd = os.dup(2)
|
|
466
|
+
try:
|
|
467
|
+
for solver_name in available_solvers:
|
|
468
|
+
try:
|
|
469
|
+
p = Problem()
|
|
470
|
+
df = pl.DataFrame({"i": [0]})
|
|
471
|
+
v = p.add_var(
|
|
472
|
+
"x", dims=("i",), index=df, lower=0.0, upper=10.0,
|
|
473
|
+
)
|
|
474
|
+
p.set_objective(v.to_expr())
|
|
475
|
+
os.dup2(devnull_fd, 1)
|
|
476
|
+
os.dup2(devnull_fd, 2)
|
|
477
|
+
with redirect_stdout(io.StringIO()), redirect_stderr(io.StringIO()):
|
|
478
|
+
polar_solve(p, solver_name=solver_name)
|
|
479
|
+
# The probe only confirms that a license file (free
|
|
480
|
+
# test license or commercial) is *installed* — not that
|
|
481
|
+
# the user holds a full commercial entitlement. Use
|
|
482
|
+
# the neutral "installed" wording to avoid overstating.
|
|
483
|
+
statuses[solver_name] = "installed"
|
|
484
|
+
except LicenseError:
|
|
485
|
+
statuses[solver_name] = "no-license"
|
|
486
|
+
except SolverNotAvailableError:
|
|
487
|
+
statuses[solver_name] = "not-installed"
|
|
488
|
+
except SolverError:
|
|
489
|
+
statuses[solver_name] = "solver-error"
|
|
490
|
+
except Exception: # noqa: BLE001 — probe should never crash startup
|
|
491
|
+
statuses[solver_name] = "probe-failed"
|
|
492
|
+
finally:
|
|
493
|
+
os.dup2(saved_stdout_fd, 1)
|
|
494
|
+
os.dup2(saved_stderr_fd, 2)
|
|
495
|
+
finally:
|
|
496
|
+
os.close(devnull_fd)
|
|
497
|
+
os.close(saved_stdout_fd)
|
|
498
|
+
os.close(saved_stderr_fd)
|
|
499
|
+
_LICENSE_PROBE_CACHE = statuses
|
|
500
|
+
return statuses
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
__all__ = [
|
|
504
|
+
"_PARAM_MAP",
|
|
505
|
+
"FlexToolUserError",
|
|
506
|
+
"_parse_highs_opt_file",
|
|
507
|
+
"_resolve_effective_highs_options",
|
|
508
|
+
"build_solver_options",
|
|
509
|
+
"probe_solver_licenses",
|
|
510
|
+
"run_one_solve",
|
|
511
|
+
]
|