solverforge 0.2.4__tar.gz → 0.2.6__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.
- {solverforge-0.2.4 → solverforge-0.2.6}/Cargo.lock +7 -5
- {solverforge-0.2.4 → solverforge-0.2.6}/Cargo.toml +2 -2
- {solverforge-0.2.4 → solverforge-0.2.6}/PKG-INFO +1 -1
- {solverforge-0.2.4 → solverforge-0.2.6}/pyproject.toml +1 -1
- {solverforge-0.2.4 → solverforge-0.2.6}/python/solverforge/__init__.py +25 -0
- solverforge-0.2.6/python/solverforge/solver/__init__.py +376 -0
- solverforge-0.2.6/python/solverforge/solver/config.py +221 -0
- solverforge-0.2.6/python/solverforge/solver/domain.py +45 -0
- solverforge-0.2.6/python/solverforge/solver/score.py +51 -0
- solverforge-0.2.6/python/solverforge/test/__init__.py +746 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-python/Cargo.toml +2 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-python/python/solverforge/__init__.py +25 -0
- solverforge-0.2.6/solverforge-python/python/solverforge/solver/__init__.py +376 -0
- solverforge-0.2.6/solverforge-python/python/solverforge/solver/config.py +221 -0
- solverforge-0.2.6/solverforge-python/python/solverforge/solver/domain.py +45 -0
- solverforge-0.2.6/solverforge-python/python/solverforge/solver/score.py +51 -0
- solverforge-0.2.6/solverforge-python/python/solverforge/test/__init__.py +746 -0
- solverforge-0.2.6/solverforge-python/src/analysis.rs +414 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-python/src/collectors.rs +182 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-python/src/decorators.rs +3 -16
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-python/src/lambda_analyzer.rs +222 -12
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-python/src/lib.rs +25 -1
- solverforge-0.2.6/solverforge-python/src/score.rs +1714 -0
- solverforge-0.2.6/solverforge-python/src/service.rs +299 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-python/src/solver.rs +66 -2
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-python/src/stream.rs +777 -49
- solverforge-0.2.6/solverforge-service/Cargo.toml +33 -0
- solverforge-0.2.6/solverforge-service/README.md +89 -0
- solverforge-0.2.6/solverforge-service/src/config.rs +106 -0
- solverforge-0.2.6/solverforge-service/src/error.rs +51 -0
- solverforge-0.2.6/solverforge-service/src/jar.rs +246 -0
- solverforge-0.2.6/solverforge-service/src/lib.rs +18 -0
- solverforge-0.2.6/solverforge-service/src/service.rs +242 -0
- solverforge-0.2.6/solverforge-service/src/util.rs +171 -0
- solverforge-0.2.6/solverforge-service/tests/employee_scheduling_integration.rs +984 -0
- solverforge-0.2.6/solverforge-service/tests/integration.rs +61 -0
- solverforge-0.2.6/solverforge-service/tests/solve_integration.rs +393 -0
- solverforge-0.2.4/solverforge-python/src/score.rs +0 -579
- {solverforge-0.2.4 → solverforge-0.2.6}/README.md +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/Cargo.toml +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/README.md +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/analysis/explanation.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/analysis/manager.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/analysis/mod.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/bridge.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/constraints/collectors.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/constraints/constraint.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/constraints/joiners.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/constraints/mod.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/constraints/stream.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/domain/annotations.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/domain/class.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/domain/constraint_config.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/domain/mod.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/domain/model.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/domain/shadow.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/error.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/handles.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/lib.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/score/bendable.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/score/hard_soft.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/score/hard_soft_decimal.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/score/mod.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/score/simple.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/score/simple_decimal.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/solver/builder.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/solver/client.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/solver/config.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/solver/environment.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/solver/factory.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/solver/mod.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/solver/request.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/solver/response.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/solver/termination.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/traits.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/value.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/wasm/expr_builder.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/wasm/expression.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/wasm/generator.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/wasm/host_functions.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/wasm/memory.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-core/src/wasm/mod.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-python/README.md +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-python/src/annotations.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-python/src/bridge.rs +0 -0
- {solverforge-0.2.4 → solverforge-0.2.6}/solverforge-python/src/joiners.rs +0 -0
|
@@ -1507,7 +1507,7 @@ dependencies = [
|
|
|
1507
1507
|
|
|
1508
1508
|
[[package]]
|
|
1509
1509
|
name = "solverforge"
|
|
1510
|
-
version = "0.2.
|
|
1510
|
+
version = "0.2.6"
|
|
1511
1511
|
dependencies = [
|
|
1512
1512
|
"solverforge-core",
|
|
1513
1513
|
"solverforge-derive",
|
|
@@ -1516,7 +1516,7 @@ dependencies = [
|
|
|
1516
1516
|
|
|
1517
1517
|
[[package]]
|
|
1518
1518
|
name = "solverforge-core"
|
|
1519
|
-
version = "0.2.
|
|
1519
|
+
version = "0.2.6"
|
|
1520
1520
|
dependencies = [
|
|
1521
1521
|
"base64",
|
|
1522
1522
|
"indexmap",
|
|
@@ -1532,7 +1532,7 @@ dependencies = [
|
|
|
1532
1532
|
|
|
1533
1533
|
[[package]]
|
|
1534
1534
|
name = "solverforge-derive"
|
|
1535
|
-
version = "0.2.
|
|
1535
|
+
version = "0.2.6"
|
|
1536
1536
|
dependencies = [
|
|
1537
1537
|
"proc-macro2",
|
|
1538
1538
|
"quote",
|
|
@@ -1544,20 +1544,22 @@ dependencies = [
|
|
|
1544
1544
|
|
|
1545
1545
|
[[package]]
|
|
1546
1546
|
name = "solverforge-python"
|
|
1547
|
-
version = "0.2.
|
|
1547
|
+
version = "0.2.6"
|
|
1548
1548
|
dependencies = [
|
|
1549
1549
|
"log",
|
|
1550
1550
|
"pyo3",
|
|
1551
|
+
"rust_decimal",
|
|
1551
1552
|
"serde",
|
|
1552
1553
|
"serde_json",
|
|
1553
1554
|
"solverforge-core",
|
|
1555
|
+
"solverforge-service",
|
|
1554
1556
|
"tempfile",
|
|
1555
1557
|
"thiserror",
|
|
1556
1558
|
]
|
|
1557
1559
|
|
|
1558
1560
|
[[package]]
|
|
1559
1561
|
name = "solverforge-service"
|
|
1560
|
-
version = "0.2.
|
|
1562
|
+
version = "0.2.6"
|
|
1561
1563
|
dependencies = [
|
|
1562
1564
|
"base64",
|
|
1563
1565
|
"chrono",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[workspace]
|
|
2
|
-
members = ["solverforge-core", "solverforge-python"]
|
|
2
|
+
members = ["solverforge-core", "solverforge-python", "solverforge-service"]
|
|
3
3
|
resolver = "2"
|
|
4
4
|
|
|
5
5
|
[workspace.package]
|
|
6
|
-
version = "0.2.
|
|
6
|
+
version = "0.2.6"
|
|
7
7
|
edition = "2021"
|
|
8
8
|
license = "Apache-2.0"
|
|
9
9
|
repository = "https://github.com/solverforge/solverforge"
|
|
@@ -31,6 +31,10 @@ from solverforge._solverforge import (
|
|
|
31
31
|
SimpleScore,
|
|
32
32
|
HardSoftScore,
|
|
33
33
|
HardMediumSoftScore,
|
|
34
|
+
HardSoftDecimalScore,
|
|
35
|
+
HardMediumSoftDecimalScore,
|
|
36
|
+
BendableScore,
|
|
37
|
+
BendableDecimalScore,
|
|
34
38
|
# Decorators
|
|
35
39
|
planning_entity,
|
|
36
40
|
planning_solution,
|
|
@@ -70,6 +74,16 @@ from solverforge._solverforge import (
|
|
|
70
74
|
ConstraintCollectors,
|
|
71
75
|
)
|
|
72
76
|
|
|
77
|
+
# Embedded service management
|
|
78
|
+
from solverforge._solverforge import (
|
|
79
|
+
ServiceConfig,
|
|
80
|
+
EmbeddedService,
|
|
81
|
+
ensure_service,
|
|
82
|
+
is_service_available,
|
|
83
|
+
get_service_url,
|
|
84
|
+
stop_service,
|
|
85
|
+
)
|
|
86
|
+
|
|
73
87
|
__all__ = [
|
|
74
88
|
"__version__",
|
|
75
89
|
# Annotation marker classes
|
|
@@ -91,6 +105,10 @@ __all__ = [
|
|
|
91
105
|
"SimpleScore",
|
|
92
106
|
"HardSoftScore",
|
|
93
107
|
"HardMediumSoftScore",
|
|
108
|
+
"HardSoftDecimalScore",
|
|
109
|
+
"HardMediumSoftDecimalScore",
|
|
110
|
+
"BendableScore",
|
|
111
|
+
"BendableDecimalScore",
|
|
94
112
|
# Decorators
|
|
95
113
|
"planning_entity",
|
|
96
114
|
"planning_solution",
|
|
@@ -128,4 +146,11 @@ __all__ = [
|
|
|
128
146
|
# Collectors
|
|
129
147
|
"Collector",
|
|
130
148
|
"ConstraintCollectors",
|
|
149
|
+
# Embedded service
|
|
150
|
+
"ServiceConfig",
|
|
151
|
+
"EmbeddedService",
|
|
152
|
+
"ensure_service",
|
|
153
|
+
"is_service_available",
|
|
154
|
+
"get_service_url",
|
|
155
|
+
"stop_service",
|
|
131
156
|
]
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""Solver module - compatibility layer for Timefold-style imports."""
|
|
2
|
+
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from threading import Thread, Lock
|
|
6
|
+
from typing import Callable, Optional, TypeVar, Generic
|
|
7
|
+
|
|
8
|
+
from solverforge._solverforge import (
|
|
9
|
+
SolverFactory,
|
|
10
|
+
Solver,
|
|
11
|
+
SolveHandle,
|
|
12
|
+
SolveResponse,
|
|
13
|
+
SolutionManager as _SolutionManager,
|
|
14
|
+
ScoreExplanation,
|
|
15
|
+
ConstraintMatch,
|
|
16
|
+
Indictment,
|
|
17
|
+
ScoreDto,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
Solution_ = TypeVar("Solution_")
|
|
22
|
+
ProblemId_ = TypeVar("ProblemId_")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SolverStatus(Enum):
|
|
26
|
+
"""Solver status enum matching Timefold's SolverStatus.
|
|
27
|
+
|
|
28
|
+
NOT_SOLVING: No active solve operation.
|
|
29
|
+
SOLVING_SCHEDULED: Solve has been submitted but not yet started.
|
|
30
|
+
SOLVING_ACTIVE: Actively solving the problem.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
NOT_SOLVING = "NOT_SOLVING"
|
|
34
|
+
SOLVING_SCHEDULED = "SOLVING_SCHEDULED"
|
|
35
|
+
SOLVING_ACTIVE = "SOLVING_ACTIVE"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SolverJob(Generic[Solution_, ProblemId_]):
|
|
39
|
+
"""Represents a problem that has been submitted to solve on the SolverManager.
|
|
40
|
+
|
|
41
|
+
Use get_final_best_solution() to wait for the solve to complete and get the result.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
problem_id: ProblemId_,
|
|
47
|
+
solver: "Solver",
|
|
48
|
+
problem: Solution_,
|
|
49
|
+
best_solution_consumer: Optional[Callable[[Solution_], None]] = None,
|
|
50
|
+
final_best_solution_consumer: Optional[Callable[[Solution_], None]] = None,
|
|
51
|
+
):
|
|
52
|
+
self._problem_id = problem_id
|
|
53
|
+
self._solver = solver
|
|
54
|
+
self._problem = problem
|
|
55
|
+
self._best_solution_consumer = best_solution_consumer
|
|
56
|
+
self._final_best_solution_consumer = final_best_solution_consumer
|
|
57
|
+
self._status = SolverStatus.SOLVING_SCHEDULED
|
|
58
|
+
self._final_solution: Optional[Solution_] = None
|
|
59
|
+
self._response: Optional[SolveResponse] = None
|
|
60
|
+
self._exception: Optional[Exception] = None
|
|
61
|
+
self._terminated_early = False
|
|
62
|
+
self._lock = Lock()
|
|
63
|
+
self._thread: Optional[Thread] = None
|
|
64
|
+
|
|
65
|
+
def _run(self):
|
|
66
|
+
"""Internal method to run the solver in a thread."""
|
|
67
|
+
try:
|
|
68
|
+
with self._lock:
|
|
69
|
+
self._status = SolverStatus.SOLVING_ACTIVE
|
|
70
|
+
|
|
71
|
+
response = self._solver.solve(self._problem)
|
|
72
|
+
|
|
73
|
+
with self._lock:
|
|
74
|
+
self._response = response
|
|
75
|
+
self._final_solution = response.solution
|
|
76
|
+
self._status = SolverStatus.NOT_SOLVING
|
|
77
|
+
|
|
78
|
+
if self._best_solution_consumer and response.solution:
|
|
79
|
+
self._best_solution_consumer(response.solution)
|
|
80
|
+
|
|
81
|
+
if self._final_best_solution_consumer and response.solution:
|
|
82
|
+
self._final_best_solution_consumer(response.solution)
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
with self._lock:
|
|
86
|
+
self._exception = e
|
|
87
|
+
self._status = SolverStatus.NOT_SOLVING
|
|
88
|
+
|
|
89
|
+
def _start(self):
|
|
90
|
+
"""Start the solver in a background thread."""
|
|
91
|
+
self._thread = Thread(target=self._run, daemon=True)
|
|
92
|
+
self._thread.start()
|
|
93
|
+
|
|
94
|
+
def get_problem_id(self) -> ProblemId_:
|
|
95
|
+
"""Get the problem id for this solver job.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
The problem id.
|
|
99
|
+
"""
|
|
100
|
+
return self._problem_id
|
|
101
|
+
|
|
102
|
+
def get_solver_status(self) -> SolverStatus:
|
|
103
|
+
"""Get the current solver status.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
NOT_SOLVING if terminated, SOLVING_SCHEDULED if waiting,
|
|
107
|
+
SOLVING_ACTIVE if actively solving.
|
|
108
|
+
"""
|
|
109
|
+
with self._lock:
|
|
110
|
+
return self._status
|
|
111
|
+
|
|
112
|
+
def get_solving_duration(self) -> timedelta:
|
|
113
|
+
"""Get the duration spent solving.
|
|
114
|
+
|
|
115
|
+
Note: Currently returns timedelta(0) as precise timing is not yet implemented.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Duration spent solving.
|
|
119
|
+
"""
|
|
120
|
+
return timedelta(0)
|
|
121
|
+
|
|
122
|
+
def get_final_best_solution(self) -> Solution_:
|
|
123
|
+
"""Wait for the solver to complete and return the final best solution.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
The best solution found.
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
Exception: If the solver encountered an error.
|
|
130
|
+
"""
|
|
131
|
+
if self._thread:
|
|
132
|
+
self._thread.join()
|
|
133
|
+
|
|
134
|
+
with self._lock:
|
|
135
|
+
if self._exception:
|
|
136
|
+
raise self._exception
|
|
137
|
+
return self._final_solution
|
|
138
|
+
|
|
139
|
+
def terminate_early(self) -> None:
|
|
140
|
+
"""Terminate the solver early.
|
|
141
|
+
|
|
142
|
+
Currently waits for the solve to complete as streaming termination
|
|
143
|
+
is not yet implemented.
|
|
144
|
+
"""
|
|
145
|
+
with self._lock:
|
|
146
|
+
self._terminated_early = True
|
|
147
|
+
|
|
148
|
+
def is_terminated_early(self) -> bool:
|
|
149
|
+
"""Check if terminate_early was called.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
True if terminate_early was called.
|
|
153
|
+
"""
|
|
154
|
+
with self._lock:
|
|
155
|
+
return self._terminated_early
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class SolverManager(Generic[Solution_, ProblemId_]):
|
|
159
|
+
"""High-level solver manager providing Timefold-compatible API.
|
|
160
|
+
|
|
161
|
+
Manages solver lifecycle and provides synchronous solving capabilities.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def create(solver_config, service_url=None):
|
|
166
|
+
"""Create a SolverManager from config.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
solver_config: SolverConfig with solution/entity classes and constraints
|
|
170
|
+
service_url: Optional URL for the solver service
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Configured SolverManager instance
|
|
174
|
+
"""
|
|
175
|
+
return SolverManager(solver_config, service_url)
|
|
176
|
+
|
|
177
|
+
def __init__(self, solver_config, service_url=None):
|
|
178
|
+
self._config = solver_config
|
|
179
|
+
self._service_url = service_url
|
|
180
|
+
self._active_solves = {}
|
|
181
|
+
|
|
182
|
+
# Extract constraint provider from score_director_factory_config
|
|
183
|
+
constraint_provider = None
|
|
184
|
+
if solver_config.score_director_factory_config:
|
|
185
|
+
constraint_provider = (
|
|
186
|
+
solver_config.score_director_factory_config.constraint_provider
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Get the native config wrapper
|
|
190
|
+
inner_config = (
|
|
191
|
+
solver_config._inner if hasattr(solver_config, "_inner") else solver_config
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if not constraint_provider:
|
|
195
|
+
raise ValueError(
|
|
196
|
+
"SolverConfig must include score_director_factory_config with constraint_provider"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
self._factory = SolverFactory.create(
|
|
200
|
+
inner_config, constraint_provider, service_url
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def solve(
|
|
204
|
+
self,
|
|
205
|
+
problem_id: ProblemId_,
|
|
206
|
+
problem: Solution_,
|
|
207
|
+
final_best_solution_consumer: Optional[Callable[[Solution_], None]] = None,
|
|
208
|
+
) -> SolverJob[Solution_, ProblemId_]:
|
|
209
|
+
"""Submits a problem to solve asynchronously and returns immediately.
|
|
210
|
+
|
|
211
|
+
The problem is solved on a background thread. Use SolverJob.get_final_best_solution()
|
|
212
|
+
to wait for the result.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
problem_id: Unique identifier for this solve operation
|
|
216
|
+
problem: The planning problem instance to solve
|
|
217
|
+
final_best_solution_consumer: Optional callback invoked with final solution
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
SolverJob that can be used to get the result or terminate early
|
|
221
|
+
"""
|
|
222
|
+
solver = self._factory.build_solver()
|
|
223
|
+
job = SolverJob(
|
|
224
|
+
problem_id=problem_id,
|
|
225
|
+
solver=solver,
|
|
226
|
+
problem=problem,
|
|
227
|
+
best_solution_consumer=None,
|
|
228
|
+
final_best_solution_consumer=final_best_solution_consumer,
|
|
229
|
+
)
|
|
230
|
+
self._active_solves[problem_id] = job
|
|
231
|
+
job._start()
|
|
232
|
+
return job
|
|
233
|
+
|
|
234
|
+
def solve_and_listen(
|
|
235
|
+
self,
|
|
236
|
+
problem_id: ProblemId_,
|
|
237
|
+
problem: Solution_,
|
|
238
|
+
listener: Callable[[Solution_], None],
|
|
239
|
+
) -> SolverJob[Solution_, ProblemId_]:
|
|
240
|
+
"""Submits a problem to solve and calls listener for best solutions.
|
|
241
|
+
|
|
242
|
+
The problem is solved on a background thread. The listener is called
|
|
243
|
+
when new best solutions are found (currently called once with final solution).
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
problem_id: Unique identifier for this solve operation
|
|
247
|
+
problem: The planning problem instance to solve
|
|
248
|
+
listener: Callback for solution updates
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
SolverJob that can be used to get the result or terminate early
|
|
252
|
+
"""
|
|
253
|
+
solver = self._factory.build_solver()
|
|
254
|
+
job = SolverJob(
|
|
255
|
+
problem_id=problem_id,
|
|
256
|
+
solver=solver,
|
|
257
|
+
problem=problem,
|
|
258
|
+
best_solution_consumer=listener,
|
|
259
|
+
final_best_solution_consumer=None,
|
|
260
|
+
)
|
|
261
|
+
self._active_solves[problem_id] = job
|
|
262
|
+
job._start()
|
|
263
|
+
return job
|
|
264
|
+
|
|
265
|
+
def get_solver_status(self, problem_id: ProblemId_) -> SolverStatus:
|
|
266
|
+
"""Get the status of a solver job.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
problem_id: The problem id to check
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
SolverStatus for the given problem, or NOT_SOLVING if not found
|
|
273
|
+
"""
|
|
274
|
+
job = self._active_solves.get(problem_id)
|
|
275
|
+
if job:
|
|
276
|
+
return job.get_solver_status()
|
|
277
|
+
return SolverStatus.NOT_SOLVING
|
|
278
|
+
|
|
279
|
+
def terminate_early(self, problem_id: ProblemId_) -> None:
|
|
280
|
+
"""Terminate a solver job early.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
problem_id: The problem id to terminate
|
|
284
|
+
"""
|
|
285
|
+
job = self._active_solves.get(problem_id)
|
|
286
|
+
if job:
|
|
287
|
+
job.terminate_early()
|
|
288
|
+
|
|
289
|
+
def close(self) -> None:
|
|
290
|
+
"""Release solver manager resources."""
|
|
291
|
+
self._active_solves.clear()
|
|
292
|
+
|
|
293
|
+
def __enter__(self) -> "SolverManager[Solution_, ProblemId_]":
|
|
294
|
+
"""Context manager entry."""
|
|
295
|
+
return self
|
|
296
|
+
|
|
297
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
298
|
+
"""Context manager exit - calls close()."""
|
|
299
|
+
self.close()
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class SolutionManager:
|
|
303
|
+
"""Solution manager for score calculation and constraint analysis.
|
|
304
|
+
|
|
305
|
+
Wraps the native SolutionManager to provide Timefold-compatible API.
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
@staticmethod
|
|
309
|
+
def create(solver_factory_or_manager):
|
|
310
|
+
"""Create a SolutionManager from a SolverFactory or SolverManager.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
solver_factory_or_manager: SolverFactory or SolverManager instance
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
Configured SolutionManager instance
|
|
317
|
+
"""
|
|
318
|
+
return SolutionManager(solver_factory_or_manager)
|
|
319
|
+
|
|
320
|
+
def __init__(self, solver_factory_or_manager):
|
|
321
|
+
if isinstance(solver_factory_or_manager, SolverManager):
|
|
322
|
+
self._factory = solver_factory_or_manager._factory
|
|
323
|
+
else:
|
|
324
|
+
self._factory = solver_factory_or_manager
|
|
325
|
+
|
|
326
|
+
# Create native SolutionManager from factory
|
|
327
|
+
self._inner = _SolutionManager.create(self._factory)
|
|
328
|
+
|
|
329
|
+
def update(self, solution):
|
|
330
|
+
"""Calculate and update the score for a solution.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
solution: Planning solution to score
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
ScoreDto with the calculated score
|
|
337
|
+
"""
|
|
338
|
+
return self._inner.update(solution)
|
|
339
|
+
|
|
340
|
+
def analyze(self, solution):
|
|
341
|
+
"""Analyze constraint matches for a solution.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
solution: Planning solution to analyze
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
ScoreExplanation with constraint match details
|
|
348
|
+
"""
|
|
349
|
+
return self._inner.analyze(solution)
|
|
350
|
+
|
|
351
|
+
def explain(self, solution):
|
|
352
|
+
"""Get score explanation for a solution.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
solution: Planning solution to explain
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
ScoreExplanation with detailed constraint breakdown
|
|
359
|
+
"""
|
|
360
|
+
return self._inner.explain(solution)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
__all__ = [
|
|
364
|
+
"SolverStatus",
|
|
365
|
+
"SolverJob",
|
|
366
|
+
"SolverFactory",
|
|
367
|
+
"Solver",
|
|
368
|
+
"SolveHandle",
|
|
369
|
+
"SolveResponse",
|
|
370
|
+
"SolverManager",
|
|
371
|
+
"SolutionManager",
|
|
372
|
+
"ScoreExplanation",
|
|
373
|
+
"ConstraintMatch",
|
|
374
|
+
"Indictment",
|
|
375
|
+
"ScoreDto",
|
|
376
|
+
]
|