stitchlab-optimization 0.0.1__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.
Files changed (25) hide show
  1. stitchlab_optimization-0.0.1/LICENSE +22 -0
  2. stitchlab_optimization-0.0.1/PKG-INFO +33 -0
  3. stitchlab_optimization-0.0.1/README.md +0 -0
  4. stitchlab_optimization-0.0.1/pyproject.toml +59 -0
  5. stitchlab_optimization-0.0.1/setup.cfg +4 -0
  6. stitchlab_optimization-0.0.1/src/stitchlab_optimization/__init__.py +17 -0
  7. stitchlab_optimization-0.0.1/src/stitchlab_optimization/builder/__init__.py +0 -0
  8. stitchlab_optimization-0.0.1/src/stitchlab_optimization/builder/model.py +437 -0
  9. stitchlab_optimization-0.0.1/src/stitchlab_optimization/builder/workflow.py +149 -0
  10. stitchlab_optimization-0.0.1/src/stitchlab_optimization/logger/__init__.py +0 -0
  11. stitchlab_optimization-0.0.1/src/stitchlab_optimization/logger/file_logger.py +120 -0
  12. stitchlab_optimization-0.0.1/src/stitchlab_optimization/logger/manager.py +94 -0
  13. stitchlab_optimization-0.0.1/src/stitchlab_optimization/logger/sqlite_logger.py +82 -0
  14. stitchlab_optimization-0.0.1/src/stitchlab_optimization/solver/__init__.py +0 -0
  15. stitchlab_optimization-0.0.1/src/stitchlab_optimization/solver/config.py +63 -0
  16. stitchlab_optimization-0.0.1/src/stitchlab_optimization/solver/engine.py +11 -0
  17. stitchlab_optimization-0.0.1/src/stitchlab_optimization/solver/status.py +127 -0
  18. stitchlab_optimization-0.0.1/src/stitchlab_optimization/tools/query/snowflake.py +200 -0
  19. stitchlab_optimization-0.0.1/src/stitchlab_optimization/tools/query/sqlite.py +0 -0
  20. stitchlab_optimization-0.0.1/src/stitchlab_optimization/tools/utils.py +41 -0
  21. stitchlab_optimization-0.0.1/src/stitchlab_optimization.egg-info/PKG-INFO +33 -0
  22. stitchlab_optimization-0.0.1/src/stitchlab_optimization.egg-info/SOURCES.txt +23 -0
  23. stitchlab_optimization-0.0.1/src/stitchlab_optimization.egg-info/dependency_links.txt +1 -0
  24. stitchlab_optimization-0.0.1/src/stitchlab_optimization.egg-info/requires.txt +13 -0
  25. stitchlab_optimization-0.0.1/src/stitchlab_optimization.egg-info/top_level.txt +1 -0
@@ -0,0 +1,22 @@
1
+
2
+ Copyright (c) 2026 StitchLab
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
21
+
22
+
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: stitchlab-optimization
3
+ Version: 0.0.1
4
+ Summary: StitchLab Optimization application
5
+ Author-email: StitchLab Team <evazuliya97@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/eva-zuliya/stitchlab-optimization
8
+ Project-URL: Repository, https://github.com/eva-zuliya/stitchlab-optimization
9
+ Project-URL: Issues, https://github.com/eva-zuliya/stitchlab-optimization/issues
10
+ Keywords: optimization,ai
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Requires-Python: <3.14,>=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: pydantic<3.0,>=2.0
22
+ Requires-Dist: python-dotenv<2.0,>=1.0
23
+ Requires-Dist: pyscipopt<7.0,>=6.0
24
+ Requires-Dist: ortools<10.0,>=9.15
25
+ Requires-Dist: gurobipy<14.0,>=13.0
26
+ Requires-Dist: pandas<4.0,>=2.0
27
+ Requires-Dist: psutil<8.0,>=5.9
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
30
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
31
+ Requires-Dist: black>=23.0.0; extra == "dev"
32
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
33
+ Dynamic: license-file
File without changes
@@ -0,0 +1,59 @@
1
+ [project]
2
+ name = "stitchlab-optimization"
3
+ version = "0.0.1"
4
+ description = "StitchLab Optimization application"
5
+ readme = "README.md"
6
+ license = {text = "MIT"}
7
+ authors = [{name = "StitchLab Team", email = "evazuliya97@gmail.com"}]
8
+ requires-python = ">=3.11,<3.14"
9
+ keywords = ["optimization", "ai"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ ]
19
+
20
+ dependencies = [
21
+ "pydantic>=2.0,<3.0",
22
+ "python-dotenv>=1.0,<2.0",
23
+ "pyscipopt>=6.0,<7.0",
24
+ "ortools>=9.15,<10.0",
25
+ "gurobipy>=13.0,<14.0",
26
+ "pandas>=2.0,<4.0",
27
+ "psutil>=5.9,<8.0"
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/eva-zuliya/stitchlab-optimization"
32
+ Repository = "https://github.com/eva-zuliya/stitchlab-optimization"
33
+ Issues = "https://github.com/eva-zuliya/stitchlab-optimization/issues"
34
+
35
+ [project.optional-dependencies]
36
+ dev = [
37
+ "pytest>=7.0.0",
38
+ "pytest-asyncio>=0.21.0",
39
+ "black>=23.0.0",
40
+ "ruff>=0.1.0",
41
+ ]
42
+
43
+ [build-system]
44
+ requires = ["setuptools>=61.0", "wheel"]
45
+ build-backend = "setuptools.build_meta"
46
+
47
+ [tool.setuptools]
48
+ package-dir = {"" = "src"}
49
+
50
+ [tool.setuptools.packages.find]
51
+ where = ["src"]
52
+
53
+ [tool.black]
54
+ line-length = 100
55
+ target-version = ['py38']
56
+
57
+ [tool.ruff]
58
+ line-length = 100
59
+ target-version = "py38"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,17 @@
1
+ """StitchLab Agent Core - A powerful agent core application for AI development."""
2
+
3
+ __version__ = "0.0.1"
4
+ __author__ = "StitchLab Team"
5
+ __license__ = "MIT"
6
+
7
+ # Expose main modules
8
+ # from . import config, schema, utils
9
+
10
+ __all__ = [
11
+ "config",
12
+ "schema",
13
+ "utils",
14
+ "__version__",
15
+ "__author__",
16
+ "__license__",
17
+ ]
@@ -0,0 +1,437 @@
1
+ from abc import ABC, ABCMeta, abstractmethod
2
+ import uuid
3
+ import time, threading
4
+ from datetime import datetime, timezone
5
+ from pyscipopt import SCIP_PARAMSETTING # type: ignore
6
+ from ortools.constraint_solver import routing_enums_pb2
7
+ from ortools.constraint_solver import pywrapcp
8
+ from ortools.sat.python import cp_model
9
+ from pydantic import BaseModel
10
+ from typing import Any, Dict, Generic, Type, Optional, TypeVar, final
11
+
12
+ from src.stitchlab_optimization.solver.engine import SolverEngine
13
+ from src.stitchlab_optimization.solver.status import SolverStatus
14
+ from src.stitchlab_optimization.logger.manager import ModelLog, LogManager
15
+ from src.stitchlab_optimization.solver.config import SolverConfig
16
+
17
+
18
+ ParamsBaseModel = TypeVar("ParamsBaseModel", bound="ModelParams")
19
+ SolutionBaseModel = TypeVar("SolutionBaseModel", bound=BaseModel)
20
+
21
+ class ModelParams(BaseModel, ABC):
22
+
23
+ @classmethod
24
+ def from_dict(cls, data: dict):
25
+ return cls(**data)
26
+
27
+
28
+ class ModelMeta(ABCMeta):
29
+ def __new__(mcls, name, bases, attrs):
30
+ # Skip base class
31
+ if ABC in bases:
32
+ return super().__new__(mcls, name, bases, attrs)
33
+
34
+ # Enforce that each subclass defines `builders`
35
+ if "builders_registry" not in attrs:
36
+ raise TypeError(f"{name} must define class-level attribute `builders_registry`.")
37
+
38
+ # Enforce correct type
39
+ if not isinstance(attrs["builders_registry"], dict):
40
+ raise TypeError(f"{name}.builders_registry must be a dict[SolverEngine, Type[ModelBuilder].")
41
+
42
+ # Set name if not provided
43
+ if "name" not in attrs:
44
+ attrs["name"] = name
45
+
46
+ return super().__new__(mcls, name, bases, attrs)
47
+
48
+
49
+ class ModelBuilder(Generic[ParamsBaseModel, SolutionBaseModel], ABC):
50
+ params: ParamsBaseModel
51
+ solution: Optional[SolutionBaseModel] = None
52
+ solver_engine: SolverEngine
53
+ solver_status: SolverStatus
54
+ model: Any = None
55
+ model_output: Any = None
56
+ model_vars: Optional[Dict[str, Any]] = None # Mandatory for OR-Tools CPSAT
57
+ runtime_message: str = ""
58
+ runtime_seconds: float = 0
59
+
60
+ @final
61
+ def __init__(self, params: ParamsBaseModel, solver_engine: SolverEngine):
62
+ self.params = params
63
+ self.solver_engine = solver_engine
64
+ self.solver_status = SolverStatus.UNSOLVED
65
+
66
+ @final
67
+ def _set_model(self, model: Any):
68
+ self.model = model
69
+
70
+ @final
71
+ def _set_model_vars(self, model_vars: Dict[str, Any]):
72
+ self.model_vars = model_vars
73
+
74
+ @final
75
+ def execute(self) -> Optional[SolutionBaseModel]:
76
+ self.build()
77
+
78
+ if self.model is None:
79
+ raise ValueError("Model must be built before execution.")
80
+
81
+ if self.solver_engine == SolverEngine.ORTOOLS_CPSAT and self.model_vars is None:
82
+ raise ValueError("Model variables (model_vars) must be set in the builder when using OR-Tools CPSAT.")
83
+
84
+ self.solve()
85
+ self.solution = self.construct_solution()
86
+
87
+ return self.solution
88
+
89
+ @abstractmethod
90
+ def build(self):
91
+ """
92
+ MUST call:
93
+ self._set_model(...)
94
+ self._set_model_vars(...)
95
+ """
96
+ ...
97
+
98
+ @abstractmethod
99
+ def construct_solution(self) -> Optional[SolutionBaseModel]:
100
+ pass
101
+
102
+ def solve(self):
103
+ if self.model is None:
104
+ print(f"\033[91m\n>>> ERROR while Solving Model : Vars is not setup while building model\n\033[0m")
105
+ self.solver_status = SolverStatus.ERROR
106
+
107
+ raise ValueError(f"ERROR while Solving Model : Model is not saved while building model using solver engine {self.solver_engine}")
108
+
109
+ if self.model_vars is None:
110
+ print(f"\033[91m\n>>> ERROR while Solving Model : Vars is not setup while building model\n\033[0m")
111
+ self.solver_status = SolverStatus.ERROR
112
+
113
+ raise ValueError(f"ERROR while Solving Model : Vars is not saved while building model using solver engine {self.solver_engine}")
114
+
115
+ if self.solver_engine == SolverEngine.PYSCIPOPT:
116
+ self.solve_pyscipopt()
117
+
118
+ elif self.solver_engine == SolverEngine.GUROBI:
119
+ self.solve_gurobi()
120
+
121
+ elif self.solver_engine == SolverEngine.ORTOOLS_SCIP:
122
+ self.solve_ortools_scip()
123
+
124
+ elif self.solver_engine == SolverEngine.ORTOOLS_ROUTING:
125
+ self.solve_ortools_routing()
126
+
127
+ elif self.solver_engine == SolverEngine.ORTOOLS_CPSAT:
128
+ self.solve_ortools_cpsat()
129
+
130
+ raise ValueError(f"Solver engine {self.solver_engine} not supported")
131
+
132
+ def solve_pyscipopt(self):
133
+ SOLVER_CONFIG = SolverConfig()
134
+ start_sol = None
135
+
136
+ self.model.setParam("display/verblevel", SOLVER_CONFIG.MODEL_SOLVER_VERBOSE)
137
+
138
+ self.model.setIntParam("parallel/maxnthreads", SOLVER_CONFIG.LIMIT_MULTI_THREAD)
139
+ self.model.setIntParam("parallel/minnthreads", SOLVER_CONFIG.LIMIT_MULTI_THREAD)
140
+
141
+ if SOLVER_CONFIG.APPLY_HEURISTICS:
142
+ # Phase 1: Heuristics only
143
+ self.model.setHeuristics(SCIP_PARAMSETTING.AGGRESSIVE)
144
+
145
+ self.model.setParam("limits/time", SOLVER_CONFIG.LIMIT_TIME_MINUTES_HEURISTICS*60)
146
+ self.model.setParam("limits/gap", SOLVER_CONFIG.LIMIT_OPTIMALITY_GAP_HEURISTICS)
147
+ self.model.setParam("limits/nodes", 500) # limit nodes so B&B doesn't go far
148
+ self.model.setParam("presolving/maxrounds", 0) # skip heavy presolve if desired
149
+ self.model.setParam("limits/memory", SOLVER_CONFIG.LIMIT_MEMORY_MB)
150
+
151
+ self.model.optimize()
152
+
153
+ try:
154
+ sol = self.model.getBestSol()
155
+ start_sol = {v.name: self.model.getSolVal(sol, v) for v in self.model.getVars()}
156
+
157
+ except:
158
+ pass
159
+
160
+ # Phase 2: Exact MILP solving
161
+ self.model.resetParams()
162
+ self.model.setHeuristics(SCIP_PARAMSETTING.DEFAULT)
163
+
164
+ self.model.setParam("limits/time", SOLVER_CONFIG.LIMIT_TIME_MINUTES_DETERMINISTIC*60)
165
+ self.model.setParam("limits/gap", SOLVER_CONFIG.LIMIT_OPTIMALITY_GAP_DETERMINISTIC)
166
+ self.model.setParam("limits/memory", SOLVER_CONFIG.LIMIT_MEMORY_MB)
167
+
168
+ try:
169
+ if start_sol is not None:
170
+ # Feed initial solution
171
+ sol_obj = self.model.createSol()
172
+ for var in self.model.getVars():
173
+ if var.name in start_sol:
174
+ self.model.setSolVal(sol_obj, var, start_sol[var.name])
175
+ self.model.addSol(sol_obj, free=True)
176
+
177
+ except:
178
+ pass
179
+
180
+ self.model.optimize()
181
+
182
+ self.solver_status = SolverStatus.from_pyscipopt_status(self.model.getStatus())
183
+ print("STATUS", self.model.getStatus(), self.solver_status, "\n\n")
184
+
185
+ if SolverStatus.is_solution_found(self.solver_status):
186
+ self.construct_solution()
187
+
188
+ def solve_gurobi(self):
189
+ SOLVER_CONFIG = SolverConfig()
190
+
191
+ self.model.setParam('OutputFlag', SOLVER_CONFIG.MODEL_SOLVER_VERBOSE)
192
+
193
+ start_sol = None
194
+ if SOLVER_CONFIG.APPLY_HEURISTICS:
195
+ # Phase 1: Heuristics only
196
+ self.model.setParam('TimeLimit', SOLVER_CONFIG.LIMIT_TIME_MINUTES_HEURISTICS * 60)
197
+ self.model.setParam('MIPGap', SOLVER_CONFIG.LIMIT_OPTIMALITY_GAP_HEURISTICS)
198
+ self.model.setParam('NodeLimit', 500) # limit nodes so B&B doesn't go far
199
+ self.model.setParam('Presolve', 0) # skip heavy presolve if desired
200
+ self.model.setParam('Threads', SOLVER_CONFIG.LIMIT_MULTI_THREAD)
201
+
202
+ # Set heuristic focus
203
+ self.model.setParam('Heuristics', 0.8) # Aggressive heuristics
204
+
205
+ self.model.optimize()
206
+
207
+ try:
208
+ status = SolverStatus.from_gurobi_status(self.model.status)
209
+ if SolverStatus.is_solution_found(status):
210
+ start_sol = {}
211
+ for var in self.model.getVars():
212
+ start_sol[var.varName] = var.x
213
+ except:
214
+ pass
215
+
216
+ # Phase 2: Exact MILP solving
217
+ # Reset parameters for exact solving
218
+ self.model.setParam('TimeLimit', SOLVER_CONFIG.LIMIT_TIME_MINUTES_DETERMINISTIC * 60)
219
+ self.model.setParam('MIPGap', SOLVER_CONFIG.LIMIT_OPTIMALITY_GAP_DETERMINISTIC)
220
+ self.model.setParam('NodeLimit', 1000000)
221
+ self.model.setParam('Presolve', -1) # Default presolve
222
+ self.model.setParam('Heuristics', 0.05) # Default heuristics
223
+ self.model.setParam('Threads', SOLVER_CONFIG.LIMIT_MULTI_THREAD)
224
+
225
+ try:
226
+ if start_sol is not None:
227
+ # Feed initial solution
228
+ for var in self.model.getVars():
229
+ if var.varName in start_sol:
230
+ var.start = start_sol[var.varName]
231
+ except:
232
+ pass
233
+
234
+ self.model.optimize()
235
+
236
+ self.solver_status = SolverStatus.from_gurobi_status(self.model.status)
237
+ print("STATUS", self.model.status, self.solver_status, "\n\n")
238
+
239
+ if SolverStatus.is_solution_found(self.solver_status):
240
+ self.construct_solution()
241
+
242
+ def solve_ortools_routing(self):
243
+ SOLVER_CONFIG = SolverConfig()
244
+
245
+ search_parameters = pywrapcp.DefaultRoutingSearchParameters()
246
+ search_parameters.first_solution_strategy = (
247
+ routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC
248
+ )
249
+
250
+ search_parameters.local_search_metaheuristic = (
251
+ routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH
252
+ )
253
+
254
+ search_parameters.solution_limit = 100
255
+ search_parameters.time_limit.seconds = int(SOLVER_CONFIG.LIMIT_TIME_MINUTES_DETERMINISTIC * 60)
256
+
257
+ self.solution = self.model.SolveWithParameters(search_parameters)
258
+
259
+ self.solver_status = SolverStatus.from_ortools_routing_status(self.model.status())
260
+ print("STATUS", self.solver_status)
261
+
262
+ if SolverStatus.is_solution_found(self.solver_status):
263
+ self.construct_solution()
264
+
265
+ def solve_ortools_cpsat(self):
266
+ SOLVER_CONFIG = SolverConfig()
267
+
268
+ solver = cp_model.CpSolver()
269
+ solver.parameters.max_time_in_seconds = SOLVER_CONFIG.LIMIT_TIME_MINUTES_DETERMINISTIC * 60
270
+ solver.parameters.num_search_workers = SOLVER_CONFIG.LIMIT_MULTI_THREAD
271
+
272
+ solver.parameters.log_search_progress = SOLVER_CONFIG.MODEL_SOLVER_VERBOSE
273
+
274
+ result_status = solver.Solve(self.model)
275
+ self.model_output = solver
276
+
277
+ self.solver_status = SolverStatus.from_ortools_cpsat_status(result_status)
278
+ print("STATUS", result_status, self.solver_status)
279
+
280
+ if SolverStatus.is_solution_found(self.solver_status):
281
+ self.construct_solution()
282
+
283
+ def solve_ortools_scip(self):
284
+ SOLVER_CONFIG = SolverConfig()
285
+
286
+ self.model.SetTimeLimit(int(SOLVER_CONFIG.LIMIT_TIME_MINUTES_DETERMINISTIC * 60 * 1000))
287
+ self.model.SetNumThreads(int(SOLVER_CONFIG.LIMIT_MULTI_THREAD))
288
+
289
+ params_str = (
290
+ f"limits/gap={SOLVER_CONFIG.LIMIT_OPTIMALITY_GAP_DETERMINISTIC}\n"
291
+ f"limits/memory={SOLVER_CONFIG.LIMIT_MEMORY_MB}\n"
292
+ f"parallel/maxnthreads={int(SOLVER_CONFIG.LIMIT_MULTI_THREAD)}\n"
293
+ f"lp/threads={int(SOLVER_CONFIG.LIMIT_MULTI_THREAD)}\n"
294
+ f"display/verblevel=5\n"
295
+ )
296
+
297
+ self.model.SetSolverSpecificParametersAsString(params_str)
298
+
299
+ status = self.model.Solve()
300
+ self.solver_status = SolverStatus.from_ortools_scip_status(status)
301
+ print("STATUS", status, self.solver_status, "\n\n")
302
+
303
+ if SolverStatus.is_solution_found(self.solver_status):
304
+ self.construct_solution()
305
+
306
+
307
+ class OptimizationModel(Generic[ParamsBaseModel, SolutionBaseModel], ABC, metaclass=ModelMeta):
308
+ id: str
309
+ name: str
310
+ builders_registry: Dict[SolverEngine, Type[ModelBuilder[ParamsBaseModel, SolutionBaseModel]]]
311
+ builder: ModelBuilder[ParamsBaseModel, SolutionBaseModel]
312
+
313
+ def __init__(self, params: ParamsBaseModel, solver_engine: Optional[SolverEngine] = None):
314
+ self.id = str(uuid.uuid4())
315
+
316
+ if solver_engine is None or solver_engine not in self.builders_registry.keys():
317
+ solver_engine = next(iter(self.builders_registry.keys()))
318
+
319
+ self.builder = self.builders_registry[solver_engine](
320
+ params=params,
321
+ solver_engine=solver_engine
322
+ )
323
+
324
+ @final
325
+ def execute(self, logger: Optional[LogManager] = None) -> Optional[SolutionBaseModel]:
326
+ start_time = time.time()
327
+ solution = None
328
+
329
+ try :
330
+ solution = self.builder.execute()
331
+ self.builder.runtime_message = "success"
332
+
333
+ except Exception as e:
334
+ print(f"\033[91m\n>>> ERROR while Solving Model : {e}\n\033[0m")
335
+ self.builder.runtime_message = f"Error : {str(e)}"
336
+
337
+ finally:
338
+ end_time = time.time()
339
+ self.builder.runtime_seconds = end_time - start_time
340
+
341
+ if logger is not None and logger.is_monitor_optimality:
342
+ logger.put_model_log(model_log=self._model_log)
343
+
344
+ return solution
345
+
346
+ def is_solution_found(self) -> bool:
347
+ return SolverStatus.is_solution_found(self.builder.solver_status)
348
+
349
+ def get_solution(self) -> Optional[SolutionBaseModel]:
350
+ return self.builder.solution
351
+
352
+ @property
353
+ def _model_log(self) -> ModelLog:
354
+ builder = self.builder
355
+ solver_engine = self.builder.solver_engine
356
+
357
+ if solver_engine == SolverEngine.GUROBI:
358
+ # Gurobi Python API
359
+ problem_size_vars = builder.model.NumVars
360
+ problem_size_cons = builder.model.NumConstrs
361
+
362
+ if builder.model.NumObj <= 1:
363
+ optimality_gap = builder.model.MIPGap
364
+ objective_value = builder.model.ObjVal
365
+
366
+ else:
367
+ objective_value = builder.model.getAttr("ObjNVal")
368
+ optimality_gap = builder.model.getAttr("ObjNRelTol")
369
+
370
+ elif solver_engine == SolverEngine.ORTOOLS_SCIP:
371
+ # OR-Tools CP-SAT solver (pywraplp.Solver)
372
+ problem_size_vars = builder.model.NumVariables()
373
+ problem_size_cons = builder.model.NumConstraints()
374
+
375
+ try:
376
+ optimality_gap = builder.model.MipGap()
377
+ except AttributeError:
378
+ optimality_gap = None
379
+
380
+ objective_value = builder.model.Objective().Value()
381
+
382
+ elif solver_engine == SolverEngine.ORTOOLS_ROUTING:
383
+ # OR-Tools RoutingModel
384
+ count_nodes = builder.model.Size()
385
+ count_vehicles = builder.model.vehicles()
386
+ problem_size_vars = count_nodes * count_nodes * count_vehicles
387
+
388
+ # RoutingModel does not expose number of constraints directly
389
+ problem_size_cons = None
390
+ objective_value = builder.solution.ObjectiveValue()
391
+ optimality_gap = None
392
+
393
+ elif solver_engine == SolverEngine.ORTOOLS_CPSAT:
394
+ problem_size_vars = len(builder.model.Proto().variables)
395
+ problem_size_cons = len(builder.model.Proto().constraints)
396
+
397
+ # Objective value (only available if model solved)
398
+ try:
399
+ objective_value = builder.solution.ObjectiveValue()
400
+ except Exception:
401
+ objective_value = None
402
+
403
+ optimality_gap = None
404
+
405
+ elif solver_engine == SolverEngine.PYSCIPOPT:
406
+ problem_size_vars = builder.model.getNVars()
407
+ problem_size_cons = builder.model.getNConss()
408
+ optimality_gap = builder.model.getGap()
409
+
410
+ try:
411
+ objective_value = builder.model.getObjVal()
412
+ except Exception:
413
+ objective_value = None
414
+
415
+ elif solver_engine == SolverEngine.SKLEARN:
416
+ # scikit-learn is not an optimization solver, so these are not applicable
417
+ problem_size_vars = None
418
+ problem_size_cons = None
419
+ optimality_gap = None
420
+ objective_value = None
421
+
422
+ else:
423
+ raise ValueError(f"Solver engine {solver_engine} not supported")
424
+
425
+ return ModelLog(
426
+ solver_engine=solver_engine,
427
+ model_id=self.id,
428
+ model_name=self.name,
429
+ status=builder.solver_status,
430
+ problem_size_vars=problem_size_vars,
431
+ problem_size_cons=problem_size_cons,
432
+ optimality_gap=optimality_gap,
433
+ objective_value=objective_value,
434
+ message=builder.runtime_message,
435
+ runtime_sec=builder.runtime_seconds,
436
+ created_timestamp=datetime.now(timezone.utc).isoformat()
437
+ )