solverforge 0.2.6__tar.gz → 0.3.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.
- {solverforge-0.2.6 → solverforge-0.3.0}/Cargo.lock +15 -5
- {solverforge-0.2.6 → solverforge-0.3.0}/Cargo.toml +1 -1
- solverforge-0.3.0/PKG-INFO +232 -0
- solverforge-0.3.0/README.md +208 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/pyproject.toml +4 -6
- {solverforge-0.2.6 → solverforge-0.3.0}/python/solverforge/__init__.py +6 -0
- {solverforge-0.2.6/solverforge-python → solverforge-0.3.0}/python/solverforge/solver/__init__.py +3 -0
- solverforge-0.3.0/solverforge-core/README.md +118 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/analysis/manager.rs +18 -24
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/bridge.rs +2 -2
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/constraints/collectors.rs +294 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/constraints/constraint.rs +227 -1
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/constraints/joiners.rs +36 -1
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/constraints/mod.rs +2 -0
- solverforge-0.3.0/solverforge-core/src/constraints/named_expr.rs +221 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/constraints/stream.rs +462 -1
- solverforge-0.3.0/solverforge-core/src/domain/annotations.rs +1057 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/domain/class.rs +17 -26
- solverforge-0.3.0/solverforge-core/src/domain/listener.rs +981 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/domain/mod.rs +6 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/domain/model.rs +134 -55
- solverforge-0.3.0/solverforge-core/src/entity_context.rs +193 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/lib.rs +15 -10
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/score/hard_soft.rs +77 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/solver/builder.rs +55 -106
- solverforge-0.3.0/solverforge-core/src/solver/change.rs +1139 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/solver/factory.rs +3 -10
- solverforge-0.3.0/solverforge-core/src/solver/manager.rs +253 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/solver/mod.rs +9 -2
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/solver/request.rs +42 -61
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/solver/termination.rs +24 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/traits.rs +34 -41
- solverforge-0.2.6/solverforge-core/src/wasm/expr_builder.rs → solverforge-0.3.0/solverforge-core/src/wasm/expression/builder.rs +142 -4
- solverforge-0.3.0/solverforge-core/src/wasm/expression/mod.rs +349 -0
- solverforge-0.3.0/solverforge-core/src/wasm/expression/substitution.rs +199 -0
- solverforge-0.3.0/solverforge-core/src/wasm/expression/tests.rs +761 -0
- solverforge-0.3.0/solverforge-core/src/wasm/generator/codegen.rs +317 -0
- solverforge-0.3.0/solverforge-core/src/wasm/generator/compiler.rs +801 -0
- solverforge-0.3.0/solverforge-core/src/wasm/generator/mod.rs +469 -0
- solverforge-0.3.0/solverforge-core/src/wasm/generator/predicate.rs +38 -0
- solverforge-0.3.0/solverforge-core/src/wasm/generator/tests.rs +280 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/wasm/host_functions.rs +62 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/wasm/memory.rs +2 -2
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/wasm/mod.rs +2 -6
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/Cargo.toml +2 -1
- solverforge-0.3.0/solverforge-python/README.md +208 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/python/solverforge/__init__.py +6 -0
- {solverforge-0.2.6 → solverforge-0.3.0/solverforge-python}/python/solverforge/solver/__init__.py +3 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/src/annotations.rs +18 -7
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/src/bridge.rs +37 -5
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/src/collectors.rs +202 -103
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/src/decorators.rs +229 -67
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/src/joiners.rs +6 -132
- solverforge-0.3.0/solverforge-python/src/lambda_analyzer/ast_convert/guards.rs +116 -0
- solverforge-0.3.0/solverforge-python/src/lambda_analyzer/ast_convert/inference.rs +549 -0
- solverforge-0.3.0/solverforge-python/src/lambda_analyzer/ast_convert/mod.rs +1035 -0
- solverforge-0.3.0/solverforge-python/src/lambda_analyzer/ast_convert/types.rs +65 -0
- solverforge-0.3.0/solverforge-python/src/lambda_analyzer/conditionals.rs +491 -0
- solverforge-0.3.0/solverforge-python/src/lambda_analyzer/constants.rs +370 -0
- solverforge-0.3.0/solverforge-python/src/lambda_analyzer/lambda_parsing.rs +183 -0
- solverforge-0.3.0/solverforge-python/src/lambda_analyzer/loops.rs +729 -0
- solverforge-0.3.0/solverforge-python/src/lambda_analyzer/method_analysis.rs +248 -0
- solverforge-0.3.0/solverforge-python/src/lambda_analyzer/mod.rs +269 -0
- solverforge-0.3.0/solverforge-python/src/lambda_analyzer/patterns.rs +36 -0
- solverforge-0.3.0/solverforge-python/src/lambda_analyzer/registry.rs +155 -0
- solverforge-0.3.0/solverforge-python/src/lambda_analyzer/sequential.rs +353 -0
- solverforge-0.3.0/solverforge-python/src/lambda_analyzer/tests.rs +428 -0
- solverforge-0.3.0/solverforge-python/src/lambda_analyzer/type_inference.rs +447 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/src/lib.rs +7 -1
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/src/service.rs +26 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/src/solver.rs +681 -151
- solverforge-0.3.0/solverforge-python/src/stream.rs +4089 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-service/Cargo.toml +1 -1
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-service/src/jar.rs +120 -18
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-service/src/lib.rs +1 -1
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-service/src/service.rs +74 -7
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-service/src/util.rs +17 -42
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-service/tests/employee_scheduling_integration.rs +82 -123
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-service/tests/solve_integration.rs +2 -2
- solverforge-0.2.6/PKG-INFO +0 -83
- solverforge-0.2.6/README.md +0 -57
- solverforge-0.2.6/solverforge-core/README.md +0 -64
- solverforge-0.2.6/solverforge-core/src/domain/annotations.rs +0 -305
- solverforge-0.2.6/solverforge-core/src/wasm/expression.rs +0 -614
- solverforge-0.2.6/solverforge-core/src/wasm/generator.rs +0 -1617
- solverforge-0.2.6/solverforge-python/README.md +0 -57
- solverforge-0.2.6/solverforge-python/src/lambda_analyzer.rs +0 -1578
- solverforge-0.2.6/solverforge-python/src/stream.rs +0 -2017
- {solverforge-0.2.6 → solverforge-0.3.0}/python/solverforge/solver/config.py +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/python/solverforge/solver/domain.py +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/python/solverforge/solver/score.py +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/python/solverforge/test/__init__.py +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/Cargo.toml +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/analysis/explanation.rs +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/analysis/mod.rs +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/domain/constraint_config.rs +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/domain/shadow.rs +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/error.rs +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/handles.rs +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/score/bendable.rs +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/score/hard_soft_decimal.rs +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/score/mod.rs +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/score/simple.rs +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/score/simple_decimal.rs +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/solver/client.rs +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/solver/config.rs +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/solver/environment.rs +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/solver/response.rs +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/value.rs +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/python/solverforge/solver/config.py +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/python/solverforge/solver/domain.py +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/python/solverforge/solver/score.py +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/python/solverforge/test/__init__.py +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/src/analysis.rs +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/src/score.rs +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-service/README.md +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-service/src/config.rs +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-service/src/error.rs +0 -0
- {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-service/tests/integration.rs +0 -0
|
@@ -1507,7 +1507,7 @@ dependencies = [
|
|
|
1507
1507
|
|
|
1508
1508
|
[[package]]
|
|
1509
1509
|
name = "solverforge"
|
|
1510
|
-
version = "0.
|
|
1510
|
+
version = "0.3.0"
|
|
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.
|
|
1519
|
+
version = "0.3.0"
|
|
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.
|
|
1535
|
+
version = "0.3.0"
|
|
1536
1536
|
dependencies = [
|
|
1537
1537
|
"proc-macro2",
|
|
1538
1538
|
"quote",
|
|
@@ -1544,8 +1544,9 @@ dependencies = [
|
|
|
1544
1544
|
|
|
1545
1545
|
[[package]]
|
|
1546
1546
|
name = "solverforge-python"
|
|
1547
|
-
version = "0.
|
|
1547
|
+
version = "0.3.0"
|
|
1548
1548
|
dependencies = [
|
|
1549
|
+
"env_logger",
|
|
1549
1550
|
"log",
|
|
1550
1551
|
"pyo3",
|
|
1551
1552
|
"rust_decimal",
|
|
@@ -1559,7 +1560,7 @@ dependencies = [
|
|
|
1559
1560
|
|
|
1560
1561
|
[[package]]
|
|
1561
1562
|
name = "solverforge-service"
|
|
1562
|
-
version = "0.
|
|
1563
|
+
version = "0.3.0"
|
|
1563
1564
|
dependencies = [
|
|
1564
1565
|
"base64",
|
|
1565
1566
|
"chrono",
|
|
@@ -1655,6 +1656,15 @@ dependencies = [
|
|
|
1655
1656
|
"windows-sys 0.61.2",
|
|
1656
1657
|
]
|
|
1657
1658
|
|
|
1659
|
+
[[package]]
|
|
1660
|
+
name = "test-support"
|
|
1661
|
+
version = "0.3.0"
|
|
1662
|
+
dependencies = [
|
|
1663
|
+
"proptest",
|
|
1664
|
+
"serde_json",
|
|
1665
|
+
"solverforge-core",
|
|
1666
|
+
]
|
|
1667
|
+
|
|
1658
1668
|
[[package]]
|
|
1659
1669
|
name = "thiserror"
|
|
1660
1670
|
version = "2.0.17"
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: solverforge
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Classifier: Programming Language :: Rust
|
|
5
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Summary: Python bindings for SolverForge constraint solver
|
|
14
|
+
Keywords: constraint,solver,optimization,scheduling,planning
|
|
15
|
+
License: Apache-2.0
|
|
16
|
+
Requires-Python: >=3.13
|
|
17
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
18
|
+
Project-URL: Homepage, https://solverforge.org
|
|
19
|
+
Project-URL: Documentation, https://docs.solverforge.org
|
|
20
|
+
Project-URL: Repository, https://github.com/solverforge/solverforge
|
|
21
|
+
Project-URL: Bug Tracker, https://github.com/solverforge/solverforge/issues
|
|
22
|
+
Project-URL: Changelog, https://github.com/solverforge/solverforge/blob/main/CHANGELOG.md
|
|
23
|
+
|
|
24
|
+
# SolverForge Python
|
|
25
|
+
|
|
26
|
+
Python bindings for the SolverForge constraint solver.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install solverforge
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from solverforge import (
|
|
38
|
+
planning_entity, planning_solution, constraint_provider,
|
|
39
|
+
PlanningId, PlanningVariable, PlanningListVariable,
|
|
40
|
+
HardSoftScore, SolverFactory, TerminationConfig,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@planning_entity
|
|
44
|
+
class Lesson:
|
|
45
|
+
id: str
|
|
46
|
+
subject: str
|
|
47
|
+
timeslot: str | None = None
|
|
48
|
+
room: str | None = None
|
|
49
|
+
|
|
50
|
+
@planning_solution
|
|
51
|
+
class Timetable:
|
|
52
|
+
timeslots: list[str]
|
|
53
|
+
rooms: list[str]
|
|
54
|
+
lessons: list[Lesson]
|
|
55
|
+
score: HardSoftScore | None = None
|
|
56
|
+
|
|
57
|
+
@constraint_provider
|
|
58
|
+
def define_constraints(factory):
|
|
59
|
+
return [
|
|
60
|
+
factory.for_each(Lesson)
|
|
61
|
+
.join(Lesson, equal(lambda l: l.room), equal(lambda l: l.timeslot))
|
|
62
|
+
.filter(lambda l1, l2: l1.id < l2.id)
|
|
63
|
+
.penalize(HardSoftScore.ONE_HARD)
|
|
64
|
+
.as_constraint("Room conflict"),
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
# Solve
|
|
68
|
+
solver = SolverFactory.create(Timetable, define_constraints)
|
|
69
|
+
.with_termination(TerminationConfig.with_spent_limit("PT30S"))
|
|
70
|
+
.build()
|
|
71
|
+
solution = solver.solve(problem)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Features
|
|
75
|
+
|
|
76
|
+
### Domain Modeling
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from solverforge import (
|
|
80
|
+
planning_entity, planning_solution,
|
|
81
|
+
PlanningVariable, PlanningListVariable,
|
|
82
|
+
InverseRelationShadow, IndexShadow, PreviousElementShadow, NextElementShadow,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Standard planning variable
|
|
86
|
+
@planning_entity
|
|
87
|
+
class Shift:
|
|
88
|
+
id: str
|
|
89
|
+
employee: str | None = None # Assigned by solver
|
|
90
|
+
|
|
91
|
+
# List planning variable (vehicle routing)
|
|
92
|
+
@planning_entity
|
|
93
|
+
class Vehicle:
|
|
94
|
+
id: str
|
|
95
|
+
visits: list[str] = [] # List of visit IDs, assigned by solver
|
|
96
|
+
|
|
97
|
+
# Shadow variables (auto-updated when list changes)
|
|
98
|
+
@planning_entity
|
|
99
|
+
class Visit:
|
|
100
|
+
id: str
|
|
101
|
+
vehicle: str | None = None # @InverseRelationShadowVariable
|
|
102
|
+
index: int | None = None # @IndexShadowVariable
|
|
103
|
+
previous: str | None = None # @PreviousElementShadowVariable
|
|
104
|
+
next: str | None = None # @NextElementShadowVariable
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Constraint Streams
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from solverforge import (
|
|
111
|
+
equal, less_than, greater_than, overlapping, filtering,
|
|
112
|
+
count, sum_, min_, max_, to_list, to_set,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
@constraint_provider
|
|
116
|
+
def constraints(factory):
|
|
117
|
+
return [
|
|
118
|
+
# forEach / forEachIncludingUnassigned / forEachUniquePair
|
|
119
|
+
factory.for_each(Lesson),
|
|
120
|
+
factory.for_each_including_unassigned(Lesson),
|
|
121
|
+
factory.for_each_unique_pair(Lesson, equal(lambda l: l.timeslot)),
|
|
122
|
+
|
|
123
|
+
# filter
|
|
124
|
+
factory.for_each(Lesson)
|
|
125
|
+
.filter(lambda l: l.room is not None),
|
|
126
|
+
|
|
127
|
+
# join with joiners
|
|
128
|
+
factory.for_each(Lesson)
|
|
129
|
+
.join(Room, equal(lambda l: l.room, lambda r: r.id)),
|
|
130
|
+
|
|
131
|
+
# ifExists / ifNotExists
|
|
132
|
+
factory.for_each(Lesson)
|
|
133
|
+
.if_exists(Conflict, equal(lambda l: l.id, lambda c: c.lesson_id)),
|
|
134
|
+
|
|
135
|
+
# groupBy with collectors
|
|
136
|
+
factory.for_each(Lesson)
|
|
137
|
+
.group_by(lambda l: l.room, count())
|
|
138
|
+
.filter(lambda room, cnt: cnt > 1)
|
|
139
|
+
.penalize(HardSoftScore.ONE_HARD, lambda room, cnt: cnt - 1),
|
|
140
|
+
|
|
141
|
+
# map / expand / flattenLast
|
|
142
|
+
factory.for_each(Vehicle)
|
|
143
|
+
.flatten_last(lambda v: v.visits),
|
|
144
|
+
]
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### SolverManager (Multi-Problem Solving)
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from solverforge import SolverManager, HttpSolverService
|
|
151
|
+
|
|
152
|
+
service = HttpSolverService("http://localhost:8080")
|
|
153
|
+
manager = SolverManager(Timetable, define_constraints, service)
|
|
154
|
+
|
|
155
|
+
# Solve multiple problems concurrently
|
|
156
|
+
manager.solve("problem-1", problem1)
|
|
157
|
+
manager.solve("problem-2", problem2)
|
|
158
|
+
|
|
159
|
+
# Check solutions
|
|
160
|
+
solution = manager.get_best_solution("problem-1")
|
|
161
|
+
if solution:
|
|
162
|
+
print(f"Score: {solution.score}")
|
|
163
|
+
|
|
164
|
+
# Terminate
|
|
165
|
+
manager.terminate("problem-1")
|
|
166
|
+
manager.terminate_all()
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### ConstraintVerifier (Testing)
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from solverforge import ConstraintVerifier
|
|
173
|
+
|
|
174
|
+
verifier = ConstraintVerifier.build(Timetable, define_constraints)
|
|
175
|
+
|
|
176
|
+
# Test specific constraint
|
|
177
|
+
verifier.verify_that(lambda f: f.for_each(Lesson)...)
|
|
178
|
+
.given(lesson1, lesson2)
|
|
179
|
+
.penalizes_by(1)
|
|
180
|
+
|
|
181
|
+
# Test entire solution
|
|
182
|
+
verifier.verify_that()
|
|
183
|
+
.given_solution(solution)
|
|
184
|
+
.scores(HardSoftScore.of(-2, -10))
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Score Types
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from solverforge import (
|
|
191
|
+
SimpleScore, HardSoftScore, HardMediumSoftScore,
|
|
192
|
+
BendableScore, SimpleDecimalScore, HardSoftDecimalScore,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
score = HardSoftScore.of(-2, -15)
|
|
196
|
+
print(f"Hard: {score.hard_score}, Soft: {score.soft_score}")
|
|
197
|
+
print(f"Feasible: {score.is_feasible()}")
|
|
198
|
+
|
|
199
|
+
# Arithmetic
|
|
200
|
+
combined = score + HardSoftScore.of(0, -5)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Solver Configuration
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
from solverforge import (
|
|
207
|
+
SolverFactory, SolverConfig, TerminationConfig,
|
|
208
|
+
EnvironmentMode, MoveThreadCount,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
solver = SolverFactory.create(Timetable, define_constraints)
|
|
212
|
+
.with_termination(
|
|
213
|
+
TerminationConfig()
|
|
214
|
+
.with_spent_limit("PT5M")
|
|
215
|
+
.with_unimproved_spent_limit("PT1M")
|
|
216
|
+
.with_best_score_feasible(True)
|
|
217
|
+
)
|
|
218
|
+
.with_environment_mode(EnvironmentMode.REPRODUCIBLE)
|
|
219
|
+
.with_random_seed(42)
|
|
220
|
+
.with_move_thread_count(MoveThreadCount.AUTO)
|
|
221
|
+
.build()
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Requirements
|
|
225
|
+
|
|
226
|
+
- Python 3.13+ (required for modern AST support)
|
|
227
|
+
- Java 21+ (for solver service, auto-started)
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
Apache-2.0
|
|
232
|
+
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# SolverForge Python
|
|
2
|
+
|
|
3
|
+
Python bindings for the SolverForge constraint solver.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install solverforge
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from solverforge import (
|
|
15
|
+
planning_entity, planning_solution, constraint_provider,
|
|
16
|
+
PlanningId, PlanningVariable, PlanningListVariable,
|
|
17
|
+
HardSoftScore, SolverFactory, TerminationConfig,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
@planning_entity
|
|
21
|
+
class Lesson:
|
|
22
|
+
id: str
|
|
23
|
+
subject: str
|
|
24
|
+
timeslot: str | None = None
|
|
25
|
+
room: str | None = None
|
|
26
|
+
|
|
27
|
+
@planning_solution
|
|
28
|
+
class Timetable:
|
|
29
|
+
timeslots: list[str]
|
|
30
|
+
rooms: list[str]
|
|
31
|
+
lessons: list[Lesson]
|
|
32
|
+
score: HardSoftScore | None = None
|
|
33
|
+
|
|
34
|
+
@constraint_provider
|
|
35
|
+
def define_constraints(factory):
|
|
36
|
+
return [
|
|
37
|
+
factory.for_each(Lesson)
|
|
38
|
+
.join(Lesson, equal(lambda l: l.room), equal(lambda l: l.timeslot))
|
|
39
|
+
.filter(lambda l1, l2: l1.id < l2.id)
|
|
40
|
+
.penalize(HardSoftScore.ONE_HARD)
|
|
41
|
+
.as_constraint("Room conflict"),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
# Solve
|
|
45
|
+
solver = SolverFactory.create(Timetable, define_constraints)
|
|
46
|
+
.with_termination(TerminationConfig.with_spent_limit("PT30S"))
|
|
47
|
+
.build()
|
|
48
|
+
solution = solver.solve(problem)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
### Domain Modeling
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from solverforge import (
|
|
57
|
+
planning_entity, planning_solution,
|
|
58
|
+
PlanningVariable, PlanningListVariable,
|
|
59
|
+
InverseRelationShadow, IndexShadow, PreviousElementShadow, NextElementShadow,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Standard planning variable
|
|
63
|
+
@planning_entity
|
|
64
|
+
class Shift:
|
|
65
|
+
id: str
|
|
66
|
+
employee: str | None = None # Assigned by solver
|
|
67
|
+
|
|
68
|
+
# List planning variable (vehicle routing)
|
|
69
|
+
@planning_entity
|
|
70
|
+
class Vehicle:
|
|
71
|
+
id: str
|
|
72
|
+
visits: list[str] = [] # List of visit IDs, assigned by solver
|
|
73
|
+
|
|
74
|
+
# Shadow variables (auto-updated when list changes)
|
|
75
|
+
@planning_entity
|
|
76
|
+
class Visit:
|
|
77
|
+
id: str
|
|
78
|
+
vehicle: str | None = None # @InverseRelationShadowVariable
|
|
79
|
+
index: int | None = None # @IndexShadowVariable
|
|
80
|
+
previous: str | None = None # @PreviousElementShadowVariable
|
|
81
|
+
next: str | None = None # @NextElementShadowVariable
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Constraint Streams
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from solverforge import (
|
|
88
|
+
equal, less_than, greater_than, overlapping, filtering,
|
|
89
|
+
count, sum_, min_, max_, to_list, to_set,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@constraint_provider
|
|
93
|
+
def constraints(factory):
|
|
94
|
+
return [
|
|
95
|
+
# forEach / forEachIncludingUnassigned / forEachUniquePair
|
|
96
|
+
factory.for_each(Lesson),
|
|
97
|
+
factory.for_each_including_unassigned(Lesson),
|
|
98
|
+
factory.for_each_unique_pair(Lesson, equal(lambda l: l.timeslot)),
|
|
99
|
+
|
|
100
|
+
# filter
|
|
101
|
+
factory.for_each(Lesson)
|
|
102
|
+
.filter(lambda l: l.room is not None),
|
|
103
|
+
|
|
104
|
+
# join with joiners
|
|
105
|
+
factory.for_each(Lesson)
|
|
106
|
+
.join(Room, equal(lambda l: l.room, lambda r: r.id)),
|
|
107
|
+
|
|
108
|
+
# ifExists / ifNotExists
|
|
109
|
+
factory.for_each(Lesson)
|
|
110
|
+
.if_exists(Conflict, equal(lambda l: l.id, lambda c: c.lesson_id)),
|
|
111
|
+
|
|
112
|
+
# groupBy with collectors
|
|
113
|
+
factory.for_each(Lesson)
|
|
114
|
+
.group_by(lambda l: l.room, count())
|
|
115
|
+
.filter(lambda room, cnt: cnt > 1)
|
|
116
|
+
.penalize(HardSoftScore.ONE_HARD, lambda room, cnt: cnt - 1),
|
|
117
|
+
|
|
118
|
+
# map / expand / flattenLast
|
|
119
|
+
factory.for_each(Vehicle)
|
|
120
|
+
.flatten_last(lambda v: v.visits),
|
|
121
|
+
]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### SolverManager (Multi-Problem Solving)
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from solverforge import SolverManager, HttpSolverService
|
|
128
|
+
|
|
129
|
+
service = HttpSolverService("http://localhost:8080")
|
|
130
|
+
manager = SolverManager(Timetable, define_constraints, service)
|
|
131
|
+
|
|
132
|
+
# Solve multiple problems concurrently
|
|
133
|
+
manager.solve("problem-1", problem1)
|
|
134
|
+
manager.solve("problem-2", problem2)
|
|
135
|
+
|
|
136
|
+
# Check solutions
|
|
137
|
+
solution = manager.get_best_solution("problem-1")
|
|
138
|
+
if solution:
|
|
139
|
+
print(f"Score: {solution.score}")
|
|
140
|
+
|
|
141
|
+
# Terminate
|
|
142
|
+
manager.terminate("problem-1")
|
|
143
|
+
manager.terminate_all()
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### ConstraintVerifier (Testing)
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from solverforge import ConstraintVerifier
|
|
150
|
+
|
|
151
|
+
verifier = ConstraintVerifier.build(Timetable, define_constraints)
|
|
152
|
+
|
|
153
|
+
# Test specific constraint
|
|
154
|
+
verifier.verify_that(lambda f: f.for_each(Lesson)...)
|
|
155
|
+
.given(lesson1, lesson2)
|
|
156
|
+
.penalizes_by(1)
|
|
157
|
+
|
|
158
|
+
# Test entire solution
|
|
159
|
+
verifier.verify_that()
|
|
160
|
+
.given_solution(solution)
|
|
161
|
+
.scores(HardSoftScore.of(-2, -10))
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Score Types
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from solverforge import (
|
|
168
|
+
SimpleScore, HardSoftScore, HardMediumSoftScore,
|
|
169
|
+
BendableScore, SimpleDecimalScore, HardSoftDecimalScore,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
score = HardSoftScore.of(-2, -15)
|
|
173
|
+
print(f"Hard: {score.hard_score}, Soft: {score.soft_score}")
|
|
174
|
+
print(f"Feasible: {score.is_feasible()}")
|
|
175
|
+
|
|
176
|
+
# Arithmetic
|
|
177
|
+
combined = score + HardSoftScore.of(0, -5)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Solver Configuration
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from solverforge import (
|
|
184
|
+
SolverFactory, SolverConfig, TerminationConfig,
|
|
185
|
+
EnvironmentMode, MoveThreadCount,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
solver = SolverFactory.create(Timetable, define_constraints)
|
|
189
|
+
.with_termination(
|
|
190
|
+
TerminationConfig()
|
|
191
|
+
.with_spent_limit("PT5M")
|
|
192
|
+
.with_unimproved_spent_limit("PT1M")
|
|
193
|
+
.with_best_score_feasible(True)
|
|
194
|
+
)
|
|
195
|
+
.with_environment_mode(EnvironmentMode.REPRODUCIBLE)
|
|
196
|
+
.with_random_seed(42)
|
|
197
|
+
.with_move_thread_count(MoveThreadCount.AUTO)
|
|
198
|
+
.build()
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Requirements
|
|
202
|
+
|
|
203
|
+
- Python 3.13+ (required for modern AST support)
|
|
204
|
+
- Java 21+ (for solver service, auto-started)
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
Apache-2.0
|
|
@@ -4,19 +4,17 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "solverforge"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Python bindings for SolverForge constraint solver"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "Apache-2.0" }
|
|
11
|
-
requires-python = ">=3.
|
|
11
|
+
requires-python = ">=3.13"
|
|
12
12
|
classifiers = [
|
|
13
13
|
"Programming Language :: Rust",
|
|
14
14
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
15
15
|
"Programming Language :: Python :: 3",
|
|
16
|
-
"Programming Language :: Python :: 3.10",
|
|
17
|
-
"Programming Language :: Python :: 3.11",
|
|
18
|
-
"Programming Language :: Python :: 3.12",
|
|
19
16
|
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Programming Language :: Python :: 3.14",
|
|
20
18
|
"Development Status :: 3 - Alpha",
|
|
21
19
|
"Intended Audience :: Developers",
|
|
22
20
|
"License :: OSI Approved :: Apache Software License",
|
|
@@ -32,7 +30,7 @@ Repository = "https://github.com/solverforge/solverforge"
|
|
|
32
30
|
Changelog = "https://github.com/solverforge/solverforge/blob/main/CHANGELOG.md"
|
|
33
31
|
|
|
34
32
|
[tool.maturin]
|
|
35
|
-
features = ["pyo3/extension-module", "pyo3/abi3-
|
|
33
|
+
features = ["pyo3/extension-module", "pyo3/abi3-py313"]
|
|
36
34
|
python-source = "python"
|
|
37
35
|
module-name = "solverforge._solverforge"
|
|
38
36
|
manifest-path = "solverforge-python/Cargo.toml"
|
|
@@ -57,6 +57,9 @@ from solverforge._solverforge import (
|
|
|
57
57
|
SolveResponse,
|
|
58
58
|
SolverFactory,
|
|
59
59
|
Solver,
|
|
60
|
+
SolverManager,
|
|
61
|
+
SolverJob,
|
|
62
|
+
SolverJobStatus,
|
|
60
63
|
# Constraint streams
|
|
61
64
|
ConstraintFactory,
|
|
62
65
|
UniConstraintStream,
|
|
@@ -131,6 +134,9 @@ __all__ = [
|
|
|
131
134
|
"SolveResponse",
|
|
132
135
|
"SolverFactory",
|
|
133
136
|
"Solver",
|
|
137
|
+
"SolverManager",
|
|
138
|
+
"SolverJob",
|
|
139
|
+
"SolverJobStatus",
|
|
134
140
|
# Constraint streams
|
|
135
141
|
"ConstraintFactory",
|
|
136
142
|
"UniConstraintStream",
|
{solverforge-0.2.6/solverforge-python → solverforge-0.3.0}/python/solverforge/solver/__init__.py
RENAMED
|
@@ -82,6 +82,9 @@ class SolverJob(Generic[Solution_, ProblemId_]):
|
|
|
82
82
|
self._final_best_solution_consumer(response.solution)
|
|
83
83
|
|
|
84
84
|
except Exception as e:
|
|
85
|
+
import logging
|
|
86
|
+
|
|
87
|
+
logging.getLogger(__name__).error("Solver failed: %s", e, exc_info=True)
|
|
85
88
|
with self._lock:
|
|
86
89
|
self._exception = e
|
|
87
90
|
self._status = SolverStatus.NOT_SOLVING
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# solverforge-core
|
|
2
|
+
|
|
3
|
+
Language-agnostic core library for SolverForge constraint solving.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This crate provides the foundation for SolverForge's constraint solving capabilities:
|
|
8
|
+
|
|
9
|
+
- **Value types** - `Value`, `ObjectHandle`, `FunctionHandle`
|
|
10
|
+
- **Score types** - `SimpleScore`, `HardSoftScore`, `HardMediumSoftScore`, `BendableScore` (+ decimal variants)
|
|
11
|
+
- **Domain modeling** - `PlanningAnnotation`, `DomainModel`, `DomainClass`, `FieldDescriptor`
|
|
12
|
+
- **Constraint streams** - `Constraint`, `ConstraintSet`, `StreamComponent`, `Joiner`, `Collector`
|
|
13
|
+
- **Expression DSL** - `Expr`, `Expression`, `NamedExpression` for type-safe constraint building
|
|
14
|
+
- **Solver** - `SolverBuilder`, `TypedSolver`, `SolverManager`, `SolverFactory`
|
|
15
|
+
- **WASM generation** - `WasmModuleBuilder`, `PredicateDefinition`
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```toml
|
|
20
|
+
[dependencies]
|
|
21
|
+
solverforge-core = "0.2"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Score Types
|
|
25
|
+
|
|
26
|
+
```rust
|
|
27
|
+
use solverforge_core::{HardSoftScore, HardMediumSoftScore, SimpleScore};
|
|
28
|
+
|
|
29
|
+
let score = HardSoftScore::of(-2, -15);
|
|
30
|
+
assert_eq!(score.hard_score(), -2);
|
|
31
|
+
assert!(score.is_feasible() == false);
|
|
32
|
+
|
|
33
|
+
let simple = SimpleScore::of(100);
|
|
34
|
+
let hms = HardMediumSoftScore::of(0, -5, -10);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Constraint Streams
|
|
38
|
+
|
|
39
|
+
```rust
|
|
40
|
+
use solverforge_core::{Constraint, StreamComponent, Joiner, WasmFunction, Collector};
|
|
41
|
+
|
|
42
|
+
let constraint = Constraint::new("Room conflict")
|
|
43
|
+
.with_stream(StreamComponent::for_each("Lesson"))
|
|
44
|
+
.with_stream(StreamComponent::join_with_joiners(
|
|
45
|
+
"Lesson",
|
|
46
|
+
vec![
|
|
47
|
+
Joiner::equal(WasmFunction::new("getRoom")),
|
|
48
|
+
Joiner::equal(WasmFunction::new("getTimeslot")),
|
|
49
|
+
],
|
|
50
|
+
))
|
|
51
|
+
.with_stream(StreamComponent::filter(WasmFunction::new("isDifferent")))
|
|
52
|
+
.with_stream(StreamComponent::penalize("1hard"));
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Expression DSL
|
|
56
|
+
|
|
57
|
+
```rust
|
|
58
|
+
use solverforge_core::{Expr, NamedExpression, IntoNamedExpression, StreamComponent};
|
|
59
|
+
use solverforge_core::wasm::FieldAccessExt;
|
|
60
|
+
|
|
61
|
+
// Build type-safe expressions
|
|
62
|
+
let has_room = Expr::is_not_null(Expr::param(0).get("Lesson", "room"))
|
|
63
|
+
.named_as("lesson_has_room");
|
|
64
|
+
|
|
65
|
+
// Use in stream components
|
|
66
|
+
let filter = StreamComponent::filter_expr(has_room);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### SolverManager (Multi-Problem Solving)
|
|
70
|
+
|
|
71
|
+
```rust
|
|
72
|
+
use solverforge_core::{SolverManager, HttpSolverService, TerminationConfig};
|
|
73
|
+
use std::sync::Arc;
|
|
74
|
+
|
|
75
|
+
let service = Arc::new(HttpSolverService::new("http://localhost:8080"));
|
|
76
|
+
let mut manager = SolverManager::<Timetable, String>::new(service)
|
|
77
|
+
.with_termination(TerminationConfig::new().with_spent_limit("PT5M"));
|
|
78
|
+
|
|
79
|
+
// Solve multiple problems concurrently
|
|
80
|
+
manager.solve("problem-1".to_string(), problem1)?;
|
|
81
|
+
manager.solve("problem-2".to_string(), problem2)?;
|
|
82
|
+
|
|
83
|
+
// Check solutions
|
|
84
|
+
if let Some(solution) = manager.get_best_solution(&"problem-1".to_string())? {
|
|
85
|
+
println!("Score: {:?}", solution.score());
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
manager.terminate_all();
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Shadow Variables
|
|
92
|
+
|
|
93
|
+
```rust
|
|
94
|
+
use solverforge_core::PlanningAnnotation;
|
|
95
|
+
|
|
96
|
+
// Available shadow variable types
|
|
97
|
+
let index = PlanningAnnotation::index_shadow("visits");
|
|
98
|
+
let next = PlanningAnnotation::next_element_shadow("visits");
|
|
99
|
+
let prev = PlanningAnnotation::previous_element_shadow("visits");
|
|
100
|
+
let anchor = PlanningAnnotation::anchor_shadow("chain");
|
|
101
|
+
let inverse = PlanningAnnotation::inverse_relation_shadow("vehicle", "visits");
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Architecture
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
solverforge-core
|
|
108
|
+
├── constraints/ # Constraint streams, joiners, collectors
|
|
109
|
+
├── domain/ # Domain model, annotations, shadow variables
|
|
110
|
+
├── solver/ # SolverBuilder, SolverManager, HTTP client
|
|
111
|
+
├── wasm/ # WASM module generation, expression compiler
|
|
112
|
+
├── score/ # Score types (Simple, HardSoft, etc.)
|
|
113
|
+
└── analysis/ # Score explanation, constraint matches
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
Apache-2.0
|