stitchlab-optimization 0.0.4__tar.gz → 0.1.0__tar.gz
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.
- {stitchlab_optimization-0.0.4/src/stitchlab_optimization.egg-info → stitchlab_optimization-0.1.0}/PKG-INFO +1 -1
- {stitchlab_optimization-0.0.4 → stitchlab_optimization-0.1.0}/pyproject.toml +1 -1
- {stitchlab_optimization-0.0.4 → stitchlab_optimization-0.1.0}/src/stitchlab_optimization/__init__.py +1 -1
- {stitchlab_optimization-0.0.4 → stitchlab_optimization-0.1.0}/src/stitchlab_optimization/builder/model.py +81 -78
- {stitchlab_optimization-0.0.4 → stitchlab_optimization-0.1.0}/src/stitchlab_optimization/builder/workflow.py +5 -5
- stitchlab_optimization-0.1.0/src/stitchlab_optimization/logger/json_logger.py +46 -0
- stitchlab_optimization-0.1.0/src/stitchlab_optimization/logger/manager.py +221 -0
- {stitchlab_optimization-0.0.4 → stitchlab_optimization-0.1.0}/src/stitchlab_optimization/logger/sqlite_logger.py +14 -26
- stitchlab_optimization-0.1.0/src/stitchlab_optimization/solver/params.py +15 -0
- {stitchlab_optimization-0.0.4 → stitchlab_optimization-0.1.0/src/stitchlab_optimization.egg-info}/PKG-INFO +1 -1
- {stitchlab_optimization-0.0.4 → stitchlab_optimization-0.1.0}/src/stitchlab_optimization.egg-info/SOURCES.txt +2 -2
- stitchlab_optimization-0.0.4/src/stitchlab_optimization/logger/file_logger.py +0 -120
- stitchlab_optimization-0.0.4/src/stitchlab_optimization/logger/manager.py +0 -94
- stitchlab_optimization-0.0.4/src/stitchlab_optimization/solver/config.py +0 -63
- {stitchlab_optimization-0.0.4 → stitchlab_optimization-0.1.0}/LICENSE +0 -0
- {stitchlab_optimization-0.0.4 → stitchlab_optimization-0.1.0}/README.md +0 -0
- {stitchlab_optimization-0.0.4 → stitchlab_optimization-0.1.0}/setup.cfg +0 -0
- {stitchlab_optimization-0.0.4 → stitchlab_optimization-0.1.0}/src/stitchlab_optimization/builder/__init__.py +0 -0
- {stitchlab_optimization-0.0.4 → stitchlab_optimization-0.1.0}/src/stitchlab_optimization/logger/__init__.py +0 -0
- {stitchlab_optimization-0.0.4 → stitchlab_optimization-0.1.0}/src/stitchlab_optimization/solver/__init__.py +0 -0
- {stitchlab_optimization-0.0.4 → stitchlab_optimization-0.1.0}/src/stitchlab_optimization/solver/engine.py +0 -0
- {stitchlab_optimization-0.0.4 → stitchlab_optimization-0.1.0}/src/stitchlab_optimization/solver/status.py +0 -0
- {stitchlab_optimization-0.0.4 → stitchlab_optimization-0.1.0}/src/stitchlab_optimization/tools/query/snowflake.py +0 -0
- {stitchlab_optimization-0.0.4 → stitchlab_optimization-0.1.0}/src/stitchlab_optimization/tools/query/sqlite.py +0 -0
- {stitchlab_optimization-0.0.4 → stitchlab_optimization-0.1.0}/src/stitchlab_optimization/tools/utils.py +0 -0
- {stitchlab_optimization-0.0.4 → stitchlab_optimization-0.1.0}/src/stitchlab_optimization.egg-info/dependency_links.txt +0 -0
- {stitchlab_optimization-0.0.4 → stitchlab_optimization-0.1.0}/src/stitchlab_optimization.egg-info/requires.txt +0 -0
- {stitchlab_optimization-0.0.4 → stitchlab_optimization-0.1.0}/src/stitchlab_optimization.egg-info/top_level.txt +0 -0
|
@@ -11,8 +11,8 @@ from typing import Any, Dict, Generic, Type, Optional, TypeVar, final
|
|
|
11
11
|
|
|
12
12
|
from ..solver.engine import SolverEngine
|
|
13
13
|
from ..solver.status import SolverStatus
|
|
14
|
-
from ..
|
|
15
|
-
from ..
|
|
14
|
+
from ..solver.params import SolverParams
|
|
15
|
+
from ..logger.manager import ModelLog, LogManager, ResourceMonitor, NullResourceMonitor
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
ParamsBaseModel = TypeVar("ParamsBaseModel", bound="ModelParams")
|
|
@@ -51,17 +51,19 @@ class ModelBuilder(Generic[ParamsBaseModel, SolutionBaseModel], ABC):
|
|
|
51
51
|
solution: Optional[SolutionBaseModel] = None
|
|
52
52
|
solver_engine: SolverEngine
|
|
53
53
|
solver_status: SolverStatus
|
|
54
|
+
solver_params: SolverParams
|
|
54
55
|
model: Any = None
|
|
55
56
|
model_output: Any = None
|
|
56
|
-
model_vars: Optional[Dict[str, Any]] = None
|
|
57
|
+
model_vars: Optional[Dict[str, Any]] = None
|
|
57
58
|
runtime_message: str = ""
|
|
58
59
|
runtime_seconds: float = 0
|
|
59
60
|
|
|
60
61
|
@final
|
|
61
|
-
def __init__(self, params: ParamsBaseModel, solver_engine: SolverEngine):
|
|
62
|
+
def __init__(self, params: ParamsBaseModel, solver_engine: SolverEngine, solver_params: SolverParams):
|
|
62
63
|
self.params = params
|
|
63
64
|
self.solver_engine = solver_engine
|
|
64
65
|
self.solver_status = SolverStatus.UNSOLVED
|
|
66
|
+
self.solver_params = solver_params
|
|
65
67
|
|
|
66
68
|
@final
|
|
67
69
|
def _set_model(self, model: Any):
|
|
@@ -78,13 +80,15 @@ class ModelBuilder(Generic[ParamsBaseModel, SolutionBaseModel], ABC):
|
|
|
78
80
|
if self.model is None:
|
|
79
81
|
raise ValueError("Model must be built before execution.")
|
|
80
82
|
|
|
81
|
-
if self.
|
|
82
|
-
raise ValueError("Model variables (model_vars) must be set in the builder
|
|
83
|
+
if self.model_vars is None:
|
|
84
|
+
raise ValueError("Model variables (model_vars) must be set in the builder before execution.")
|
|
83
85
|
|
|
84
86
|
self.solve()
|
|
85
|
-
|
|
87
|
+
|
|
88
|
+
if SolverStatus.is_solution_found(self.solver_status):
|
|
89
|
+
return self.construct_solution()
|
|
86
90
|
|
|
87
|
-
return
|
|
91
|
+
return None
|
|
88
92
|
|
|
89
93
|
@abstractmethod
|
|
90
94
|
def build(self):
|
|
@@ -112,42 +116,38 @@ class ModelBuilder(Generic[ParamsBaseModel, SolutionBaseModel], ABC):
|
|
|
112
116
|
|
|
113
117
|
raise ValueError(f"ERROR while Solving Model : Vars is not saved while building model using solver engine {self.solver_engine}")
|
|
114
118
|
|
|
115
|
-
|
|
116
|
-
self.solve_pyscipopt
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
self.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
self.solve_ortools_scip()
|
|
119
|
+
SOLVER = {
|
|
120
|
+
SolverEngine.PYSCIPOPT: self.solve_pyscipopt,
|
|
121
|
+
SolverEngine.GUROBI: self.solve_gurobi,
|
|
122
|
+
SolverEngine.ORTOOLS_SCIP: self.solve_ortools_scip,
|
|
123
|
+
SolverEngine.ORTOOLS_ROUTING: self.solve_ortools_routing,
|
|
124
|
+
SolverEngine.ORTOOLS_CPSAT: self.solve_ortools_cpsat,
|
|
125
|
+
}
|
|
123
126
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
self.solve_ortools_cpsat()
|
|
129
|
-
|
|
130
|
-
else:
|
|
127
|
+
# with ResourceMonitor() as monitor:
|
|
128
|
+
try:
|
|
129
|
+
SOLVER[self.solver_engine]()
|
|
130
|
+
except KeyError:
|
|
131
131
|
raise ValueError(f"Solver engine {self.solver_engine} not supported")
|
|
132
132
|
|
|
133
133
|
def solve_pyscipopt(self):
|
|
134
|
-
|
|
134
|
+
PARAMS = self.solver_params
|
|
135
135
|
start_sol = None
|
|
136
136
|
|
|
137
|
-
self.model.setParam("display/verblevel",
|
|
137
|
+
self.model.setParam("display/verblevel", PARAMS.MODEL_SOLVER_VERBOSE)
|
|
138
138
|
|
|
139
|
-
self.model.setIntParam("parallel/maxnthreads",
|
|
140
|
-
self.model.setIntParam("parallel/minnthreads",
|
|
139
|
+
self.model.setIntParam("parallel/maxnthreads", PARAMS.LIMIT_MULTI_THREAD)
|
|
140
|
+
self.model.setIntParam("parallel/minnthreads", PARAMS.LIMIT_MULTI_THREAD)
|
|
141
141
|
|
|
142
|
-
if
|
|
142
|
+
if PARAMS.APPLY_HEURISTICS:
|
|
143
143
|
# Phase 1: Heuristics only
|
|
144
144
|
self.model.setHeuristics(SCIP_PARAMSETTING.AGGRESSIVE)
|
|
145
145
|
|
|
146
|
-
self.model.setParam("limits/time",
|
|
147
|
-
self.model.setParam("limits/gap",
|
|
146
|
+
self.model.setParam("limits/time", PARAMS.LIMIT_TIME_MINUTES_HEURISTICS*60)
|
|
147
|
+
self.model.setParam("limits/gap", PARAMS.LIMIT_OPTIMALITY_GAP_HEURISTICS)
|
|
148
148
|
self.model.setParam("limits/nodes", 500) # limit nodes so B&B doesn't go far
|
|
149
149
|
self.model.setParam("presolving/maxrounds", 0) # skip heavy presolve if desired
|
|
150
|
-
self.model.setParam("limits/memory",
|
|
150
|
+
self.model.setParam("limits/memory", PARAMS.LIMIT_MEMORY_MB)
|
|
151
151
|
|
|
152
152
|
self.model.optimize()
|
|
153
153
|
|
|
@@ -162,9 +162,9 @@ class ModelBuilder(Generic[ParamsBaseModel, SolutionBaseModel], ABC):
|
|
|
162
162
|
self.model.resetParams()
|
|
163
163
|
self.model.setHeuristics(SCIP_PARAMSETTING.DEFAULT)
|
|
164
164
|
|
|
165
|
-
self.model.setParam("limits/time",
|
|
166
|
-
self.model.setParam("limits/gap",
|
|
167
|
-
self.model.setParam("limits/memory",
|
|
165
|
+
self.model.setParam("limits/time", PARAMS.LIMIT_TIME_MINUTES_DETERMINISTIC*60)
|
|
166
|
+
self.model.setParam("limits/gap", PARAMS.LIMIT_OPTIMALITY_GAP_DETERMINISTIC)
|
|
167
|
+
self.model.setParam("limits/memory", PARAMS.LIMIT_MEMORY_MB)
|
|
168
168
|
|
|
169
169
|
try:
|
|
170
170
|
if start_sol is not None:
|
|
@@ -182,23 +182,20 @@ class ModelBuilder(Generic[ParamsBaseModel, SolutionBaseModel], ABC):
|
|
|
182
182
|
|
|
183
183
|
self.solver_status = SolverStatus.from_pyscipopt_status(self.model.getStatus())
|
|
184
184
|
print("STATUS", self.model.getStatus(), self.solver_status, "\n\n")
|
|
185
|
-
|
|
186
|
-
if SolverStatus.is_solution_found(self.solver_status):
|
|
187
|
-
self.construct_solution()
|
|
188
|
-
|
|
185
|
+
|
|
189
186
|
def solve_gurobi(self):
|
|
190
|
-
|
|
187
|
+
PARAMS = self.solver_params
|
|
191
188
|
|
|
192
|
-
self.model.setParam('OutputFlag',
|
|
189
|
+
self.model.setParam('OutputFlag', PARAMS.MODEL_SOLVER_VERBOSE)
|
|
193
190
|
|
|
194
191
|
start_sol = None
|
|
195
|
-
if
|
|
192
|
+
if PARAMS.APPLY_HEURISTICS:
|
|
196
193
|
# Phase 1: Heuristics only
|
|
197
|
-
self.model.setParam('TimeLimit',
|
|
198
|
-
self.model.setParam('MIPGap',
|
|
194
|
+
self.model.setParam('TimeLimit', PARAMS.LIMIT_TIME_MINUTES_HEURISTICS * 60)
|
|
195
|
+
self.model.setParam('MIPGap', PARAMS.LIMIT_OPTIMALITY_GAP_HEURISTICS)
|
|
199
196
|
self.model.setParam('NodeLimit', 500) # limit nodes so B&B doesn't go far
|
|
200
197
|
self.model.setParam('Presolve', 0) # skip heavy presolve if desired
|
|
201
|
-
self.model.setParam('Threads',
|
|
198
|
+
self.model.setParam('Threads', PARAMS.LIMIT_MULTI_THREAD)
|
|
202
199
|
|
|
203
200
|
# Set heuristic focus
|
|
204
201
|
self.model.setParam('Heuristics', 0.8) # Aggressive heuristics
|
|
@@ -216,12 +213,12 @@ class ModelBuilder(Generic[ParamsBaseModel, SolutionBaseModel], ABC):
|
|
|
216
213
|
|
|
217
214
|
# Phase 2: Exact MILP solving
|
|
218
215
|
# Reset parameters for exact solving
|
|
219
|
-
self.model.setParam('TimeLimit',
|
|
220
|
-
self.model.setParam('MIPGap',
|
|
216
|
+
self.model.setParam('TimeLimit', PARAMS.LIMIT_TIME_MINUTES_DETERMINISTIC * 60)
|
|
217
|
+
self.model.setParam('MIPGap', PARAMS.LIMIT_OPTIMALITY_GAP_DETERMINISTIC)
|
|
221
218
|
self.model.setParam('NodeLimit', 1000000)
|
|
222
219
|
self.model.setParam('Presolve', -1) # Default presolve
|
|
223
220
|
self.model.setParam('Heuristics', 0.05) # Default heuristics
|
|
224
|
-
self.model.setParam('Threads',
|
|
221
|
+
self.model.setParam('Threads', PARAMS.LIMIT_MULTI_THREAD)
|
|
225
222
|
|
|
226
223
|
try:
|
|
227
224
|
if start_sol is not None:
|
|
@@ -237,11 +234,8 @@ class ModelBuilder(Generic[ParamsBaseModel, SolutionBaseModel], ABC):
|
|
|
237
234
|
self.solver_status = SolverStatus.from_gurobi_status(self.model.status)
|
|
238
235
|
print("STATUS", self.model.status, self.solver_status, "\n\n")
|
|
239
236
|
|
|
240
|
-
if SolverStatus.is_solution_found(self.solver_status):
|
|
241
|
-
self.construct_solution()
|
|
242
|
-
|
|
243
237
|
def solve_ortools_routing(self):
|
|
244
|
-
|
|
238
|
+
PARAMS = self.solver_params
|
|
245
239
|
|
|
246
240
|
search_parameters = pywrapcp.DefaultRoutingSearchParameters()
|
|
247
241
|
search_parameters.first_solution_strategy = (
|
|
@@ -253,24 +247,21 @@ class ModelBuilder(Generic[ParamsBaseModel, SolutionBaseModel], ABC):
|
|
|
253
247
|
)
|
|
254
248
|
|
|
255
249
|
search_parameters.solution_limit = 100
|
|
256
|
-
search_parameters.time_limit.seconds = int(
|
|
250
|
+
search_parameters.time_limit.seconds = int(PARAMS.LIMIT_TIME_MINUTES_DETERMINISTIC * 60)
|
|
257
251
|
|
|
258
252
|
self.solution = self.model.SolveWithParameters(search_parameters)
|
|
259
253
|
|
|
260
254
|
self.solver_status = SolverStatus.from_ortools_routing_status(self.model.status())
|
|
261
255
|
print("STATUS", self.solver_status)
|
|
262
256
|
|
|
263
|
-
if SolverStatus.is_solution_found(self.solver_status):
|
|
264
|
-
self.construct_solution()
|
|
265
|
-
|
|
266
257
|
def solve_ortools_cpsat(self):
|
|
267
|
-
|
|
258
|
+
PARAMS = self.solver_params
|
|
268
259
|
|
|
269
260
|
solver = cp_model.CpSolver()
|
|
270
|
-
solver.parameters.max_time_in_seconds =
|
|
271
|
-
solver.parameters.num_search_workers =
|
|
261
|
+
solver.parameters.max_time_in_seconds = PARAMS.LIMIT_TIME_MINUTES_DETERMINISTIC * 60
|
|
262
|
+
solver.parameters.num_search_workers = PARAMS.LIMIT_MULTI_THREAD
|
|
272
263
|
|
|
273
|
-
solver.parameters.log_search_progress =
|
|
264
|
+
solver.parameters.log_search_progress = PARAMS.MODEL_SOLVER_VERBOSE
|
|
274
265
|
|
|
275
266
|
result_status = solver.Solve(self.model)
|
|
276
267
|
self.model_output = solver
|
|
@@ -278,20 +269,17 @@ class ModelBuilder(Generic[ParamsBaseModel, SolutionBaseModel], ABC):
|
|
|
278
269
|
self.solver_status = SolverStatus.from_ortools_cpsat_status(result_status)
|
|
279
270
|
print("STATUS", result_status, self.solver_status)
|
|
280
271
|
|
|
281
|
-
if SolverStatus.is_solution_found(self.solver_status):
|
|
282
|
-
self.construct_solution()
|
|
283
|
-
|
|
284
272
|
def solve_ortools_scip(self):
|
|
285
|
-
|
|
273
|
+
PARAMS = self.solver_params
|
|
286
274
|
|
|
287
|
-
self.model.SetTimeLimit(int(
|
|
288
|
-
self.model.SetNumThreads(int(
|
|
275
|
+
self.model.SetTimeLimit(int(PARAMS.LIMIT_TIME_MINUTES_DETERMINISTIC * 60 * 1000))
|
|
276
|
+
self.model.SetNumThreads(int(PARAMS.LIMIT_MULTI_THREAD))
|
|
289
277
|
|
|
290
278
|
params_str = (
|
|
291
|
-
f"limits/gap={
|
|
292
|
-
f"limits/memory={
|
|
293
|
-
f"parallel/maxnthreads={int(
|
|
294
|
-
f"lp/threads={int(
|
|
279
|
+
f"limits/gap={PARAMS.LIMIT_OPTIMALITY_GAP_DETERMINISTIC}\n"
|
|
280
|
+
f"limits/memory={PARAMS.LIMIT_MEMORY_MB}\n"
|
|
281
|
+
f"parallel/maxnthreads={int(PARAMS.LIMIT_MULTI_THREAD)}\n"
|
|
282
|
+
f"lp/threads={int(PARAMS.LIMIT_MULTI_THREAD)}\n"
|
|
295
283
|
f"display/verblevel=5\n"
|
|
296
284
|
)
|
|
297
285
|
|
|
@@ -301,9 +289,6 @@ class ModelBuilder(Generic[ParamsBaseModel, SolutionBaseModel], ABC):
|
|
|
301
289
|
self.solver_status = SolverStatus.from_ortools_scip_status(status)
|
|
302
290
|
print("STATUS", status, self.solver_status, "\n\n")
|
|
303
291
|
|
|
304
|
-
if SolverStatus.is_solution_found(self.solver_status):
|
|
305
|
-
self.construct_solution()
|
|
306
|
-
|
|
307
292
|
|
|
308
293
|
class OptimizationModel(Generic[ParamsBaseModel, SolutionBaseModel], ABC, metaclass=ModelMeta):
|
|
309
294
|
id: str
|
|
@@ -311,15 +296,19 @@ class OptimizationModel(Generic[ParamsBaseModel, SolutionBaseModel], ABC, metacl
|
|
|
311
296
|
builders_registry: Dict[SolverEngine, Type[ModelBuilder[ParamsBaseModel, SolutionBaseModel]]]
|
|
312
297
|
builder: ModelBuilder[ParamsBaseModel, SolutionBaseModel]
|
|
313
298
|
|
|
314
|
-
def __init__(self, params: ParamsBaseModel, solver_engine: Optional[SolverEngine] = None):
|
|
299
|
+
def __init__(self, params: ParamsBaseModel, solver_engine: Optional[SolverEngine] = None, solver_params: Optional[SolverParams] = None):
|
|
315
300
|
self.id = str(uuid.uuid4())
|
|
316
301
|
|
|
317
302
|
if solver_engine is None or solver_engine not in self.builders_registry.keys():
|
|
318
303
|
solver_engine = next(iter(self.builders_registry.keys()))
|
|
319
304
|
|
|
305
|
+
if solver_params is None:
|
|
306
|
+
solver_params = SolverParams()
|
|
307
|
+
|
|
320
308
|
self.builder = self.builders_registry[solver_engine](
|
|
321
309
|
params=params,
|
|
322
|
-
solver_engine=solver_engine
|
|
310
|
+
solver_engine=solver_engine,
|
|
311
|
+
solver_params=solver_params
|
|
323
312
|
)
|
|
324
313
|
|
|
325
314
|
@final
|
|
@@ -327,9 +316,18 @@ class OptimizationModel(Generic[ParamsBaseModel, SolutionBaseModel], ABC, metacl
|
|
|
327
316
|
start_time = time.time()
|
|
328
317
|
solution = None
|
|
329
318
|
|
|
319
|
+
monitor = (
|
|
320
|
+
ResourceMonitor(
|
|
321
|
+
interval_seconds=logger.monitor_resource_interval_seconds
|
|
322
|
+
)
|
|
323
|
+
if logger and logger.enable_monitor_resource
|
|
324
|
+
else NullResourceMonitor()
|
|
325
|
+
)
|
|
326
|
+
|
|
330
327
|
try :
|
|
331
|
-
|
|
332
|
-
|
|
328
|
+
with monitor:
|
|
329
|
+
solution = self.builder.execute()
|
|
330
|
+
self.builder.runtime_message = "success"
|
|
333
331
|
|
|
334
332
|
except Exception as e:
|
|
335
333
|
print(f"\033[91m\n>>> ERROR while Solving Model : {e}\n\033[0m")
|
|
@@ -339,8 +337,11 @@ class OptimizationModel(Generic[ParamsBaseModel, SolutionBaseModel], ABC, metacl
|
|
|
339
337
|
end_time = time.time()
|
|
340
338
|
self.builder.runtime_seconds = end_time - start_time
|
|
341
339
|
|
|
342
|
-
if logger is not None and logger.
|
|
343
|
-
|
|
340
|
+
if logger is not None and logger.enable_monitor_optimality:
|
|
341
|
+
log = self._model_log
|
|
342
|
+
log.resource_stats = monitor.stats
|
|
343
|
+
|
|
344
|
+
logger.put_model_log(model_log=log)
|
|
344
345
|
|
|
345
346
|
return solution
|
|
346
347
|
|
|
@@ -354,6 +355,7 @@ class OptimizationModel(Generic[ParamsBaseModel, SolutionBaseModel], ABC, metacl
|
|
|
354
355
|
def _model_log(self) -> ModelLog:
|
|
355
356
|
builder = self.builder
|
|
356
357
|
solver_engine = self.builder.solver_engine
|
|
358
|
+
solver_params = self.builder.solver_params
|
|
357
359
|
|
|
358
360
|
if solver_engine == SolverEngine.GUROBI:
|
|
359
361
|
# Gurobi Python API
|
|
@@ -427,6 +429,7 @@ class OptimizationModel(Generic[ParamsBaseModel, SolutionBaseModel], ABC, metacl
|
|
|
427
429
|
|
|
428
430
|
return ModelLog(
|
|
429
431
|
solver_engine=solver_engine,
|
|
432
|
+
solver_params=solver_params,
|
|
430
433
|
model_id=self.id,
|
|
431
434
|
model_name=self.name,
|
|
432
435
|
status=builder.solver_status,
|
|
@@ -10,7 +10,7 @@ import traceback
|
|
|
10
10
|
from ..logger.manager import LogManager, WorkflowLog
|
|
11
11
|
from ..builder.model import OptimizationModel
|
|
12
12
|
from ..solver.engine import SolverEngine
|
|
13
|
-
from ..solver.
|
|
13
|
+
from ..solver.params import SolverParams
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
InputBaseModel = TypeVar("InputBaseModel", bound=BaseModel)
|
|
@@ -105,7 +105,7 @@ class OptimizationWorkflow(Generic[InputBaseModel, OutputBaseModel], ABC, metacl
|
|
|
105
105
|
self.runtime_seconds = time.time() - start_time
|
|
106
106
|
self.end_timestamp = datetime.now(timezone.utc).isoformat()
|
|
107
107
|
|
|
108
|
-
if self._logger is not None and self._logger.
|
|
108
|
+
if self._logger is not None and self._logger.enable_monitor_runtime:
|
|
109
109
|
self._logger.put_workflow_log(workflow_log=self._workflow_log)
|
|
110
110
|
|
|
111
111
|
if self.runtime_message == "success" and result is not None:
|
|
@@ -118,14 +118,15 @@ class OptimizationWorkflow(Generic[InputBaseModel, OutputBaseModel], ABC, metacl
|
|
|
118
118
|
pass
|
|
119
119
|
|
|
120
120
|
@final
|
|
121
|
-
def execute_model(self, model_name: str, params: Any, solver_engine: Optional[SolverEngine] = None) -> Any:
|
|
121
|
+
def execute_model(self, model_name: str, params: Any, solver_engine: Optional[SolverEngine] = None, solver_params: Optional[SolverParams] = None) -> Any:
|
|
122
122
|
if model_name not in self.models_registry:
|
|
123
123
|
raise ValueError(f"Model '{model_name}' not found in models_registry.")
|
|
124
124
|
|
|
125
125
|
model_cls = self.models_registry[model_name]
|
|
126
126
|
model_instance = model_cls(
|
|
127
127
|
params=params,
|
|
128
|
-
solver_engine=solver_engine
|
|
128
|
+
solver_engine=solver_engine,
|
|
129
|
+
solver_params=solver_params
|
|
129
130
|
)
|
|
130
131
|
|
|
131
132
|
output = model_instance.execute(logger=self._logger)
|
|
@@ -140,7 +141,6 @@ class OptimizationWorkflow(Generic[InputBaseModel, OutputBaseModel], ABC, metacl
|
|
|
140
141
|
workflow_name=self.name,
|
|
141
142
|
model_ids_execution=self.model_ids_execution,
|
|
142
143
|
payload=self.payload.model_dump(),
|
|
143
|
-
solver_parameter=SolverConfig().SOLVER_PARAMETER,
|
|
144
144
|
message=self.runtime_message,
|
|
145
145
|
start_timestamp=self.start_timestamp,
|
|
146
146
|
end_timestamp=self.end_timestamp,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from .manager import LogManager, ModelLog, WorkflowLog
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class JSONLogManager(LogManager):
|
|
6
|
+
directory_path: str
|
|
7
|
+
|
|
8
|
+
def __init__(
|
|
9
|
+
self, directory_path: str,
|
|
10
|
+
enable_monitor_optimality: bool = True,
|
|
11
|
+
enable_monitor_runtime: bool = True,
|
|
12
|
+
enable_monitor_resource: bool = False
|
|
13
|
+
):
|
|
14
|
+
self.directory_path = directory_path
|
|
15
|
+
self.enable_monitor_optimality = enable_monitor_optimality
|
|
16
|
+
self.enable_monitor_resource = enable_monitor_resource
|
|
17
|
+
self.enable_monitor_runtime = enable_monitor_runtime
|
|
18
|
+
|
|
19
|
+
self.init_directory()
|
|
20
|
+
|
|
21
|
+
def init_directory(self):
|
|
22
|
+
if self.enable_monitor_optimality:
|
|
23
|
+
Path(
|
|
24
|
+
f"{self.directory_path}/{self._dir_model_execution_log}"
|
|
25
|
+
).mkdir(parents=True, exist_ok=True)
|
|
26
|
+
|
|
27
|
+
if self.enable_monitor_runtime:
|
|
28
|
+
Path(
|
|
29
|
+
f"{self.directory_path}/{self._dir_workflow_execution_log}"
|
|
30
|
+
).mkdir(parents=True, exist_ok=True)
|
|
31
|
+
|
|
32
|
+
def put_model_log(self, model_log: ModelLog):
|
|
33
|
+
file_path = Path(f"{self.directory_path}/{self._dir_model_execution_log}/{model_log.model_id}.json")
|
|
34
|
+
|
|
35
|
+
file_path.write_text(
|
|
36
|
+
model_log.model_dump_json(indent=2),
|
|
37
|
+
encoding="utf-8"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def put_workflow_log(self, workflow_log: WorkflowLog):
|
|
41
|
+
file_path = Path(f"{self.directory_path}/{self._dir_workflow_execution_log}/{workflow_log.workflow_id}.json")
|
|
42
|
+
|
|
43
|
+
file_path.write_text(
|
|
44
|
+
workflow_log.model_dump_json(indent=2),
|
|
45
|
+
encoding="utf-8"
|
|
46
|
+
)
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from abc import abstractmethod, ABC
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
import psutil
|
|
9
|
+
import threading
|
|
10
|
+
from statistics import mean
|
|
11
|
+
|
|
12
|
+
from ..solver.engine import SolverEngine
|
|
13
|
+
from ..solver.status import SolverStatus
|
|
14
|
+
from ..solver.params import SolverParams
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ResourceStats(BaseModel):
|
|
18
|
+
peak_cpu_percent: float
|
|
19
|
+
avg_cpu_percent: float
|
|
20
|
+
peak_memory_mb: float
|
|
21
|
+
avg_memory_mb: float
|
|
22
|
+
max_num_threads: int
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ResourceSnapshot(BaseModel):
|
|
26
|
+
timestamp: float
|
|
27
|
+
cpu_percent: float
|
|
28
|
+
memory_mb: float
|
|
29
|
+
num_threads: int
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ModelLog(BaseModel):
|
|
33
|
+
solver_engine: SolverEngine
|
|
34
|
+
solver_params: SolverParams
|
|
35
|
+
model_id: str
|
|
36
|
+
model_name: str
|
|
37
|
+
status: SolverStatus
|
|
38
|
+
problem_size_vars: Optional[int]
|
|
39
|
+
problem_size_cons: Optional[int]
|
|
40
|
+
optimality_gap: Optional[float]
|
|
41
|
+
objective_value: Optional[float]
|
|
42
|
+
message: Optional[str]
|
|
43
|
+
runtime_sec: float
|
|
44
|
+
resource_stats: Optional[ResourceStats] = None
|
|
45
|
+
created_timestamp: str
|
|
46
|
+
|
|
47
|
+
def to_sql_log(self) -> dict:
|
|
48
|
+
return {
|
|
49
|
+
"id": None,
|
|
50
|
+
"solver_engine": self.solver_engine.value,
|
|
51
|
+
"solver_params": self.solver_params.model_dump_json(),
|
|
52
|
+
"model_id": self.model_id,
|
|
53
|
+
"model_name": self.model_name,
|
|
54
|
+
"problem_size_vars": self.problem_size_vars,
|
|
55
|
+
"problem_size_cons": self.problem_size_cons,
|
|
56
|
+
"optimality_gap": self.optimality_gap,
|
|
57
|
+
"objective_value": self.objective_value,
|
|
58
|
+
"status": self.status.value,
|
|
59
|
+
"message": self.message,
|
|
60
|
+
"runtime_sec": self.runtime_sec,
|
|
61
|
+
"resource_stats": self.resource_stats.model_dump_json(),
|
|
62
|
+
"created_timestamp": self.created_timestamp
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class WorkflowLog(BaseModel):
|
|
67
|
+
workflow_id: str
|
|
68
|
+
workflow_name: str
|
|
69
|
+
model_ids_execution: dict
|
|
70
|
+
payload: dict
|
|
71
|
+
message: Optional[str]
|
|
72
|
+
start_timestamp: str
|
|
73
|
+
end_timestamp: str
|
|
74
|
+
runtime_sec: float
|
|
75
|
+
created_timestamp: str
|
|
76
|
+
|
|
77
|
+
def to_sql_log(self) -> dict:
|
|
78
|
+
model_ids = json.dumps(self.model_ids_execution)
|
|
79
|
+
payload = json.dumps(self.payload)
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
"id": None,
|
|
83
|
+
"workflow_id": self.workflow_id,
|
|
84
|
+
"workflow_name": self.workflow_name,
|
|
85
|
+
"model_ids": model_ids,
|
|
86
|
+
"payload": payload,
|
|
87
|
+
"message": self.message,
|
|
88
|
+
"start_timestamp": self.start_timestamp,
|
|
89
|
+
"end_timestamp": self.end_timestamp,
|
|
90
|
+
"runtime_sec": self.runtime_sec,
|
|
91
|
+
"created_timestamp": self.created_timestamp
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class LogManager(ABC):
|
|
96
|
+
enable_monitor_optimality: bool = True
|
|
97
|
+
enable_monitor_runtime: bool = True
|
|
98
|
+
enable_monitor_resource: bool = False
|
|
99
|
+
monitor_resource_interval_seconds: int = 5
|
|
100
|
+
|
|
101
|
+
_dir_model_execution_log: str = "log_execution_model"
|
|
102
|
+
_dir_workflow_execution_log: str = "log_execution_workflow"
|
|
103
|
+
|
|
104
|
+
def __init__(self):
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
@abstractmethod
|
|
108
|
+
def put_model_log(self, model_log: ModelLog):
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
@abstractmethod
|
|
112
|
+
def put_workflow_log(self, workflow_log: WorkflowLog):
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class BaseResourceMonitor(ABC):
|
|
117
|
+
stats: Optional[ResourceStats] = None
|
|
118
|
+
|
|
119
|
+
@abstractmethod
|
|
120
|
+
def __enter__(self):
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
@abstractmethod
|
|
124
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class NullResourceMonitor(BaseResourceMonitor):
|
|
129
|
+
stats = None
|
|
130
|
+
|
|
131
|
+
def __enter__(self):
|
|
132
|
+
return self
|
|
133
|
+
|
|
134
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class ResourceMonitor(BaseResourceMonitor):
|
|
139
|
+
|
|
140
|
+
def __init__(self, interval_seconds: int = 1):
|
|
141
|
+
self._running = False
|
|
142
|
+
self.interval_seconds = interval_seconds
|
|
143
|
+
self.stats: Optional[ResourceStats] = None
|
|
144
|
+
self._thread: Optional[threading.Thread] = None
|
|
145
|
+
self._snapshots: list[ResourceSnapshot] = []
|
|
146
|
+
self._stop_event = threading.Event()
|
|
147
|
+
|
|
148
|
+
def __enter__(self):
|
|
149
|
+
self.start()
|
|
150
|
+
return self
|
|
151
|
+
|
|
152
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
153
|
+
self.stop()
|
|
154
|
+
|
|
155
|
+
def start(self):
|
|
156
|
+
if self._running:
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
self._running = True
|
|
160
|
+
self._stop_event.clear()
|
|
161
|
+
|
|
162
|
+
proc = psutil.Process(os.getpid())
|
|
163
|
+
proc.cpu_percent(interval=None)
|
|
164
|
+
self._take_sample(proc)
|
|
165
|
+
|
|
166
|
+
self._thread = threading.Thread(
|
|
167
|
+
target=self._sampling_loop,
|
|
168
|
+
daemon=True
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
self._thread.start()
|
|
172
|
+
|
|
173
|
+
def stop(self):
|
|
174
|
+
if not self._running:
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
self._running = False
|
|
178
|
+
self._stop_event.set()
|
|
179
|
+
|
|
180
|
+
if self._thread is not None:
|
|
181
|
+
self._thread.join()
|
|
182
|
+
|
|
183
|
+
self.stats = self._aggregate()
|
|
184
|
+
|
|
185
|
+
def _take_sample(self, proc: psutil.Process):
|
|
186
|
+
try:
|
|
187
|
+
self._snapshots.append(
|
|
188
|
+
ResourceSnapshot(
|
|
189
|
+
timestamp=time.time(),
|
|
190
|
+
cpu_percent=proc.cpu_percent(interval=None),
|
|
191
|
+
memory_mb=proc.memory_info().rss / 1024 / 1024,
|
|
192
|
+
num_threads=proc.num_threads()
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
except (
|
|
196
|
+
psutil.NoSuchProcess,
|
|
197
|
+
psutil.AccessDenied,
|
|
198
|
+
):
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
def _sampling_loop(self):
|
|
202
|
+
proc = psutil.Process(os.getpid())
|
|
203
|
+
|
|
204
|
+
while not self._stop_event.wait(self.interval_seconds):
|
|
205
|
+
self._take_sample(proc)
|
|
206
|
+
|
|
207
|
+
def _aggregate(self) -> Optional[ResourceStats]:
|
|
208
|
+
if not self._snapshots:
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
cpu_values = [x.cpu_percent for x in self._snapshots]
|
|
212
|
+
mem_values = [x.memory_mb for x in self._snapshots]
|
|
213
|
+
thread_values = [x.num_threads for x in self._snapshots]
|
|
214
|
+
|
|
215
|
+
return ResourceStats(
|
|
216
|
+
peak_cpu_percent=max(cpu_values),
|
|
217
|
+
avg_cpu_percent=mean(cpu_values),
|
|
218
|
+
peak_memory_mb=max(mem_values),
|
|
219
|
+
avg_memory_mb=mean(mem_values),
|
|
220
|
+
max_num_threads=max(thread_values)
|
|
221
|
+
)
|
|
@@ -6,21 +6,27 @@ import pandas as pd
|
|
|
6
6
|
class SQLiteLogManager(LogManager):
|
|
7
7
|
db_path: str
|
|
8
8
|
|
|
9
|
-
def __init__(
|
|
9
|
+
def __init__(
|
|
10
|
+
self, db_path: str,
|
|
11
|
+
enable_monitor_optimality: bool = True,
|
|
12
|
+
enable_monitor_runtime: bool = True,
|
|
13
|
+
enable_monitor_resource: bool = False
|
|
14
|
+
):
|
|
10
15
|
self.db_path = db_path
|
|
11
|
-
self.
|
|
12
|
-
self.
|
|
13
|
-
self.
|
|
16
|
+
self.enable_monitor_optimality = enable_monitor_optimality
|
|
17
|
+
self.enable_monitor_resource = enable_monitor_resource
|
|
18
|
+
self.enable_monitor_runtime = enable_monitor_runtime
|
|
14
19
|
|
|
15
20
|
self.init_db()
|
|
16
21
|
|
|
17
22
|
def init_db(self):
|
|
18
23
|
with sqlite3.connect(self.db_path) as conn:
|
|
19
|
-
if self.
|
|
24
|
+
if self.enable_monitor_optimality:
|
|
20
25
|
conn.execute(f"""
|
|
21
26
|
CREATE TABLE IF NOT EXISTS {self._dir_model_execution_log} (
|
|
22
27
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
23
28
|
solver_engine TEXT, -- gurobi / ortools / routing
|
|
29
|
+
solver_params TEXT, -- store JSON solver parameter as string
|
|
24
30
|
model_id TEXT,
|
|
25
31
|
model_name TEXT,
|
|
26
32
|
problem_size_vars INTEGER, -- number of decision variables
|
|
@@ -30,11 +36,12 @@ class SQLiteLogManager(LogManager):
|
|
|
30
36
|
status TEXT, -- START / DONE / ERROR
|
|
31
37
|
message TEXT, -- optional note
|
|
32
38
|
runtime_sec REAL, -- optional
|
|
39
|
+
resource_stats TEXT, -- store JSON resource stats as string
|
|
33
40
|
created_timestamp TEXT
|
|
34
41
|
);
|
|
35
42
|
""")
|
|
36
43
|
|
|
37
|
-
if self.
|
|
44
|
+
if self.enable_monitor_runtime:
|
|
38
45
|
conn.execute(f"""
|
|
39
46
|
CREATE TABLE IF NOT EXISTS {self._dir_workflow_execution_log} (
|
|
40
47
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -42,7 +49,6 @@ class SQLiteLogManager(LogManager):
|
|
|
42
49
|
workflow_name TEXT,
|
|
43
50
|
model_ids TEXT, -- store list of str as JSON string
|
|
44
51
|
payload TEXT, -- store JSON payload as string
|
|
45
|
-
solver_parameter TEXT, -- store JSON solver parameter as string
|
|
46
52
|
message TEXT, -- store error message
|
|
47
53
|
start_timestamp TEXT, -- store start timestamp
|
|
48
54
|
end_timestamp TEXT, -- store end timestamp
|
|
@@ -51,21 +57,6 @@ class SQLiteLogManager(LogManager):
|
|
|
51
57
|
);
|
|
52
58
|
""")
|
|
53
59
|
|
|
54
|
-
if self.is_monitor_resource:
|
|
55
|
-
conn.execute(f"""
|
|
56
|
-
CREATE TABLE IF NOT EXISTS {self._dir_resource_occupation_log} (
|
|
57
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
58
|
-
machine_id TEXT,
|
|
59
|
-
model_id TEXT,
|
|
60
|
-
cpu_percent REAL,
|
|
61
|
-
num_cores INTEGER,
|
|
62
|
-
limit_thread INTEGER,
|
|
63
|
-
memory_mb REAL,
|
|
64
|
-
limit_memory_mb REAL,
|
|
65
|
-
timestamp TEXT
|
|
66
|
-
);
|
|
67
|
-
""")
|
|
68
|
-
|
|
69
60
|
conn.commit()
|
|
70
61
|
|
|
71
62
|
def put_model_log(self, model_log: ModelLog):
|
|
@@ -76,7 +67,4 @@ class SQLiteLogManager(LogManager):
|
|
|
76
67
|
def put_workflow_log(self, workflow_log: WorkflowLog):
|
|
77
68
|
with sqlite3.connect(self.db_path) as conn:
|
|
78
69
|
data = pd.DataFrame([workflow_log.to_sql_log()])
|
|
79
|
-
data.to_sql(self._dir_workflow_execution_log, conn, if_exists="append", index=False)
|
|
80
|
-
|
|
81
|
-
def put_resource_log(self, resource_log):
|
|
82
|
-
return super().put_resource_log(resource_log)
|
|
70
|
+
data.to_sql(self._dir_workflow_execution_log, conn, if_exists="append", index=False)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SolverParams(BaseModel):
|
|
6
|
+
TRUE_THRESHOLD: float = 0.5
|
|
7
|
+
APPLY_HEURISTICS: bool = True
|
|
8
|
+
MODEL_SOLVER_VERBOSE: bool = False
|
|
9
|
+
|
|
10
|
+
LIMIT_TIME_MINUTES_HEURISTICS: float = 3
|
|
11
|
+
LIMIT_TIME_MINUTES_DETERMINISTIC: float = 3
|
|
12
|
+
LIMIT_OPTIMALITY_GAP_DETERMINISTIC: float = 0.35
|
|
13
|
+
LIMIT_OPTIMALITY_GAP_HEURISTICS: float = 0.75
|
|
14
|
+
LIMIT_MEMORY_MB: int = 1024*8
|
|
15
|
+
LIMIT_MULTI_THREAD: int = 6
|
|
@@ -11,12 +11,12 @@ src/stitchlab_optimization/builder/__init__.py
|
|
|
11
11
|
src/stitchlab_optimization/builder/model.py
|
|
12
12
|
src/stitchlab_optimization/builder/workflow.py
|
|
13
13
|
src/stitchlab_optimization/logger/__init__.py
|
|
14
|
-
src/stitchlab_optimization/logger/
|
|
14
|
+
src/stitchlab_optimization/logger/json_logger.py
|
|
15
15
|
src/stitchlab_optimization/logger/manager.py
|
|
16
16
|
src/stitchlab_optimization/logger/sqlite_logger.py
|
|
17
17
|
src/stitchlab_optimization/solver/__init__.py
|
|
18
|
-
src/stitchlab_optimization/solver/config.py
|
|
19
18
|
src/stitchlab_optimization/solver/engine.py
|
|
19
|
+
src/stitchlab_optimization/solver/params.py
|
|
20
20
|
src/stitchlab_optimization/solver/status.py
|
|
21
21
|
src/stitchlab_optimization/tools/utils.py
|
|
22
22
|
src/stitchlab_optimization/tools/query/snowflake.py
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
class ModelLog(BaseModel):
|
|
2
|
-
solver_engine: SolverEngine
|
|
3
|
-
model_id: str
|
|
4
|
-
exec_id: str
|
|
5
|
-
model_name: str
|
|
6
|
-
status: SolverStatus
|
|
7
|
-
problem_size_vars: Optional[int]
|
|
8
|
-
problem_size_cons: Optional[int]
|
|
9
|
-
optimality_gap: Optional[float]
|
|
10
|
-
objective_value: Optional[float]
|
|
11
|
-
message: Optional[str]
|
|
12
|
-
runtime_sec: float
|
|
13
|
-
created_at: str
|
|
14
|
-
|
|
15
|
-
@classmethod
|
|
16
|
-
def from_model(cls, model: OptimizationModel, solver_engine: SolverEngine):
|
|
17
|
-
|
|
18
|
-
builder = model.builders[solver_engine]
|
|
19
|
-
|
|
20
|
-
if solver_engine == SolverEngine.GUROBI:
|
|
21
|
-
# Gurobi Python API
|
|
22
|
-
problem_size_vars = builder.model.NumVars
|
|
23
|
-
problem_size_cons = builder.model.NumConstrs
|
|
24
|
-
|
|
25
|
-
if builder.model.NumObj <= 1:
|
|
26
|
-
optimality_gap = builder.model.MIPGap
|
|
27
|
-
objective_value = builder.model.ObjVal
|
|
28
|
-
|
|
29
|
-
else:
|
|
30
|
-
objective_value = builder.model.getAttr("ObjNVal")
|
|
31
|
-
optimality_gap = builder.model.getAttr("ObjNRelTol")
|
|
32
|
-
|
|
33
|
-
elif solver_engine == SolverEngine.ORTOOLS_SCIP:
|
|
34
|
-
# OR-Tools CP-SAT solver (pywraplp.Solver)
|
|
35
|
-
problem_size_vars = builder.model.NumVariables()
|
|
36
|
-
problem_size_cons = builder.model.NumConstraints()
|
|
37
|
-
|
|
38
|
-
try:
|
|
39
|
-
optimality_gap = builder.model.MipGap()
|
|
40
|
-
except AttributeError:
|
|
41
|
-
optimality_gap = None
|
|
42
|
-
|
|
43
|
-
objective_value = builder.model.Objective().Value()
|
|
44
|
-
|
|
45
|
-
elif solver_engine == SolverEngine.ORTOOLS_ROUTING:
|
|
46
|
-
# OR-Tools RoutingModel
|
|
47
|
-
count_nodes = builder.model.Size()
|
|
48
|
-
count_vehicles = builder.model.vehicles()
|
|
49
|
-
problem_size_vars = count_nodes * count_nodes * count_vehicles
|
|
50
|
-
|
|
51
|
-
# RoutingModel does not expose number of constraints directly
|
|
52
|
-
problem_size_cons = None
|
|
53
|
-
objective_value = builder.solution.ObjectiveValue()
|
|
54
|
-
optimality_gap = None
|
|
55
|
-
|
|
56
|
-
elif solver_engine == SolverEngine.ORTOOLS_CPSAT:
|
|
57
|
-
problem_size_vars = len(builder.model.Proto().variables)
|
|
58
|
-
problem_size_cons = len(builder.model.Proto().constraints)
|
|
59
|
-
|
|
60
|
-
# Objective value (only available if model solved)
|
|
61
|
-
try:
|
|
62
|
-
objective_value = builder.solution.ObjectiveValue()
|
|
63
|
-
except Exception:
|
|
64
|
-
objective_value = None
|
|
65
|
-
|
|
66
|
-
optimality_gap = None
|
|
67
|
-
|
|
68
|
-
elif solver_engine == SolverEngine.PYSCIPOPT:
|
|
69
|
-
problem_size_vars = builder.model.getNVars()
|
|
70
|
-
problem_size_cons = builder.model.getNConss()
|
|
71
|
-
optimality_gap = builder.model.getGap()
|
|
72
|
-
|
|
73
|
-
try:
|
|
74
|
-
objective_value = builder.model.getObjVal()
|
|
75
|
-
except Exception:
|
|
76
|
-
objective_value = None
|
|
77
|
-
|
|
78
|
-
elif solver_engine == SolverEngine.SKLEARN:
|
|
79
|
-
# scikit-learn is not an optimization solver, so these are not applicable
|
|
80
|
-
problem_size_vars = None
|
|
81
|
-
problem_size_cons = None
|
|
82
|
-
optimality_gap = None
|
|
83
|
-
objective_value = None
|
|
84
|
-
|
|
85
|
-
else:
|
|
86
|
-
raise ValueError(f"Solver engine {solver_engine} not supported")
|
|
87
|
-
|
|
88
|
-
return ModelLog(
|
|
89
|
-
solver_engine=solver_engine,
|
|
90
|
-
model_id=model.id,
|
|
91
|
-
exec_id=builder.id,
|
|
92
|
-
model_name=model.name,
|
|
93
|
-
status=builder.solver_status,
|
|
94
|
-
problem_size_vars=problem_size_vars,
|
|
95
|
-
problem_size_cons=problem_size_cons,
|
|
96
|
-
optimality_gap=optimality_gap,
|
|
97
|
-
objective_value=objective_value,
|
|
98
|
-
message=builder.runtime_message,
|
|
99
|
-
runtime_sec=builder.runtime_seconds,
|
|
100
|
-
created_at=datetime.now(timezone.utc).isoformat()
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
def write_to_db(self):
|
|
104
|
-
log = {
|
|
105
|
-
"id": None,
|
|
106
|
-
"solver_engine": self.solver_engine.value,
|
|
107
|
-
"model_id": self.model_id,
|
|
108
|
-
"exec_id": self.exec_id,
|
|
109
|
-
"model_name": self.model_name,
|
|
110
|
-
"problem_size_vars": self.problem_size_vars,
|
|
111
|
-
"problem_size_cons": self.problem_size_cons,
|
|
112
|
-
"optimality_gap": self.optimality_gap,
|
|
113
|
-
"objective_value": self.objective_value,
|
|
114
|
-
"status": self.status.value,
|
|
115
|
-
"message": self.message,
|
|
116
|
-
"runtime_sec": self.runtime_sec,
|
|
117
|
-
"created_at": self.created_at
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
insert_to_sqlite(table_name="execution_log", data=log)
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
from abc import abstractmethod, ABC
|
|
2
|
-
from pydantic import BaseModel
|
|
3
|
-
from typing import Optional
|
|
4
|
-
import json
|
|
5
|
-
|
|
6
|
-
from ..solver.engine import SolverEngine
|
|
7
|
-
from ..solver.status import SolverStatus
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class ModelLog(BaseModel):
|
|
11
|
-
solver_engine: SolverEngine
|
|
12
|
-
model_id: str
|
|
13
|
-
model_name: str
|
|
14
|
-
status: SolverStatus
|
|
15
|
-
problem_size_vars: Optional[int]
|
|
16
|
-
problem_size_cons: Optional[int]
|
|
17
|
-
optimality_gap: Optional[float]
|
|
18
|
-
objective_value: Optional[float]
|
|
19
|
-
message: Optional[str]
|
|
20
|
-
runtime_sec: float
|
|
21
|
-
created_timestamp: str
|
|
22
|
-
|
|
23
|
-
def to_sql_log(self) -> dict:
|
|
24
|
-
return {
|
|
25
|
-
"id": None,
|
|
26
|
-
"solver_engine": self.solver_engine.value,
|
|
27
|
-
"model_id": self.model_id,
|
|
28
|
-
"model_name": self.model_name,
|
|
29
|
-
"problem_size_vars": self.problem_size_vars,
|
|
30
|
-
"problem_size_cons": self.problem_size_cons,
|
|
31
|
-
"optimality_gap": self.optimality_gap,
|
|
32
|
-
"objective_value": self.objective_value,
|
|
33
|
-
"status": self.status.value,
|
|
34
|
-
"message": self.message,
|
|
35
|
-
"runtime_sec": self.runtime_sec,
|
|
36
|
-
"created_timestamp": self.created_timestamp
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class WorkflowLog(BaseModel):
|
|
41
|
-
workflow_id: str
|
|
42
|
-
workflow_name: str
|
|
43
|
-
model_ids_execution: dict
|
|
44
|
-
payload: dict
|
|
45
|
-
solver_parameter: dict
|
|
46
|
-
message: Optional[str]
|
|
47
|
-
start_timestamp: str
|
|
48
|
-
end_timestamp: str
|
|
49
|
-
runtime_sec: float
|
|
50
|
-
created_timestamp: str
|
|
51
|
-
|
|
52
|
-
def to_sql_log(self) -> dict:
|
|
53
|
-
model_ids = json.dumps(self.model_ids_execution)
|
|
54
|
-
payload = json.dumps(self.payload)
|
|
55
|
-
solver_parameter = json.dumps(self.solver_parameter)
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
"id": None,
|
|
59
|
-
"workflow_id": self.workflow_id,
|
|
60
|
-
"workflow_name": self.workflow_name,
|
|
61
|
-
"model_ids": model_ids,
|
|
62
|
-
"payload": payload,
|
|
63
|
-
"solver_parameter": solver_parameter,
|
|
64
|
-
"message": self.message,
|
|
65
|
-
"start_timestamp": self.start_timestamp,
|
|
66
|
-
"end_timestamp": self.end_timestamp,
|
|
67
|
-
"runtime_sec": self.runtime_sec,
|
|
68
|
-
"created_timestamp": self.created_timestamp
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
class LogManager(ABC):
|
|
73
|
-
is_monitor_optimality: bool = True
|
|
74
|
-
is_monitor_runtime: bool = True
|
|
75
|
-
is_monitor_resource: bool = False
|
|
76
|
-
|
|
77
|
-
_dir_model_execution_log: str = "log_execution_model"
|
|
78
|
-
_dir_workflow_execution_log: str = "log_execution_workflow"
|
|
79
|
-
_dir_resource_occupation_log: str = "log_resource_occupation"
|
|
80
|
-
|
|
81
|
-
def __init__(self):
|
|
82
|
-
pass
|
|
83
|
-
|
|
84
|
-
@abstractmethod
|
|
85
|
-
def put_model_log(self, model_log: ModelLog):
|
|
86
|
-
pass
|
|
87
|
-
|
|
88
|
-
@abstractmethod
|
|
89
|
-
def put_workflow_log(self, workflow_log: WorkflowLog):
|
|
90
|
-
pass
|
|
91
|
-
|
|
92
|
-
@abstractmethod
|
|
93
|
-
def put_resource_log(self, resource_log: dict):
|
|
94
|
-
pass
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from dotenv import load_dotenv
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class SolverConfig:
|
|
6
|
-
_instance = None
|
|
7
|
-
|
|
8
|
-
def __new__(cls):
|
|
9
|
-
if cls._instance is None:
|
|
10
|
-
if cls._instance is None:
|
|
11
|
-
load_dotenv() # load .env
|
|
12
|
-
cls._instance = super().__new__(cls)
|
|
13
|
-
cls._instance._load_env()
|
|
14
|
-
|
|
15
|
-
return cls._instance
|
|
16
|
-
|
|
17
|
-
def _load_env(self):
|
|
18
|
-
self.TRUE_THRESHOLD = float(os.getenv("TRUE_THRESHOLD", 0.5))
|
|
19
|
-
self.APPLY_HEURISTICS = os.getenv("APPLY_HEURISTICS", "True").lower() in ("true", "1", "yes")
|
|
20
|
-
self.MODEL_SOLVER_VERBOSE = os.getenv("MODEL_SOLVER_VERBOSE", "False").lower() in ("true", "1", "yes")
|
|
21
|
-
|
|
22
|
-
self.LIMIT_TIME_MINUTES_HEURISTICS = float(os.getenv("LIMIT_TIME_MINUTES_HEURISTICS", 3))
|
|
23
|
-
self.LIMIT_TIME_MINUTES_DETERMINISTIC = float(os.getenv("LIMIT_TIME_MINUTES_DETERMINISTIC", 3))
|
|
24
|
-
self.LIMIT_OPTIMALITY_GAP_DETERMINISTIC = float(os.getenv("LIMIT_OPTIMALITY_GAP_DETERMINISTIC", 0.35))
|
|
25
|
-
self.LIMIT_OPTIMALITY_GAP_HEURISTICS = float(os.getenv("LIMIT_OPTIMALITY_GAP_HEURISTICS", 0.75))
|
|
26
|
-
self.LIMIT_MEMORY_MB = int(os.getenv("LIMIT_MEMORY_MB", 1024*8))
|
|
27
|
-
self.LIMIT_MULTI_THREAD = int(os.getenv("LIMIT_MULTI_THREAD", 6))
|
|
28
|
-
|
|
29
|
-
self.SOLVER_PARAMETER = {
|
|
30
|
-
"true_threshold": self.TRUE_THRESHOLD,
|
|
31
|
-
"limit_minutes_heuristics": self.LIMIT_TIME_MINUTES_HEURISTICS,
|
|
32
|
-
"limit_minutes_deterministic": self.LIMIT_TIME_MINUTES_DETERMINISTIC,
|
|
33
|
-
"limit_optimality_gap_deterministic": self.LIMIT_OPTIMALITY_GAP_DETERMINISTIC,
|
|
34
|
-
"limit_optimality_gap_heuristics": self.LIMIT_OPTIMALITY_GAP_HEURISTICS,
|
|
35
|
-
"limit_memory_mb": self.LIMIT_MEMORY_MB,
|
|
36
|
-
"limit_multi_thread": self.LIMIT_MULTI_THREAD,
|
|
37
|
-
"apply_heuristics": self.APPLY_HEURISTICS,
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
self.MONITOR_EXECUTION = os.getenv("MONITOR_EXECUTION", "False").lower() in ("true", "1", "yes")
|
|
41
|
-
self.MONITOR_RESOURCE = os.getenv("MONITOR_RESOURCE", "False").lower() in ("true", "1", "yes")
|
|
42
|
-
self.MONITOR_RESOURCE_INTERVAL_SECONDS = int(os.getenv("MONITOR_RESOURCE_INTERVAL_SECONDS", 60))
|
|
43
|
-
|
|
44
|
-
self.SQLITE_DB_PATH = os.getenv("SQLITE_DB_PATH", "log.sqlite")
|
|
45
|
-
self.EXPORT_DIRECTORY = os.getenv("EXPORT_DIRECTORY", "export")
|
|
46
|
-
|
|
47
|
-
self._load_gurobi_credentials()
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _load_gurobi_credentials(self):
|
|
51
|
-
self.GUROBI_ENV = None
|
|
52
|
-
|
|
53
|
-
GUROBI_KEYS = {
|
|
54
|
-
"WLSAccessID": os.getenv("WLSACCESSID", None),
|
|
55
|
-
"WLSSecret": os.getenv("WLSSECRET", None),
|
|
56
|
-
"LicenseID": int(os.getenv("LICENSEID", 0)),
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if GUROBI_KEYS["WLSAccessID"] is not None and GUROBI_KEYS["WLSSecret"] is not None:
|
|
60
|
-
import gurobipy as gp
|
|
61
|
-
|
|
62
|
-
self.GUROBI_ENV = gp.Env(params=GUROBI_KEYS)
|
|
63
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|