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.
Files changed (119) hide show
  1. {solverforge-0.2.6 → solverforge-0.3.0}/Cargo.lock +15 -5
  2. {solverforge-0.2.6 → solverforge-0.3.0}/Cargo.toml +1 -1
  3. solverforge-0.3.0/PKG-INFO +232 -0
  4. solverforge-0.3.0/README.md +208 -0
  5. {solverforge-0.2.6 → solverforge-0.3.0}/pyproject.toml +4 -6
  6. {solverforge-0.2.6 → solverforge-0.3.0}/python/solverforge/__init__.py +6 -0
  7. {solverforge-0.2.6/solverforge-python → solverforge-0.3.0}/python/solverforge/solver/__init__.py +3 -0
  8. solverforge-0.3.0/solverforge-core/README.md +118 -0
  9. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/analysis/manager.rs +18 -24
  10. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/bridge.rs +2 -2
  11. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/constraints/collectors.rs +294 -0
  12. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/constraints/constraint.rs +227 -1
  13. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/constraints/joiners.rs +36 -1
  14. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/constraints/mod.rs +2 -0
  15. solverforge-0.3.0/solverforge-core/src/constraints/named_expr.rs +221 -0
  16. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/constraints/stream.rs +462 -1
  17. solverforge-0.3.0/solverforge-core/src/domain/annotations.rs +1057 -0
  18. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/domain/class.rs +17 -26
  19. solverforge-0.3.0/solverforge-core/src/domain/listener.rs +981 -0
  20. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/domain/mod.rs +6 -0
  21. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/domain/model.rs +134 -55
  22. solverforge-0.3.0/solverforge-core/src/entity_context.rs +193 -0
  23. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/lib.rs +15 -10
  24. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/score/hard_soft.rs +77 -0
  25. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/solver/builder.rs +55 -106
  26. solverforge-0.3.0/solverforge-core/src/solver/change.rs +1139 -0
  27. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/solver/factory.rs +3 -10
  28. solverforge-0.3.0/solverforge-core/src/solver/manager.rs +253 -0
  29. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/solver/mod.rs +9 -2
  30. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/solver/request.rs +42 -61
  31. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/solver/termination.rs +24 -0
  32. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/traits.rs +34 -41
  33. solverforge-0.2.6/solverforge-core/src/wasm/expr_builder.rs → solverforge-0.3.0/solverforge-core/src/wasm/expression/builder.rs +142 -4
  34. solverforge-0.3.0/solverforge-core/src/wasm/expression/mod.rs +349 -0
  35. solverforge-0.3.0/solverforge-core/src/wasm/expression/substitution.rs +199 -0
  36. solverforge-0.3.0/solverforge-core/src/wasm/expression/tests.rs +761 -0
  37. solverforge-0.3.0/solverforge-core/src/wasm/generator/codegen.rs +317 -0
  38. solverforge-0.3.0/solverforge-core/src/wasm/generator/compiler.rs +801 -0
  39. solverforge-0.3.0/solverforge-core/src/wasm/generator/mod.rs +469 -0
  40. solverforge-0.3.0/solverforge-core/src/wasm/generator/predicate.rs +38 -0
  41. solverforge-0.3.0/solverforge-core/src/wasm/generator/tests.rs +280 -0
  42. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/wasm/host_functions.rs +62 -0
  43. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/wasm/memory.rs +2 -2
  44. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/wasm/mod.rs +2 -6
  45. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/Cargo.toml +2 -1
  46. solverforge-0.3.0/solverforge-python/README.md +208 -0
  47. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/python/solverforge/__init__.py +6 -0
  48. {solverforge-0.2.6 → solverforge-0.3.0/solverforge-python}/python/solverforge/solver/__init__.py +3 -0
  49. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/src/annotations.rs +18 -7
  50. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/src/bridge.rs +37 -5
  51. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/src/collectors.rs +202 -103
  52. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/src/decorators.rs +229 -67
  53. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/src/joiners.rs +6 -132
  54. solverforge-0.3.0/solverforge-python/src/lambda_analyzer/ast_convert/guards.rs +116 -0
  55. solverforge-0.3.0/solverforge-python/src/lambda_analyzer/ast_convert/inference.rs +549 -0
  56. solverforge-0.3.0/solverforge-python/src/lambda_analyzer/ast_convert/mod.rs +1035 -0
  57. solverforge-0.3.0/solverforge-python/src/lambda_analyzer/ast_convert/types.rs +65 -0
  58. solverforge-0.3.0/solverforge-python/src/lambda_analyzer/conditionals.rs +491 -0
  59. solverforge-0.3.0/solverforge-python/src/lambda_analyzer/constants.rs +370 -0
  60. solverforge-0.3.0/solverforge-python/src/lambda_analyzer/lambda_parsing.rs +183 -0
  61. solverforge-0.3.0/solverforge-python/src/lambda_analyzer/loops.rs +729 -0
  62. solverforge-0.3.0/solverforge-python/src/lambda_analyzer/method_analysis.rs +248 -0
  63. solverforge-0.3.0/solverforge-python/src/lambda_analyzer/mod.rs +269 -0
  64. solverforge-0.3.0/solverforge-python/src/lambda_analyzer/patterns.rs +36 -0
  65. solverforge-0.3.0/solverforge-python/src/lambda_analyzer/registry.rs +155 -0
  66. solverforge-0.3.0/solverforge-python/src/lambda_analyzer/sequential.rs +353 -0
  67. solverforge-0.3.0/solverforge-python/src/lambda_analyzer/tests.rs +428 -0
  68. solverforge-0.3.0/solverforge-python/src/lambda_analyzer/type_inference.rs +447 -0
  69. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/src/lib.rs +7 -1
  70. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/src/service.rs +26 -0
  71. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/src/solver.rs +681 -151
  72. solverforge-0.3.0/solverforge-python/src/stream.rs +4089 -0
  73. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-service/Cargo.toml +1 -1
  74. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-service/src/jar.rs +120 -18
  75. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-service/src/lib.rs +1 -1
  76. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-service/src/service.rs +74 -7
  77. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-service/src/util.rs +17 -42
  78. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-service/tests/employee_scheduling_integration.rs +82 -123
  79. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-service/tests/solve_integration.rs +2 -2
  80. solverforge-0.2.6/PKG-INFO +0 -83
  81. solverforge-0.2.6/README.md +0 -57
  82. solverforge-0.2.6/solverforge-core/README.md +0 -64
  83. solverforge-0.2.6/solverforge-core/src/domain/annotations.rs +0 -305
  84. solverforge-0.2.6/solverforge-core/src/wasm/expression.rs +0 -614
  85. solverforge-0.2.6/solverforge-core/src/wasm/generator.rs +0 -1617
  86. solverforge-0.2.6/solverforge-python/README.md +0 -57
  87. solverforge-0.2.6/solverforge-python/src/lambda_analyzer.rs +0 -1578
  88. solverforge-0.2.6/solverforge-python/src/stream.rs +0 -2017
  89. {solverforge-0.2.6 → solverforge-0.3.0}/python/solverforge/solver/config.py +0 -0
  90. {solverforge-0.2.6 → solverforge-0.3.0}/python/solverforge/solver/domain.py +0 -0
  91. {solverforge-0.2.6 → solverforge-0.3.0}/python/solverforge/solver/score.py +0 -0
  92. {solverforge-0.2.6 → solverforge-0.3.0}/python/solverforge/test/__init__.py +0 -0
  93. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/Cargo.toml +0 -0
  94. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/analysis/explanation.rs +0 -0
  95. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/analysis/mod.rs +0 -0
  96. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/domain/constraint_config.rs +0 -0
  97. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/domain/shadow.rs +0 -0
  98. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/error.rs +0 -0
  99. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/handles.rs +0 -0
  100. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/score/bendable.rs +0 -0
  101. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/score/hard_soft_decimal.rs +0 -0
  102. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/score/mod.rs +0 -0
  103. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/score/simple.rs +0 -0
  104. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/score/simple_decimal.rs +0 -0
  105. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/solver/client.rs +0 -0
  106. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/solver/config.rs +0 -0
  107. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/solver/environment.rs +0 -0
  108. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/solver/response.rs +0 -0
  109. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-core/src/value.rs +0 -0
  110. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/python/solverforge/solver/config.py +0 -0
  111. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/python/solverforge/solver/domain.py +0 -0
  112. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/python/solverforge/solver/score.py +0 -0
  113. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/python/solverforge/test/__init__.py +0 -0
  114. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/src/analysis.rs +0 -0
  115. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-python/src/score.rs +0 -0
  116. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-service/README.md +0 -0
  117. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-service/src/config.rs +0 -0
  118. {solverforge-0.2.6 → solverforge-0.3.0}/solverforge-service/src/error.rs +0 -0
  119. {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.2.6"
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.2.6"
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.2.6"
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.2.6"
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.2.6"
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"
@@ -3,7 +3,7 @@ members = ["solverforge-core", "solverforge-python", "solverforge-service"]
3
3
  resolver = "2"
4
4
 
5
5
  [workspace.package]
6
- version = "0.2.6"
6
+ version = "0.3.0"
7
7
  edition = "2021"
8
8
  license = "Apache-2.0"
9
9
  repository = "https://github.com/solverforge/solverforge"
@@ -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.2.6"
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.10"
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-py310"]
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",
@@ -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