mlflow-optimization 0.1.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.
- mlflow_optimization/__init__.py +50 -0
- mlflow_optimization/_bootstrap.py +129 -0
- mlflow_optimization/common/diagnostics.py +19 -0
- mlflow_optimization/common/patching.py +82 -0
- mlflow_optimization/features/annotations/annotate.py +97 -0
- mlflow_optimization/features/annotations/types.py +22 -0
- mlflow_optimization/features/artifacts/policy.py +16 -0
- mlflow_optimization/features/artifacts/ports.py +17 -0
- mlflow_optimization/features/autolog/ports.py +49 -0
- mlflow_optimization/features/autolog/service.py +147 -0
- mlflow_optimization/features/autolog/types.py +31 -0
- mlflow_optimization/features/metadata/collect.py +52 -0
- mlflow_optimization/features/metadata/types.py +42 -0
- mlflow_optimization/features/metrics/normalize.py +121 -0
- mlflow_optimization/features/metrics/types.py +54 -0
- mlflow_optimization/features/outcomes/classify.py +53 -0
- mlflow_optimization/features/outcomes/types.py +40 -0
- mlflow_optimization/features/runs/ports.py +30 -0
- mlflow_optimization/features/runs/record.py +231 -0
- mlflow_optimization/features/runs/types.py +43 -0
- mlflow_optimization/integrations/cp_sat/__init__.py +5 -0
- mlflow_optimization/integrations/cp_sat/adapter.py +464 -0
- mlflow_optimization/integrations/gurobipy/__init__.py +5 -0
- mlflow_optimization/integrations/gurobipy/adapter.py +474 -0
- mlflow_optimization/integrations/mathopt/__init__.py +1 -0
- mlflow_optimization/integrations/mathopt/adapter.py +939 -0
- mlflow_optimization/integrations/mlflow/artifacts.py +44 -0
- mlflow_optimization/integrations/mlflow/runs.py +480 -0
- mlflow_optimization/integrations/pyomo/__init__.py +5 -0
- mlflow_optimization/integrations/pyomo/adapter.py +1499 -0
- mlflow_optimization/integrations/pyscipopt/__init__.py +5 -0
- mlflow_optimization/integrations/pyscipopt/adapter.py +657 -0
- mlflow_optimization/integrations/routing/__init__.py +5 -0
- mlflow_optimization/integrations/routing/adapter.py +429 -0
- mlflow_optimization/py.typed +0 -0
- mlflow_optimization-0.1.0.dist-info/METADATA +52 -0
- mlflow_optimization-0.1.0.dist-info/RECORD +40 -0
- mlflow_optimization-0.1.0.dist-info/WHEEL +4 -0
- mlflow_optimization-0.1.0.dist-info/licenses/LICENSE-APACHE +176 -0
- mlflow_optimization-0.1.0.dist-info/licenses/LICENSE-MIT +21 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
"""Observation of Gurobi solve calls."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from math import inf
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import gurobipy
|
|
9
|
+
from mlflow_optimization.common.patching import Patch, receiver_of
|
|
10
|
+
from mlflow_optimization.features.autolog.ports import (
|
|
11
|
+
ObservationError,
|
|
12
|
+
SolveObserver,
|
|
13
|
+
SolverIntegration,
|
|
14
|
+
)
|
|
15
|
+
from mlflow_optimization.features.autolog.types import SolveObservation
|
|
16
|
+
from mlflow_optimization.features.metadata.types import (
|
|
17
|
+
ParameterOrigin,
|
|
18
|
+
ReportedMetadata,
|
|
19
|
+
SolverParameter,
|
|
20
|
+
)
|
|
21
|
+
from mlflow_optimization.features.metrics.types import (
|
|
22
|
+
MissingReason,
|
|
23
|
+
ObjectiveSense,
|
|
24
|
+
ReportedMetrics,
|
|
25
|
+
)
|
|
26
|
+
from mlflow_optimization.features.outcomes.types import ReportedOutcome, Termination
|
|
27
|
+
|
|
28
|
+
_STATUS_TERMINATIONS: dict[int, tuple[str, Termination]] = {
|
|
29
|
+
gurobipy.GRB.LOADED: ("LOADED", Termination.UNKNOWN),
|
|
30
|
+
gurobipy.GRB.OPTIMAL: ("OPTIMAL", Termination.OPTIMAL),
|
|
31
|
+
gurobipy.GRB.INFEASIBLE: ("INFEASIBLE", Termination.INFEASIBLE),
|
|
32
|
+
gurobipy.GRB.INF_OR_UNBD: ("INF_OR_UNBD", Termination.INFEASIBLE_OR_UNBOUNDED),
|
|
33
|
+
gurobipy.GRB.UNBOUNDED: ("UNBOUNDED", Termination.UNBOUNDED),
|
|
34
|
+
gurobipy.GRB.CUTOFF: ("CUTOFF", Termination.OTHER_LIMIT),
|
|
35
|
+
gurobipy.GRB.ITERATION_LIMIT: ("ITERATION_LIMIT", Termination.ITERATION_LIMIT),
|
|
36
|
+
gurobipy.GRB.NODE_LIMIT: ("NODE_LIMIT", Termination.NODE_LIMIT),
|
|
37
|
+
gurobipy.GRB.TIME_LIMIT: ("TIME_LIMIT", Termination.TIME_LIMIT),
|
|
38
|
+
gurobipy.GRB.SOLUTION_LIMIT: ("SOLUTION_LIMIT", Termination.SOLUTION_LIMIT),
|
|
39
|
+
gurobipy.GRB.INTERRUPTED: ("INTERRUPTED", Termination.INTERRUPTED),
|
|
40
|
+
gurobipy.GRB.NUMERIC: ("NUMERIC", Termination.UNKNOWN),
|
|
41
|
+
gurobipy.GRB.SUBOPTIMAL: ("SUBOPTIMAL", Termination.FEASIBLE),
|
|
42
|
+
gurobipy.GRB.INPROGRESS: ("INPROGRESS", Termination.UNKNOWN),
|
|
43
|
+
gurobipy.GRB.USER_OBJ_LIMIT: ("USER_OBJ_LIMIT", Termination.OTHER_LIMIT),
|
|
44
|
+
gurobipy.GRB.WORK_LIMIT: ("WORK_LIMIT", Termination.OTHER_LIMIT),
|
|
45
|
+
gurobipy.GRB.MEM_LIMIT: ("MEM_LIMIT", Termination.OTHER_LIMIT),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class GurobiAdapter(SolverIntegration):
|
|
50
|
+
"""The Gurobi solver integration."""
|
|
51
|
+
|
|
52
|
+
api: str = "gurobipy"
|
|
53
|
+
|
|
54
|
+
def __init__(self) -> None:
|
|
55
|
+
self._patch: Patch = Patch(gurobipy.Model, "optimize")
|
|
56
|
+
|
|
57
|
+
def install(self, observer: SolveObserver) -> None:
|
|
58
|
+
def factory(original: Any) -> Callable[..., Any]:
|
|
59
|
+
@wraps(original)
|
|
60
|
+
def wrapped(*args: Any, **kwargs: Any) -> Any:
|
|
61
|
+
solve_observer: SolveObserver = observer
|
|
62
|
+
receiver: Any = receiver_of(original, args, kwargs)
|
|
63
|
+
if receiver is None:
|
|
64
|
+
return original(*args, **kwargs)
|
|
65
|
+
|
|
66
|
+
def call() -> Any:
|
|
67
|
+
return original(*args, **kwargs)
|
|
68
|
+
|
|
69
|
+
def extract(
|
|
70
|
+
result: Any | None, error: BaseException | None
|
|
71
|
+
) -> SolveObservation:
|
|
72
|
+
if not isinstance(receiver, gurobipy.Model):
|
|
73
|
+
raise ObservationError(
|
|
74
|
+
"Could not find the Gurobi model for this solve."
|
|
75
|
+
) from error
|
|
76
|
+
model: gurobipy.Model = receiver
|
|
77
|
+
try:
|
|
78
|
+
return _extract(model, error)
|
|
79
|
+
except Exception as observation_error:
|
|
80
|
+
raise ObservationError(
|
|
81
|
+
"Could not build a Gurobi solve observation."
|
|
82
|
+
) from observation_error
|
|
83
|
+
|
|
84
|
+
return solve_observer.observe(self.api, call, extract)
|
|
85
|
+
|
|
86
|
+
return wrapped
|
|
87
|
+
|
|
88
|
+
self._patch.install(factory)
|
|
89
|
+
|
|
90
|
+
def uninstall(self) -> None:
|
|
91
|
+
self._patch.uninstall()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _extract(model: gurobipy.Model, error: BaseException | None) -> SolveObservation:
|
|
95
|
+
metadata: ReportedMetadata = _read_metadata(model)
|
|
96
|
+
if error is not None:
|
|
97
|
+
error_termination: Termination = (
|
|
98
|
+
Termination.INTERRUPTED
|
|
99
|
+
if isinstance(error, KeyboardInterrupt)
|
|
100
|
+
else Termination.ERROR
|
|
101
|
+
)
|
|
102
|
+
error_status: str = (
|
|
103
|
+
"INTERRUPTED" if isinstance(error, KeyboardInterrupt) else "ERROR"
|
|
104
|
+
)
|
|
105
|
+
error_outcome: ReportedOutcome = ReportedOutcome(
|
|
106
|
+
error_status, error_termination, None
|
|
107
|
+
)
|
|
108
|
+
error_metrics: ReportedMetrics = ReportedMetrics()
|
|
109
|
+
return SolveObservation(
|
|
110
|
+
api="gurobipy",
|
|
111
|
+
outcome=error_outcome,
|
|
112
|
+
metrics=error_metrics,
|
|
113
|
+
metadata=metadata,
|
|
114
|
+
)
|
|
115
|
+
raw_status: str
|
|
116
|
+
termination: Termination
|
|
117
|
+
raw_status, termination = _read_status(model)
|
|
118
|
+
solution_count: float | MissingReason = _read_solver_number(model, "SolCount")
|
|
119
|
+
has_solution: bool | None
|
|
120
|
+
if isinstance(solution_count, MissingReason):
|
|
121
|
+
has_solution = None
|
|
122
|
+
else:
|
|
123
|
+
has_solution = solution_count > 0
|
|
124
|
+
has_objective: bool | None = _read_has_objective(model)
|
|
125
|
+
sense: ObjectiveSense | None = _read_sense(model)
|
|
126
|
+
objective_value: float | MissingReason = _read_objective_value(
|
|
127
|
+
model, has_objective, has_solution
|
|
128
|
+
)
|
|
129
|
+
dual_bound: float | MissingReason = _read_dual_bound(model, has_objective)
|
|
130
|
+
solver_time_seconds: float | MissingReason = _read_solver_number(model, "Runtime")
|
|
131
|
+
statistics: dict[str, float | MissingReason] = {
|
|
132
|
+
"node_count": _read_solver_number(model, "NodeCount"),
|
|
133
|
+
"simplex_iterations": _read_solver_number(model, "IterCount"),
|
|
134
|
+
"barrier_iterations": _read_solver_number(model, "BarIterCount"),
|
|
135
|
+
}
|
|
136
|
+
native: dict[str, float] = {}
|
|
137
|
+
if not isinstance(solution_count, MissingReason):
|
|
138
|
+
native["solution_count"] = solution_count
|
|
139
|
+
for attribute_name, metric_name in (("MIPGap", "mip_gap"), ("Work", "work")):
|
|
140
|
+
metric_value: float | MissingReason = _read_solver_number(model, attribute_name)
|
|
141
|
+
if not isinstance(metric_value, MissingReason):
|
|
142
|
+
native[metric_name] = metric_value
|
|
143
|
+
outcome: ReportedOutcome = ReportedOutcome(raw_status, termination, has_solution)
|
|
144
|
+
metrics: ReportedMetrics = ReportedMetrics(
|
|
145
|
+
has_objective=has_objective,
|
|
146
|
+
sense=sense,
|
|
147
|
+
objective_value=objective_value,
|
|
148
|
+
dual_bound=dual_bound,
|
|
149
|
+
solver_time_seconds=solver_time_seconds,
|
|
150
|
+
statistics=statistics,
|
|
151
|
+
native=native,
|
|
152
|
+
)
|
|
153
|
+
return SolveObservation(
|
|
154
|
+
api="gurobipy", outcome=outcome, metrics=metrics, metadata=metadata
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _read_metadata(model: gurobipy.Model) -> ReportedMetadata:
|
|
159
|
+
problem: dict[str, int] = {}
|
|
160
|
+
for attribute_name, problem_name in (
|
|
161
|
+
("NumVars", "num_variables"),
|
|
162
|
+
("NumConstrs", "num_constraints"),
|
|
163
|
+
("NumIntVars", "num_integer_variables"),
|
|
164
|
+
("NumQConstrs", "num_quadratic_constraints"),
|
|
165
|
+
("NumGenConstrs", "num_general_constraints"),
|
|
166
|
+
("NumSOS", "num_sos_constraints"),
|
|
167
|
+
("NumNZs", "num_nonzeros"),
|
|
168
|
+
):
|
|
169
|
+
problem_value: int | None = _read_problem_size(model, attribute_name)
|
|
170
|
+
if problem_value is not None:
|
|
171
|
+
problem[problem_name] = problem_value
|
|
172
|
+
api_version: str | None = _read_api_version()
|
|
173
|
+
solver_version: str | None = _read_solver_version()
|
|
174
|
+
parameters: tuple[SolverParameter, ...] = _read_parameters(model)
|
|
175
|
+
return ReportedMetadata(
|
|
176
|
+
solver="gurobi",
|
|
177
|
+
api_version=api_version,
|
|
178
|
+
solver_version=solver_version,
|
|
179
|
+
problem=problem,
|
|
180
|
+
parameters=parameters,
|
|
181
|
+
wall_time_includes_problem_build=False,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _read_attribute(model: gurobipy.Model, attribute_name: str) -> tuple[bool, Any]:
|
|
186
|
+
try:
|
|
187
|
+
value: Any = getattr(model, attribute_name)
|
|
188
|
+
except Exception: # noqa: BLE001
|
|
189
|
+
return False, None
|
|
190
|
+
return True, value
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _read_status(model: gurobipy.Model) -> tuple[str, Termination]:
|
|
194
|
+
readable: bool
|
|
195
|
+
raw_value: Any
|
|
196
|
+
readable, raw_value = _read_attribute(model, "Status")
|
|
197
|
+
if not readable:
|
|
198
|
+
return "UNKNOWN", Termination.UNKNOWN
|
|
199
|
+
try:
|
|
200
|
+
status_code: int = int(raw_value)
|
|
201
|
+
except Exception: # noqa: BLE001
|
|
202
|
+
return "UNKNOWN", Termination.UNKNOWN
|
|
203
|
+
status_name: str
|
|
204
|
+
termination: Termination
|
|
205
|
+
status_name, termination = _STATUS_TERMINATIONS.get(
|
|
206
|
+
status_code, ("UNKNOWN", Termination.UNKNOWN)
|
|
207
|
+
)
|
|
208
|
+
return f"{status_name} ({status_code})", termination
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _read_solver_number(
|
|
212
|
+
model: gurobipy.Model, attribute_name: str
|
|
213
|
+
) -> float | MissingReason:
|
|
214
|
+
readable: bool
|
|
215
|
+
raw_value: Any
|
|
216
|
+
readable, raw_value = _read_attribute(model, attribute_name)
|
|
217
|
+
if not readable:
|
|
218
|
+
return MissingReason.FAILED
|
|
219
|
+
try:
|
|
220
|
+
value: float = float(raw_value)
|
|
221
|
+
solver_infinity: float = float(gurobipy.GRB.INFINITY)
|
|
222
|
+
except Exception: # noqa: BLE001
|
|
223
|
+
return MissingReason.FAILED
|
|
224
|
+
if value == solver_infinity:
|
|
225
|
+
return inf
|
|
226
|
+
if value == -solver_infinity:
|
|
227
|
+
return -inf
|
|
228
|
+
return value
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _read_problem_size(model: gurobipy.Model, attribute_name: str) -> int | None:
|
|
232
|
+
readable: bool
|
|
233
|
+
raw_value: Any
|
|
234
|
+
readable, raw_value = _read_attribute(model, attribute_name)
|
|
235
|
+
if not readable:
|
|
236
|
+
return None
|
|
237
|
+
try:
|
|
238
|
+
return int(raw_value)
|
|
239
|
+
except Exception: # noqa: BLE001
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _read_has_objective(model: gurobipy.Model) -> bool | None:
|
|
244
|
+
objective_count: int | None = _read_problem_size(model, "NumObj")
|
|
245
|
+
if objective_count is None:
|
|
246
|
+
return None
|
|
247
|
+
if objective_count <= 0:
|
|
248
|
+
return False
|
|
249
|
+
objective_indices: tuple[int | None, ...]
|
|
250
|
+
if objective_count == 1:
|
|
251
|
+
objective_indices = (None,)
|
|
252
|
+
else:
|
|
253
|
+
objective_indices = tuple(range(objective_count))
|
|
254
|
+
objective_unavailable: bool = False
|
|
255
|
+
for objective_index in objective_indices:
|
|
256
|
+
try:
|
|
257
|
+
objective: Any
|
|
258
|
+
if objective_index is None:
|
|
259
|
+
objective = model.getObjective()
|
|
260
|
+
else:
|
|
261
|
+
objective = model.getObjective(objective_index)
|
|
262
|
+
except Exception: # noqa: BLE001
|
|
263
|
+
objective_unavailable = True
|
|
264
|
+
continue
|
|
265
|
+
objective_has_content: bool | None = _objective_expression_has_content(
|
|
266
|
+
objective
|
|
267
|
+
)
|
|
268
|
+
if objective_has_content is True:
|
|
269
|
+
return True
|
|
270
|
+
if objective_has_content is None:
|
|
271
|
+
objective_unavailable = True
|
|
272
|
+
piecewise_objective: bool | None = _has_piecewise_linear_objective(model)
|
|
273
|
+
if piecewise_objective is True:
|
|
274
|
+
return True
|
|
275
|
+
if piecewise_objective is None:
|
|
276
|
+
objective_unavailable = True
|
|
277
|
+
if objective_unavailable:
|
|
278
|
+
return None
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _has_piecewise_linear_objective(model: gurobipy.Model) -> bool | None:
|
|
283
|
+
try:
|
|
284
|
+
variables: list[gurobipy.Var] = model.getVars()
|
|
285
|
+
except Exception: # noqa: BLE001
|
|
286
|
+
return None
|
|
287
|
+
objective_unavailable: bool = False
|
|
288
|
+
for variable in variables:
|
|
289
|
+
try:
|
|
290
|
+
points: Any = model.getPWLObj(variable)
|
|
291
|
+
except Exception: # noqa: BLE001
|
|
292
|
+
objective_unavailable = True
|
|
293
|
+
continue
|
|
294
|
+
if points:
|
|
295
|
+
return True
|
|
296
|
+
if objective_unavailable:
|
|
297
|
+
return None
|
|
298
|
+
return False
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _objective_expression_has_content(objective: Any) -> bool | None:
|
|
302
|
+
quadratic_term_count: int | None
|
|
303
|
+
linear_expression: Any
|
|
304
|
+
if isinstance(objective, gurobipy.QuadExpr):
|
|
305
|
+
try:
|
|
306
|
+
quadratic_term_count = int(objective.size())
|
|
307
|
+
except Exception: # noqa: BLE001
|
|
308
|
+
quadratic_term_count = None
|
|
309
|
+
try:
|
|
310
|
+
linear_expression = objective.getLinExpr()
|
|
311
|
+
except Exception: # noqa: BLE001
|
|
312
|
+
linear_expression = None
|
|
313
|
+
elif isinstance(objective, gurobipy.LinExpr):
|
|
314
|
+
quadratic_term_count = 0
|
|
315
|
+
linear_expression = objective
|
|
316
|
+
elif isinstance(objective, gurobipy.NLExpr):
|
|
317
|
+
return True
|
|
318
|
+
else:
|
|
319
|
+
return None
|
|
320
|
+
linear_term_count: int | None
|
|
321
|
+
if linear_expression is None:
|
|
322
|
+
linear_term_count = None
|
|
323
|
+
constant_value: float | None = None
|
|
324
|
+
else:
|
|
325
|
+
try:
|
|
326
|
+
linear_term_count = int(linear_expression.size())
|
|
327
|
+
except Exception: # noqa: BLE001
|
|
328
|
+
linear_term_count = None
|
|
329
|
+
try:
|
|
330
|
+
constant_value = float(linear_expression.getConstant())
|
|
331
|
+
except Exception: # noqa: BLE001
|
|
332
|
+
constant_value = None
|
|
333
|
+
if quadratic_term_count is not None and quadratic_term_count > 0:
|
|
334
|
+
return True
|
|
335
|
+
if linear_term_count is not None and linear_term_count > 0:
|
|
336
|
+
return True
|
|
337
|
+
if constant_value is not None and constant_value != 0.0:
|
|
338
|
+
return True
|
|
339
|
+
if (
|
|
340
|
+
quadratic_term_count is None
|
|
341
|
+
or linear_term_count is None
|
|
342
|
+
or constant_value is None
|
|
343
|
+
):
|
|
344
|
+
return None
|
|
345
|
+
return False
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _read_sense(model: gurobipy.Model) -> ObjectiveSense | None:
|
|
349
|
+
sense_value: int | None = _read_problem_size(model, "ModelSense")
|
|
350
|
+
if sense_value == gurobipy.GRB.MINIMIZE:
|
|
351
|
+
return ObjectiveSense.MINIMIZE
|
|
352
|
+
if sense_value == gurobipy.GRB.MAXIMIZE:
|
|
353
|
+
return ObjectiveSense.MAXIMIZE
|
|
354
|
+
return None
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _read_objective_value(
|
|
358
|
+
model: gurobipy.Model,
|
|
359
|
+
has_objective: bool | None,
|
|
360
|
+
has_solution: bool | None,
|
|
361
|
+
) -> float | MissingReason:
|
|
362
|
+
if has_objective is False:
|
|
363
|
+
return MissingReason.UNDEFINED
|
|
364
|
+
if has_solution is False:
|
|
365
|
+
return MissingReason.NO_SOLUTION
|
|
366
|
+
return _read_solver_number(model, "ObjVal")
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _read_dual_bound(
|
|
370
|
+
model: gurobipy.Model, has_objective: bool | None
|
|
371
|
+
) -> float | MissingReason:
|
|
372
|
+
if has_objective is False:
|
|
373
|
+
return MissingReason.UNDEFINED
|
|
374
|
+
return _read_solver_number(model, "ObjBound")
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _read_parameters(model: gurobipy.Model) -> tuple[SolverParameter, ...]:
|
|
378
|
+
readable: bool
|
|
379
|
+
parameter_set: Any
|
|
380
|
+
readable, parameter_set = _read_attribute(model, "Params")
|
|
381
|
+
if not readable:
|
|
382
|
+
return ()
|
|
383
|
+
parameter_names: list[str] = []
|
|
384
|
+
try:
|
|
385
|
+
available_parameter_names: list[str] = dir(parameter_set)
|
|
386
|
+
except Exception: # noqa: BLE001
|
|
387
|
+
return ()
|
|
388
|
+
for parameter_name in available_parameter_names:
|
|
389
|
+
if parameter_name and parameter_name[0].isupper():
|
|
390
|
+
parameter_names.append(parameter_name)
|
|
391
|
+
parameters: list[SolverParameter] = []
|
|
392
|
+
for parameter_name in parameter_names:
|
|
393
|
+
try:
|
|
394
|
+
parameter_info: Any = model.getParamInfo(parameter_name)
|
|
395
|
+
except Exception: # noqa: BLE001,S112
|
|
396
|
+
continue
|
|
397
|
+
try:
|
|
398
|
+
parameter_type: Any = parameter_info[1]
|
|
399
|
+
except Exception: # noqa: BLE001,S112
|
|
400
|
+
continue
|
|
401
|
+
# NOTE: LicenseID identifies a WLS license.
|
|
402
|
+
if parameter_type is str or parameter_name == "LicenseID":
|
|
403
|
+
continue
|
|
404
|
+
try:
|
|
405
|
+
current_parameter_value: Any = parameter_info[2]
|
|
406
|
+
except Exception: # noqa: BLE001,S112
|
|
407
|
+
continue
|
|
408
|
+
try:
|
|
409
|
+
default_parameter_value: Any = parameter_info[5]
|
|
410
|
+
except Exception: # noqa: BLE001,S112
|
|
411
|
+
continue
|
|
412
|
+
current_value: int | float | str | bool | None = _plain_parameter_value(
|
|
413
|
+
current_parameter_value
|
|
414
|
+
)
|
|
415
|
+
default_value: int | float | str | bool | None = _plain_parameter_value(
|
|
416
|
+
default_parameter_value
|
|
417
|
+
)
|
|
418
|
+
if (
|
|
419
|
+
current_value is None
|
|
420
|
+
or default_value is None
|
|
421
|
+
or current_value == default_value
|
|
422
|
+
):
|
|
423
|
+
continue
|
|
424
|
+
parameters.append(
|
|
425
|
+
SolverParameter(
|
|
426
|
+
name=parameter_name,
|
|
427
|
+
value=current_value,
|
|
428
|
+
origin=ParameterOrigin.EXPLICIT,
|
|
429
|
+
)
|
|
430
|
+
)
|
|
431
|
+
return tuple(parameters)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _plain_parameter_value(value: Any) -> int | float | str | bool | None:
|
|
435
|
+
if isinstance(value, (int, float, str)):
|
|
436
|
+
return value
|
|
437
|
+
return None
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _read_api_version() -> str | None:
|
|
441
|
+
try:
|
|
442
|
+
version_value: Any = getattr(gurobipy, "__version__", None)
|
|
443
|
+
except Exception: # noqa: BLE001
|
|
444
|
+
return None
|
|
445
|
+
if isinstance(version_value, str):
|
|
446
|
+
return version_value
|
|
447
|
+
return None
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _read_solver_version() -> str | None:
|
|
451
|
+
try:
|
|
452
|
+
solver_module: Any = getattr(gurobipy, "gurobi", None)
|
|
453
|
+
except Exception: # noqa: BLE001
|
|
454
|
+
return None
|
|
455
|
+
if solver_module is None:
|
|
456
|
+
return None
|
|
457
|
+
try:
|
|
458
|
+
version_function: Any = getattr(solver_module, "version", None)
|
|
459
|
+
except Exception: # noqa: BLE001
|
|
460
|
+
return None
|
|
461
|
+
if not callable(version_function):
|
|
462
|
+
return None
|
|
463
|
+
try:
|
|
464
|
+
version: Any = version_function()
|
|
465
|
+
except Exception: # noqa: BLE001
|
|
466
|
+
return None
|
|
467
|
+
if not isinstance(version, tuple):
|
|
468
|
+
return None
|
|
469
|
+
version_parts: list[str] = []
|
|
470
|
+
for version_part in version:
|
|
471
|
+
if not isinstance(version_part, int):
|
|
472
|
+
return None
|
|
473
|
+
version_parts.append(str(version_part))
|
|
474
|
+
return ".".join(version_parts)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MathOpt solver integration."""
|