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.
- stitchlab_optimization-0.0.1/LICENSE +22 -0
- stitchlab_optimization-0.0.1/PKG-INFO +33 -0
- stitchlab_optimization-0.0.1/README.md +0 -0
- stitchlab_optimization-0.0.1/pyproject.toml +59 -0
- stitchlab_optimization-0.0.1/setup.cfg +4 -0
- stitchlab_optimization-0.0.1/src/stitchlab_optimization/__init__.py +17 -0
- stitchlab_optimization-0.0.1/src/stitchlab_optimization/builder/__init__.py +0 -0
- stitchlab_optimization-0.0.1/src/stitchlab_optimization/builder/model.py +437 -0
- stitchlab_optimization-0.0.1/src/stitchlab_optimization/builder/workflow.py +149 -0
- stitchlab_optimization-0.0.1/src/stitchlab_optimization/logger/__init__.py +0 -0
- stitchlab_optimization-0.0.1/src/stitchlab_optimization/logger/file_logger.py +120 -0
- stitchlab_optimization-0.0.1/src/stitchlab_optimization/logger/manager.py +94 -0
- stitchlab_optimization-0.0.1/src/stitchlab_optimization/logger/sqlite_logger.py +82 -0
- stitchlab_optimization-0.0.1/src/stitchlab_optimization/solver/__init__.py +0 -0
- stitchlab_optimization-0.0.1/src/stitchlab_optimization/solver/config.py +63 -0
- stitchlab_optimization-0.0.1/src/stitchlab_optimization/solver/engine.py +11 -0
- stitchlab_optimization-0.0.1/src/stitchlab_optimization/solver/status.py +127 -0
- stitchlab_optimization-0.0.1/src/stitchlab_optimization/tools/query/snowflake.py +200 -0
- stitchlab_optimization-0.0.1/src/stitchlab_optimization/tools/query/sqlite.py +0 -0
- stitchlab_optimization-0.0.1/src/stitchlab_optimization/tools/utils.py +41 -0
- stitchlab_optimization-0.0.1/src/stitchlab_optimization.egg-info/PKG-INFO +33 -0
- stitchlab_optimization-0.0.1/src/stitchlab_optimization.egg-info/SOURCES.txt +23 -0
- stitchlab_optimization-0.0.1/src/stitchlab_optimization.egg-info/dependency_links.txt +1 -0
- stitchlab_optimization-0.0.1/src/stitchlab_optimization.egg-info/requires.txt +13 -0
- 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,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
|
+
]
|
|
File without changes
|
|
@@ -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
|
+
)
|