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,464 @@
|
|
|
1
|
+
"""Observation support for OR-Tools CP-SAT."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import ortools
|
|
8
|
+
from ortools.sat import sat_parameters_pb2
|
|
9
|
+
from ortools.sat.python import cp_model
|
|
10
|
+
|
|
11
|
+
from mlflow_optimization.common.patching import Patch, receiver_of
|
|
12
|
+
from mlflow_optimization.features.autolog.ports import (
|
|
13
|
+
ObservationError,
|
|
14
|
+
SolveObserver,
|
|
15
|
+
)
|
|
16
|
+
from mlflow_optimization.features.autolog.types import SolveObservation
|
|
17
|
+
from mlflow_optimization.features.metadata.types import (
|
|
18
|
+
ParameterOrigin,
|
|
19
|
+
ReportedMetadata,
|
|
20
|
+
SolverParameter,
|
|
21
|
+
)
|
|
22
|
+
from mlflow_optimization.features.metrics.types import (
|
|
23
|
+
MissingReason,
|
|
24
|
+
ObjectiveSense,
|
|
25
|
+
ReportedMetrics,
|
|
26
|
+
)
|
|
27
|
+
from mlflow_optimization.features.outcomes.types import ReportedOutcome, Termination
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CpSatAdapter:
|
|
31
|
+
"""The OR-Tools CP-SAT integration."""
|
|
32
|
+
|
|
33
|
+
api: str = "cp_sat"
|
|
34
|
+
_entry_points: tuple[str, ...] = (
|
|
35
|
+
"solve",
|
|
36
|
+
"Solve",
|
|
37
|
+
"SolveWithSolutionCallback",
|
|
38
|
+
"SearchForAllSolutions",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def __init__(self) -> None:
|
|
42
|
+
self._observer: SolveObserver | None = None
|
|
43
|
+
self._patches: list[Patch] = []
|
|
44
|
+
|
|
45
|
+
def install(self, observer: SolveObserver) -> None:
|
|
46
|
+
self._observer = observer
|
|
47
|
+
if self._patches:
|
|
48
|
+
return
|
|
49
|
+
for entry_point in self._entry_points:
|
|
50
|
+
if not hasattr(cp_model.CpSolver, entry_point):
|
|
51
|
+
continue
|
|
52
|
+
patch: Patch = Patch(cp_model.CpSolver, entry_point)
|
|
53
|
+
patch.install(self._wrap)
|
|
54
|
+
self._patches.append(patch)
|
|
55
|
+
|
|
56
|
+
def uninstall(self) -> None:
|
|
57
|
+
for patch in reversed(self._patches):
|
|
58
|
+
patch.uninstall()
|
|
59
|
+
self._patches.clear()
|
|
60
|
+
self._observer = None
|
|
61
|
+
|
|
62
|
+
def _wrap(self, original: Any) -> Callable[..., Any]:
|
|
63
|
+
@wraps(original)
|
|
64
|
+
def wrapped(*args: Any, **kwargs: Any) -> Any:
|
|
65
|
+
observer: SolveObserver | None = self._observer
|
|
66
|
+
if observer is None:
|
|
67
|
+
return original(*args, **kwargs)
|
|
68
|
+
solver: Any | None = receiver_of(original, args, kwargs)
|
|
69
|
+
if solver is None:
|
|
70
|
+
return original(*args, **kwargs)
|
|
71
|
+
model: Any | None = kwargs.get("model")
|
|
72
|
+
if model is None and len(args) > 1:
|
|
73
|
+
model = args[1]
|
|
74
|
+
|
|
75
|
+
def call() -> Any:
|
|
76
|
+
return original(*args, **kwargs)
|
|
77
|
+
|
|
78
|
+
def extract(
|
|
79
|
+
result: Any | None,
|
|
80
|
+
error: BaseException | None,
|
|
81
|
+
) -> SolveObservation:
|
|
82
|
+
return self._extract(solver, model, result, error)
|
|
83
|
+
|
|
84
|
+
return observer.observe(self.api, call, extract)
|
|
85
|
+
|
|
86
|
+
return wrapped
|
|
87
|
+
|
|
88
|
+
def _extract(
|
|
89
|
+
self,
|
|
90
|
+
solver: Any,
|
|
91
|
+
model: Any | None,
|
|
92
|
+
result: Any | None,
|
|
93
|
+
error: BaseException | None,
|
|
94
|
+
) -> SolveObservation:
|
|
95
|
+
try:
|
|
96
|
+
outcome: ReportedOutcome = self._read_outcome(
|
|
97
|
+
solver,
|
|
98
|
+
model,
|
|
99
|
+
result,
|
|
100
|
+
error,
|
|
101
|
+
)
|
|
102
|
+
metrics: ReportedMetrics = self._read_metrics(
|
|
103
|
+
solver,
|
|
104
|
+
model,
|
|
105
|
+
outcome,
|
|
106
|
+
error is not None,
|
|
107
|
+
)
|
|
108
|
+
metadata: ReportedMetadata = self._read_metadata(solver, model)
|
|
109
|
+
return SolveObservation(self.api, outcome, metrics, metadata)
|
|
110
|
+
except Exception as extraction_error:
|
|
111
|
+
raise ObservationError(
|
|
112
|
+
"Could not build a CP-SAT solve observation."
|
|
113
|
+
) from extraction_error
|
|
114
|
+
|
|
115
|
+
def _read_outcome(
|
|
116
|
+
self,
|
|
117
|
+
solver: Any,
|
|
118
|
+
model: Any | None,
|
|
119
|
+
result: Any | None,
|
|
120
|
+
error: BaseException | None,
|
|
121
|
+
) -> ReportedOutcome:
|
|
122
|
+
if error is not None:
|
|
123
|
+
termination: Termination = (
|
|
124
|
+
Termination.INTERRUPTED
|
|
125
|
+
if isinstance(error, KeyboardInterrupt)
|
|
126
|
+
else Termination.ERROR
|
|
127
|
+
)
|
|
128
|
+
raw_status: str = f"{termination.value} ({type(error).__name__})"
|
|
129
|
+
return ReportedOutcome(raw_status, termination, None)
|
|
130
|
+
|
|
131
|
+
raw_status = self._read_raw_status(result)
|
|
132
|
+
if result == cp_model.OPTIMAL:
|
|
133
|
+
termination = Termination.OPTIMAL
|
|
134
|
+
has_solution: bool | None = True
|
|
135
|
+
elif result == cp_model.FEASIBLE:
|
|
136
|
+
termination = Termination.FEASIBLE
|
|
137
|
+
has_solution = True
|
|
138
|
+
elif result == cp_model.INFEASIBLE:
|
|
139
|
+
termination = Termination.INFEASIBLE
|
|
140
|
+
has_solution = False
|
|
141
|
+
elif result == cp_model.MODEL_INVALID:
|
|
142
|
+
termination = Termination.ERROR
|
|
143
|
+
has_solution = False
|
|
144
|
+
elif result == cp_model.UNKNOWN:
|
|
145
|
+
termination = Termination.UNKNOWN
|
|
146
|
+
has_solution = self._read_solution_availability(solver, model)
|
|
147
|
+
else:
|
|
148
|
+
termination = Termination.UNKNOWN
|
|
149
|
+
has_solution = self._read_solution_availability(solver, model)
|
|
150
|
+
return ReportedOutcome(raw_status, termination, has_solution)
|
|
151
|
+
|
|
152
|
+
def _read_raw_status(self, result: Any | None) -> str:
|
|
153
|
+
if result is None:
|
|
154
|
+
return "unknown"
|
|
155
|
+
status_names: dict[Any, str] = {
|
|
156
|
+
cp_model.OPTIMAL: "OPTIMAL",
|
|
157
|
+
cp_model.FEASIBLE: "FEASIBLE",
|
|
158
|
+
cp_model.INFEASIBLE: "INFEASIBLE",
|
|
159
|
+
cp_model.MODEL_INVALID: "MODEL_INVALID",
|
|
160
|
+
cp_model.UNKNOWN: "UNKNOWN",
|
|
161
|
+
}
|
|
162
|
+
status_name: str = status_names.get(result, "unknown")
|
|
163
|
+
if status_name == "unknown":
|
|
164
|
+
try:
|
|
165
|
+
status_name = str(result.name)
|
|
166
|
+
except Exception: # noqa: BLE001
|
|
167
|
+
status_name = "unknown"
|
|
168
|
+
try:
|
|
169
|
+
status_code: int = int(result)
|
|
170
|
+
except Exception: # noqa: BLE001
|
|
171
|
+
return status_name
|
|
172
|
+
if status_name == "unknown":
|
|
173
|
+
return f"unknown ({status_code})"
|
|
174
|
+
return f"{status_name} ({status_code})"
|
|
175
|
+
|
|
176
|
+
def _read_solution_availability(
|
|
177
|
+
self,
|
|
178
|
+
solver: Any,
|
|
179
|
+
model: Any | None,
|
|
180
|
+
) -> bool | None:
|
|
181
|
+
if model is None:
|
|
182
|
+
return None
|
|
183
|
+
try:
|
|
184
|
+
variable_count: int = len(model.proto.variables)
|
|
185
|
+
except Exception: # noqa: BLE001
|
|
186
|
+
return None
|
|
187
|
+
try:
|
|
188
|
+
solution_count: int = len(solver.response_proto.solution)
|
|
189
|
+
except Exception: # noqa: BLE001
|
|
190
|
+
return None
|
|
191
|
+
if variable_count == 0:
|
|
192
|
+
return None
|
|
193
|
+
return solution_count == variable_count
|
|
194
|
+
|
|
195
|
+
def _read_metrics(
|
|
196
|
+
self,
|
|
197
|
+
solver: Any,
|
|
198
|
+
model: Any | None,
|
|
199
|
+
outcome: ReportedOutcome,
|
|
200
|
+
call_failed: bool,
|
|
201
|
+
) -> ReportedMetrics:
|
|
202
|
+
has_objective: bool | None = self._read_has_objective(model)
|
|
203
|
+
sense: ObjectiveSense | None = self._read_sense(model, has_objective)
|
|
204
|
+
if has_objective is False:
|
|
205
|
+
objective_value: float | MissingReason = MissingReason.UNSUPPORTED
|
|
206
|
+
dual_bound: float | MissingReason = MissingReason.UNSUPPORTED
|
|
207
|
+
elif call_failed:
|
|
208
|
+
objective_value = MissingReason.FAILED
|
|
209
|
+
dual_bound = MissingReason.FAILED
|
|
210
|
+
elif has_objective is None:
|
|
211
|
+
objective_value = MissingReason.UNDEFINED
|
|
212
|
+
dual_bound = MissingReason.UNDEFINED
|
|
213
|
+
else:
|
|
214
|
+
objective_value = self._read_objective_value(
|
|
215
|
+
solver,
|
|
216
|
+
outcome.has_solution,
|
|
217
|
+
)
|
|
218
|
+
dual_bound = self._read_dual_bound(solver, outcome)
|
|
219
|
+
if call_failed:
|
|
220
|
+
solver_time_seconds: float | MissingReason = MissingReason.FAILED
|
|
221
|
+
native: dict[str, float] = {}
|
|
222
|
+
else:
|
|
223
|
+
solver_time_seconds = self._read_response_metric(solver, "wall_time")
|
|
224
|
+
native = self._read_native_metrics(solver)
|
|
225
|
+
return ReportedMetrics(
|
|
226
|
+
has_objective=has_objective,
|
|
227
|
+
sense=sense,
|
|
228
|
+
objective_value=objective_value,
|
|
229
|
+
dual_bound=dual_bound,
|
|
230
|
+
solver_time_seconds=solver_time_seconds,
|
|
231
|
+
statistics={},
|
|
232
|
+
native=native,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def _read_has_objective(self, model: Any | None) -> bool | None:
|
|
236
|
+
if model is None:
|
|
237
|
+
return None
|
|
238
|
+
try:
|
|
239
|
+
model_proto: Any = model.proto
|
|
240
|
+
except Exception: # noqa: BLE001
|
|
241
|
+
return None
|
|
242
|
+
has_floating_point_objective: bool | None = None
|
|
243
|
+
try:
|
|
244
|
+
has_floating_point_objective = bool(
|
|
245
|
+
model_proto.has_floating_point_objective()
|
|
246
|
+
)
|
|
247
|
+
except Exception: # noqa: BLE001,S110
|
|
248
|
+
pass
|
|
249
|
+
if has_floating_point_objective is True:
|
|
250
|
+
return True
|
|
251
|
+
has_integer_objective: bool | None = None
|
|
252
|
+
try:
|
|
253
|
+
has_integer_objective = bool(model_proto.has_objective())
|
|
254
|
+
except Exception: # noqa: BLE001,S110
|
|
255
|
+
pass
|
|
256
|
+
if has_integer_objective is True:
|
|
257
|
+
return True
|
|
258
|
+
if has_floating_point_objective is False and has_integer_objective is False:
|
|
259
|
+
return False
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
def _read_sense(
|
|
263
|
+
self,
|
|
264
|
+
model: Any | None,
|
|
265
|
+
has_objective: bool | None,
|
|
266
|
+
) -> ObjectiveSense | None:
|
|
267
|
+
if model is None or has_objective is not True:
|
|
268
|
+
return None
|
|
269
|
+
try:
|
|
270
|
+
model_proto: Any = model.proto
|
|
271
|
+
except Exception: # noqa: BLE001
|
|
272
|
+
return None
|
|
273
|
+
has_floating_point_objective: bool | None = None
|
|
274
|
+
try:
|
|
275
|
+
has_floating_point_objective = bool(
|
|
276
|
+
model_proto.has_floating_point_objective()
|
|
277
|
+
)
|
|
278
|
+
except Exception: # noqa: BLE001,S110
|
|
279
|
+
pass
|
|
280
|
+
if has_floating_point_objective is True:
|
|
281
|
+
try:
|
|
282
|
+
maximize: bool = bool(model_proto.floating_point_objective.maximize)
|
|
283
|
+
except Exception: # noqa: BLE001
|
|
284
|
+
return None
|
|
285
|
+
return ObjectiveSense.MAXIMIZE if maximize else ObjectiveSense.MINIMIZE
|
|
286
|
+
if has_floating_point_objective is not False:
|
|
287
|
+
return None
|
|
288
|
+
try:
|
|
289
|
+
scaling_factor: float = float(model_proto.objective.scaling_factor)
|
|
290
|
+
except Exception: # noqa: BLE001
|
|
291
|
+
return None
|
|
292
|
+
if scaling_factor < 0:
|
|
293
|
+
return ObjectiveSense.MAXIMIZE
|
|
294
|
+
if scaling_factor > 0:
|
|
295
|
+
return ObjectiveSense.MINIMIZE
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
def _read_objective_value(
|
|
299
|
+
self,
|
|
300
|
+
solver: Any,
|
|
301
|
+
has_solution: bool | None,
|
|
302
|
+
) -> float | MissingReason:
|
|
303
|
+
if has_solution is False:
|
|
304
|
+
return MissingReason.NO_SOLUTION
|
|
305
|
+
if has_solution is None:
|
|
306
|
+
return MissingReason.UNDEFINED
|
|
307
|
+
return self._read_response_metric(solver, "objective_value")
|
|
308
|
+
|
|
309
|
+
def _read_dual_bound(
|
|
310
|
+
self,
|
|
311
|
+
solver: Any,
|
|
312
|
+
outcome: ReportedOutcome,
|
|
313
|
+
) -> float | MissingReason:
|
|
314
|
+
if outcome.has_solution is True:
|
|
315
|
+
return self._read_response_metric(solver, "best_objective_bound")
|
|
316
|
+
if outcome.termination is not Termination.UNKNOWN:
|
|
317
|
+
return MissingReason.UNDEFINED
|
|
318
|
+
value: float | MissingReason = self._read_response_metric(
|
|
319
|
+
solver,
|
|
320
|
+
"best_objective_bound",
|
|
321
|
+
)
|
|
322
|
+
if isinstance(value, MissingReason):
|
|
323
|
+
return value
|
|
324
|
+
if value != 0.0:
|
|
325
|
+
return value
|
|
326
|
+
return MissingReason.UNDEFINED
|
|
327
|
+
|
|
328
|
+
def _read_response_metric(
|
|
329
|
+
self,
|
|
330
|
+
solver: Any,
|
|
331
|
+
name: str,
|
|
332
|
+
) -> float | MissingReason:
|
|
333
|
+
try:
|
|
334
|
+
response: Any = solver.response_proto
|
|
335
|
+
value: float = float(getattr(response, name))
|
|
336
|
+
except Exception: # noqa: BLE001
|
|
337
|
+
return MissingReason.FAILED
|
|
338
|
+
return value
|
|
339
|
+
|
|
340
|
+
def _read_native_metrics(self, solver: Any) -> dict[str, float]:
|
|
341
|
+
native_fields: tuple[str, ...] = (
|
|
342
|
+
"user_time",
|
|
343
|
+
"deterministic_time",
|
|
344
|
+
"num_branches",
|
|
345
|
+
"num_conflicts",
|
|
346
|
+
"num_booleans",
|
|
347
|
+
"num_integers",
|
|
348
|
+
"num_fixed_booleans",
|
|
349
|
+
"num_binary_propagations",
|
|
350
|
+
"num_integer_propagations",
|
|
351
|
+
"num_restarts",
|
|
352
|
+
"num_lp_iterations",
|
|
353
|
+
"gap_integral",
|
|
354
|
+
)
|
|
355
|
+
native: dict[str, float] = {}
|
|
356
|
+
for name in native_fields:
|
|
357
|
+
metric_value: float | MissingReason = self._read_response_metric(
|
|
358
|
+
solver,
|
|
359
|
+
name,
|
|
360
|
+
)
|
|
361
|
+
if not isinstance(metric_value, MissingReason):
|
|
362
|
+
native[name] = metric_value
|
|
363
|
+
return native
|
|
364
|
+
|
|
365
|
+
def _read_metadata(self, solver: Any, model: Any | None) -> ReportedMetadata:
|
|
366
|
+
problem: dict[str, int] = self._read_problem_size(model)
|
|
367
|
+
parameters: tuple[SolverParameter, ...] = self._read_parameters(solver)
|
|
368
|
+
return ReportedMetadata(
|
|
369
|
+
solver="cp_sat",
|
|
370
|
+
api_version=ortools.__version__,
|
|
371
|
+
solver_version=ortools.__version__,
|
|
372
|
+
problem=problem,
|
|
373
|
+
parameters=parameters,
|
|
374
|
+
wall_time_includes_problem_build=False,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
def _read_problem_size(self, model: Any | None) -> dict[str, int]:
|
|
378
|
+
if model is None:
|
|
379
|
+
return {}
|
|
380
|
+
try:
|
|
381
|
+
model_proto: Any = model.proto
|
|
382
|
+
except Exception: # noqa: BLE001
|
|
383
|
+
return {}
|
|
384
|
+
problem: dict[str, int] = {}
|
|
385
|
+
variables: Any | None = None
|
|
386
|
+
try:
|
|
387
|
+
variables = model_proto.variables
|
|
388
|
+
if variables is not None:
|
|
389
|
+
variable_count: int = len(variables)
|
|
390
|
+
problem["num_variables"] = variable_count
|
|
391
|
+
problem["num_integer_variables"] = variable_count
|
|
392
|
+
except Exception: # noqa: BLE001
|
|
393
|
+
variables = None
|
|
394
|
+
try:
|
|
395
|
+
problem["num_constraints"] = len(model_proto.constraints)
|
|
396
|
+
except Exception: # noqa: BLE001,S110
|
|
397
|
+
pass
|
|
398
|
+
if variables is not None:
|
|
399
|
+
boolean_variable_count: int = 0
|
|
400
|
+
boolean_variable_count_known: bool = True
|
|
401
|
+
for variable in variables:
|
|
402
|
+
try:
|
|
403
|
+
domain: list[int] = list(variable.domain)
|
|
404
|
+
if domain == [0, 1]:
|
|
405
|
+
boolean_variable_count += 1
|
|
406
|
+
except Exception: # noqa: BLE001
|
|
407
|
+
boolean_variable_count_known = False
|
|
408
|
+
if boolean_variable_count_known:
|
|
409
|
+
problem["num_boolean_variables"] = boolean_variable_count
|
|
410
|
+
return problem
|
|
411
|
+
|
|
412
|
+
def _read_parameters(self, solver: Any) -> tuple[SolverParameter, ...]:
|
|
413
|
+
try:
|
|
414
|
+
configured_parameters: Any = solver.parameters
|
|
415
|
+
default_parameters: Any = sat_parameters_pb2.SatParameters()
|
|
416
|
+
except Exception: # noqa: BLE001
|
|
417
|
+
return ()
|
|
418
|
+
parameters: list[SolverParameter] = []
|
|
419
|
+
parameter: SolverParameter | None
|
|
420
|
+
for field in sat_parameters_pb2.SatParameters.DESCRIPTOR.fields:
|
|
421
|
+
parameter = None
|
|
422
|
+
try:
|
|
423
|
+
configured_value: Any = getattr(configured_parameters, field.name)
|
|
424
|
+
default_value: Any = getattr(default_parameters, field.name)
|
|
425
|
+
if field.is_repeated:
|
|
426
|
+
configured_value = list(configured_value)
|
|
427
|
+
default_value = list(default_value)
|
|
428
|
+
if configured_value == default_value:
|
|
429
|
+
continue
|
|
430
|
+
value: int | float | str | bool = self._parameter_value(
|
|
431
|
+
configured_value,
|
|
432
|
+
field,
|
|
433
|
+
)
|
|
434
|
+
parameter = SolverParameter(field.name, value, ParameterOrigin.EXPLICIT)
|
|
435
|
+
except Exception: # noqa: BLE001
|
|
436
|
+
parameter = None
|
|
437
|
+
if parameter is not None:
|
|
438
|
+
parameters.append(parameter)
|
|
439
|
+
return tuple(parameters)
|
|
440
|
+
|
|
441
|
+
def _parameter_value(self, value: Any, field: Any) -> int | float | str | bool:
|
|
442
|
+
if field.is_repeated:
|
|
443
|
+
if field.enum_type is None:
|
|
444
|
+
return str(list(value))
|
|
445
|
+
enum_values: list[str | int] = []
|
|
446
|
+
for number in value:
|
|
447
|
+
repeated_enum_value: Any | None = field.enum_type.values_by_number.get(
|
|
448
|
+
number
|
|
449
|
+
)
|
|
450
|
+
enum_values.append(
|
|
451
|
+
str(repeated_enum_value.name)
|
|
452
|
+
if repeated_enum_value is not None
|
|
453
|
+
else number
|
|
454
|
+
)
|
|
455
|
+
return str(enum_values)
|
|
456
|
+
if field.enum_type is not None:
|
|
457
|
+
number: int = int(value)
|
|
458
|
+
enum_value: Any | None = field.enum_type.values_by_number.get(number)
|
|
459
|
+
if enum_value is not None:
|
|
460
|
+
return str(enum_value.name)
|
|
461
|
+
return number
|
|
462
|
+
if isinstance(value, int | float | str):
|
|
463
|
+
return value
|
|
464
|
+
return str(value)
|