solverforge 0.6.7__tar.gz → 0.6.9__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.6.7 → solverforge-0.6.9}/Cargo.lock +1 -1
- {solverforge-0.6.7 → solverforge-0.6.9}/Cargo.toml +1 -1
- {solverforge-0.6.7 → solverforge-0.6.9}/PKG-INFO +23 -10
- {solverforge-0.6.7 → solverforge-0.6.9}/README.md +22 -9
- {solverforge-0.6.7 → solverforge-0.6.9}/pyproject.toml +1 -1
- {solverforge-0.6.7 → solverforge-0.6.9}/python/solverforge/__init__.py +3 -1
- {solverforge-0.6.7 → solverforge-0.6.9}/python/solverforge/decorators.py +15 -1
- {solverforge-0.6.7 → solverforge-0.6.9}/python/solverforge/fields.py +13 -3
- {solverforge-0.6.7 → solverforge-0.6.9}/src/schema/build.rs +18 -1
- {solverforge-0.6.7 → solverforge-0.6.9}/src/schema/parse.rs +20 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/schema/types.rs +4 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/state/entity_table.rs +5 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/state/marshal.rs +38 -1
- {solverforge-0.6.7 → solverforge-0.6.9}/src/state/solution.rs +1 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/LICENSE +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/python/solverforge/_native.pyi +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/python/solverforge/config.py +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/python/solverforge/console.py +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/python/solverforge/constraints.py +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/python/solverforge/errors.py +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/python/solverforge/events.py +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/python/solverforge/groups.py +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/python/solverforge/joiner.py +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/python/solverforge/manager.py +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/python/solverforge/model.py +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/python/solverforge/py.typed +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/python/solverforge/score.py +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/python/solverforge/solver.py +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/python/solverforge/typing.py +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/python/solverforge/ui.py +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/rust-toolchain.toml +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/bindings.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/callbacks/call.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/callbacks/mod.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/callbacks/registry.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/callbacks/threading.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/config.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/constraints/compiled.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/constraints/evaluate.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/constraints/incremental.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/constraints/list_precedence.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/constraints/matches.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/constraints/mod.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/constraints/state.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/constraints/stream_plan.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/descriptor/extractor.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/descriptor/mod.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/descriptor/variable.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/error.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/intern.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/lib.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/manager/candidate_trace.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/manager/events.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/manager/jobs.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/manager/mod.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/manager/provenance.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/proxy/entity.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/proxy/list.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/proxy/mod.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/proxy/solution.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/runtime/candidate_metric.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/runtime/dynamic_assignment_group.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/runtime/dynamic_provider.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/runtime/list_slots.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/runtime/mod.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/runtime/scalar_slots.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/schema/compiled.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/schema/mod.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/schema/python.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/schema/runtime_plan.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/schema/validate.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/score.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/solver/api.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/solver/console.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/solver/mod.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/solver/progress.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/solver/result.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/solver/solvable.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/state/callback_view.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/state/clone.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/state/mod.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/state/variables.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/ui.rs +0 -0
- {solverforge-0.6.7 → solverforge-0.6.9}/src/value.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: solverforge
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.9
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -51,12 +51,12 @@ requirements, and other APIs that are not part of this package.
|
|
|
51
51
|
|
|
52
52
|
## Installation
|
|
53
53
|
|
|
54
|
-
The package version prepared by this checkout is `solverforge` `0.6.
|
|
54
|
+
The package version prepared by this checkout is `solverforge` `0.6.9`, targeting
|
|
55
55
|
the published and registry-locked SolverForge Rust `0.19.7` dependency base.
|
|
56
56
|
Install the matching package after its Python release with:
|
|
57
57
|
|
|
58
58
|
```sh
|
|
59
|
-
python3.14 -m pip install "solverforge==0.6.
|
|
59
|
+
python3.14 -m pip install "solverforge==0.6.9"
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
The installable wheel contains the core `solverforge` package, native extension,
|
|
@@ -76,12 +76,12 @@ make deliveries-run PORT=7861
|
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
The same source-checkout examples can be run against the matching published
|
|
79
|
-
package once `0.6.
|
|
79
|
+
package once `0.6.9` is released, without `PYTHONPATH` or an editable checkout:
|
|
80
80
|
|
|
81
81
|
```sh
|
|
82
82
|
python3.14 -m venv .venv-examples
|
|
83
83
|
. .venv-examples/bin/activate
|
|
84
|
-
python -m pip install "solverforge[examples]==0.6.
|
|
84
|
+
python -m pip install "solverforge[examples]==0.6.9"
|
|
85
85
|
python examples/nqueens.py
|
|
86
86
|
python -m examples.solverforge_hospital
|
|
87
87
|
python -m examples.solverforge_deliveries
|
|
@@ -104,16 +104,19 @@ from solverforge import (
|
|
|
104
104
|
Solver,
|
|
105
105
|
constraint_provider,
|
|
106
106
|
planning_entity,
|
|
107
|
+
planning_pin,
|
|
107
108
|
planning_solution,
|
|
108
109
|
planning_variable,
|
|
109
110
|
)
|
|
110
111
|
|
|
111
112
|
@planning_entity
|
|
112
113
|
class Shift:
|
|
114
|
+
pinned = planning_pin()
|
|
113
115
|
nurse = planning_variable(value_range_provider="nurses", allows_unassigned=True)
|
|
114
116
|
|
|
115
117
|
def __init__(self, required: bool = True, nurse: int | None = None) -> None:
|
|
116
118
|
self.required = required
|
|
119
|
+
self.pinned = False
|
|
117
120
|
self.nurse = nurse
|
|
118
121
|
|
|
119
122
|
@constraint_provider
|
|
@@ -243,11 +246,21 @@ Run `make help` for focused targets such as `make test-hospital`,
|
|
|
243
246
|
per-query Python callbacks. Provider-backed value ranges are imported once per
|
|
244
247
|
variable and shared across rows in Rust-owned state. Row candidate callbacks
|
|
245
248
|
remain row-specific and define move legality throughout construction and local
|
|
246
|
-
search, including assignment-group swaps and rematches.
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
input
|
|
249
|
+
search, including assignment-group swaps and rematches.
|
|
250
|
+
- `planning_pin()` declares the entity attribute that holds an entity's pinned
|
|
251
|
+
flag. The declared attribute must hold a `bool` on every instance; an unset or
|
|
252
|
+
non-`bool` value is rejected at import rather than read as "not pinned". A
|
|
253
|
+
pinned entity keeps its input planning state: its genuine scalar value and the
|
|
254
|
+
elements its list variable owns survive construction, local search,
|
|
255
|
+
ruin/recreate, and exhaustive search, while every unpinned entity in the same
|
|
256
|
+
solve is still constructed and searched. Pinning preserves input state, it does
|
|
257
|
+
not exempt a row from the mandatory-completion gate: a pinned required scalar
|
|
258
|
+
that nothing assigned fails the solve, and a pinned `allows_unassigned` scalar
|
|
259
|
+
stays unassigned. It is a declared field, not a callback — there is no dynamic
|
|
260
|
+
`is_pinned()` hook, because the compiled runtime reads a resolved per-row flag.
|
|
261
|
+
The variable-level `planning_variable(pinning=True)` flag stays rejected with
|
|
262
|
+
`ModelValidationError`: SolverForge pins whole entities, and that flag names a
|
|
263
|
+
route the framework does not have.
|
|
251
264
|
- `scalar_assignment_group(...)` declares assignment-aware scalar groups for
|
|
252
265
|
grouped scalar local search and assignment-group construction. Group metadata
|
|
253
266
|
covers required entities, capacity keys, assignment rules, ordering callbacks,
|
|
@@ -20,12 +20,12 @@ requirements, and other APIs that are not part of this package.
|
|
|
20
20
|
|
|
21
21
|
## Installation
|
|
22
22
|
|
|
23
|
-
The package version prepared by this checkout is `solverforge` `0.6.
|
|
23
|
+
The package version prepared by this checkout is `solverforge` `0.6.9`, targeting
|
|
24
24
|
the published and registry-locked SolverForge Rust `0.19.7` dependency base.
|
|
25
25
|
Install the matching package after its Python release with:
|
|
26
26
|
|
|
27
27
|
```sh
|
|
28
|
-
python3.14 -m pip install "solverforge==0.6.
|
|
28
|
+
python3.14 -m pip install "solverforge==0.6.9"
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
The installable wheel contains the core `solverforge` package, native extension,
|
|
@@ -45,12 +45,12 @@ make deliveries-run PORT=7861
|
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
The same source-checkout examples can be run against the matching published
|
|
48
|
-
package once `0.6.
|
|
48
|
+
package once `0.6.9` is released, without `PYTHONPATH` or an editable checkout:
|
|
49
49
|
|
|
50
50
|
```sh
|
|
51
51
|
python3.14 -m venv .venv-examples
|
|
52
52
|
. .venv-examples/bin/activate
|
|
53
|
-
python -m pip install "solverforge[examples]==0.6.
|
|
53
|
+
python -m pip install "solverforge[examples]==0.6.9"
|
|
54
54
|
python examples/nqueens.py
|
|
55
55
|
python -m examples.solverforge_hospital
|
|
56
56
|
python -m examples.solverforge_deliveries
|
|
@@ -73,16 +73,19 @@ from solverforge import (
|
|
|
73
73
|
Solver,
|
|
74
74
|
constraint_provider,
|
|
75
75
|
planning_entity,
|
|
76
|
+
planning_pin,
|
|
76
77
|
planning_solution,
|
|
77
78
|
planning_variable,
|
|
78
79
|
)
|
|
79
80
|
|
|
80
81
|
@planning_entity
|
|
81
82
|
class Shift:
|
|
83
|
+
pinned = planning_pin()
|
|
82
84
|
nurse = planning_variable(value_range_provider="nurses", allows_unassigned=True)
|
|
83
85
|
|
|
84
86
|
def __init__(self, required: bool = True, nurse: int | None = None) -> None:
|
|
85
87
|
self.required = required
|
|
88
|
+
self.pinned = False
|
|
86
89
|
self.nurse = nurse
|
|
87
90
|
|
|
88
91
|
@constraint_provider
|
|
@@ -212,11 +215,21 @@ Run `make help` for focused targets such as `make test-hospital`,
|
|
|
212
215
|
per-query Python callbacks. Provider-backed value ranges are imported once per
|
|
213
216
|
variable and shared across rows in Rust-owned state. Row candidate callbacks
|
|
214
217
|
remain row-specific and define move legality throughout construction and local
|
|
215
|
-
search, including assignment-group swaps and rematches.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
input
|
|
218
|
+
search, including assignment-group swaps and rematches.
|
|
219
|
+
- `planning_pin()` declares the entity attribute that holds an entity's pinned
|
|
220
|
+
flag. The declared attribute must hold a `bool` on every instance; an unset or
|
|
221
|
+
non-`bool` value is rejected at import rather than read as "not pinned". A
|
|
222
|
+
pinned entity keeps its input planning state: its genuine scalar value and the
|
|
223
|
+
elements its list variable owns survive construction, local search,
|
|
224
|
+
ruin/recreate, and exhaustive search, while every unpinned entity in the same
|
|
225
|
+
solve is still constructed and searched. Pinning preserves input state, it does
|
|
226
|
+
not exempt a row from the mandatory-completion gate: a pinned required scalar
|
|
227
|
+
that nothing assigned fails the solve, and a pinned `allows_unassigned` scalar
|
|
228
|
+
stays unassigned. It is a declared field, not a callback — there is no dynamic
|
|
229
|
+
`is_pinned()` hook, because the compiled runtime reads a resolved per-row flag.
|
|
230
|
+
The variable-level `planning_variable(pinning=True)` flag stays rejected with
|
|
231
|
+
`ModelValidationError`: SolverForge pins whole entities, and that flag names a
|
|
232
|
+
route the framework does not have.
|
|
220
233
|
- `scalar_assignment_group(...)` declares assignment-aware scalar groups for
|
|
221
234
|
grouped scalar local search and assignment-group construction. Group metadata
|
|
222
235
|
covers required entities, capacity keys, assignment rules, ordering callbacks,
|
|
@@ -28,6 +28,7 @@ from .fields import (
|
|
|
28
28
|
SolutionField,
|
|
29
29
|
planning_id,
|
|
30
30
|
planning_list_variable,
|
|
31
|
+
planning_pin,
|
|
31
32
|
planning_variable,
|
|
32
33
|
)
|
|
33
34
|
from .groups import (
|
|
@@ -40,7 +41,7 @@ from .manager import JobHandle, SolverManager
|
|
|
40
41
|
from .score import HardMediumSoftScore, HardSoftDecimalScore, HardSoftScore, SoftScore
|
|
41
42
|
from .solver import Solver
|
|
42
43
|
|
|
43
|
-
__version__ = "0.6.
|
|
44
|
+
__version__ = "0.6.9"
|
|
44
45
|
|
|
45
46
|
__all__ = [
|
|
46
47
|
"CallbackError",
|
|
@@ -78,6 +79,7 @@ __all__ = [
|
|
|
78
79
|
"planning_entity",
|
|
79
80
|
"planning_id",
|
|
80
81
|
"planning_list_variable",
|
|
82
|
+
"planning_pin",
|
|
81
83
|
"planning_solution",
|
|
82
84
|
"planning_variable",
|
|
83
85
|
"problem_fact",
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
4
|
from typing import Any, TypeVar
|
|
5
5
|
|
|
6
|
+
from .errors import ModelValidationError
|
|
6
7
|
from .fields import (
|
|
7
8
|
CapacityRouteFeasibility,
|
|
8
9
|
EntityCallback,
|
|
@@ -22,8 +23,11 @@ T = TypeVar("T", bound=type[object])
|
|
|
22
23
|
|
|
23
24
|
def _collect_fields(cls: type[object]) -> list[dict[str, object]]:
|
|
24
25
|
fields: list[dict[str, object]] = []
|
|
26
|
+
pin_fields: list[str] = []
|
|
25
27
|
for name, value in vars(cls).items():
|
|
26
28
|
if isinstance(value, PlanningField):
|
|
29
|
+
if value.metadata.kind == "planning_pin":
|
|
30
|
+
pin_fields.append(name)
|
|
27
31
|
fields.append(
|
|
28
32
|
{
|
|
29
33
|
"name": name,
|
|
@@ -51,6 +55,9 @@ def _collect_fields(cls: type[object]) -> list[dict[str, object]]:
|
|
|
51
55
|
"list_metadata": _list_metadata(value.metadata.list_metadata),
|
|
52
56
|
}
|
|
53
57
|
)
|
|
58
|
+
if len(pin_fields) > 1:
|
|
59
|
+
msg = f"{cls.__name__} declares more than one planning_pin field"
|
|
60
|
+
raise ModelValidationError(msg)
|
|
54
61
|
return fields
|
|
55
62
|
|
|
56
63
|
|
|
@@ -133,12 +140,19 @@ def planning_entity(cls: T) -> T:
|
|
|
133
140
|
|
|
134
141
|
|
|
135
142
|
def problem_fact(cls: T) -> T:
|
|
143
|
+
fields = _collect_fields(cls)
|
|
144
|
+
if any(field["kind"] == "planning_pin" for field in fields):
|
|
145
|
+
msg = (
|
|
146
|
+
f"{cls.__name__} is a problem fact; planning_pin is only valid on a "
|
|
147
|
+
"planning entity"
|
|
148
|
+
)
|
|
149
|
+
raise ModelValidationError(msg)
|
|
136
150
|
setattr(
|
|
137
151
|
cls,
|
|
138
152
|
"__solverforge_fact__",
|
|
139
153
|
{
|
|
140
154
|
"type_name": cls.__name__,
|
|
141
|
-
"fields":
|
|
155
|
+
"fields": fields,
|
|
142
156
|
},
|
|
143
157
|
)
|
|
144
158
|
return cls
|
|
@@ -181,6 +181,17 @@ def planning_id() -> PlanningField:
|
|
|
181
181
|
return PlanningField(FieldMetadata(kind="planning_id"))
|
|
182
182
|
|
|
183
183
|
|
|
184
|
+
def planning_pin() -> PlanningField:
|
|
185
|
+
"""Declares the entity attribute that holds this entity's pinned flag.
|
|
186
|
+
|
|
187
|
+
The declared attribute must hold a ``bool`` on every instance. A pinned
|
|
188
|
+
entity keeps its input planning state: its genuine scalar value and its
|
|
189
|
+
list ownership survive construction and search.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
return PlanningField(FieldMetadata(kind="planning_pin"))
|
|
193
|
+
|
|
194
|
+
|
|
184
195
|
def planning_variable(
|
|
185
196
|
*,
|
|
186
197
|
value_range_provider: str,
|
|
@@ -195,9 +206,8 @@ def planning_variable(
|
|
|
195
206
|
if pinning:
|
|
196
207
|
msg = (
|
|
197
208
|
"planning_variable(pinning=True) is not supported: SolverForge pins a whole "
|
|
198
|
-
"planning entity
|
|
199
|
-
"
|
|
200
|
-
"value by restricting candidate_values instead."
|
|
209
|
+
"planning entity, not one variable. Declare an entity pin field with "
|
|
210
|
+
"planning_pin() instead."
|
|
201
211
|
)
|
|
202
212
|
raise ModelValidationError(msg)
|
|
203
213
|
value_candidates = _resolve_metadata_source(
|
|
@@ -1,14 +1,26 @@
|
|
|
1
|
-
use std::any::TypeId;
|
|
1
|
+
use std::any::{Any, TypeId};
|
|
2
2
|
|
|
3
3
|
use solverforge_bridge::{EntityClassId, VariableId};
|
|
4
4
|
use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, VariableDescriptor};
|
|
5
5
|
|
|
6
6
|
use crate::descriptor::extractor::DynamicEntityExtractor;
|
|
7
7
|
use crate::intern::intern;
|
|
8
|
+
use crate::state::entity_table::DynamicEntityRow;
|
|
8
9
|
use crate::state::PyDynamicSolution;
|
|
9
10
|
|
|
10
11
|
use super::DynamicSchema;
|
|
11
12
|
|
|
13
|
+
/// The one entity pin predicate for every compiled Python schema.
|
|
14
|
+
///
|
|
15
|
+
/// The declaration is resolved per row at import, so this stays a capture-free
|
|
16
|
+
/// function over Rust-owned state: no thread-local slot lookup, no Python
|
|
17
|
+
/// callback, and no per-candidate reinterpretation of the declared attribute.
|
|
18
|
+
fn pinned_row_predicate(entity: &dyn Any) -> bool {
|
|
19
|
+
entity
|
|
20
|
+
.downcast_ref::<DynamicEntityRow>()
|
|
21
|
+
.is_some_and(|row| row.pinned)
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
pub fn solution_descriptor(schema: &DynamicSchema) -> SolutionDescriptor {
|
|
13
25
|
let mut descriptor = SolutionDescriptor::new(
|
|
14
26
|
intern(schema.solution_type.clone()),
|
|
@@ -37,6 +49,11 @@ pub fn solution_descriptor(schema: &DynamicSchema) -> SolutionDescriptor {
|
|
|
37
49
|
.with_logical_id(VariableId(variable_index));
|
|
38
50
|
entity_descriptor = entity_descriptor.with_variable(descriptor_variable);
|
|
39
51
|
}
|
|
52
|
+
if let Some(pin_field) = entity.pin_field.as_deref() {
|
|
53
|
+
entity_descriptor = entity_descriptor
|
|
54
|
+
.with_pin_field(intern(pin_field.to_string()))
|
|
55
|
+
.with_pin_predicate(pinned_row_predicate);
|
|
56
|
+
}
|
|
40
57
|
descriptor = descriptor.with_entity(entity_descriptor);
|
|
41
58
|
}
|
|
42
59
|
descriptor
|
|
@@ -34,9 +34,20 @@ pub fn parse_schema(schema: &Bound<'_, PyDict>) -> PyResult<DynamicSchema> {
|
|
|
34
34
|
.ok_or_else(|| py_err(format!("entity `{type_name}` is missing `fields`")))?;
|
|
35
35
|
let fields = fields_any.cast::<PyList>()?;
|
|
36
36
|
let mut variables = Vec::new();
|
|
37
|
+
let mut pin_field: Option<String> = None;
|
|
37
38
|
for field_any in fields.iter() {
|
|
38
39
|
let field = field_any.cast::<PyDict>()?;
|
|
39
40
|
let kind = required_str(field, "kind")?;
|
|
41
|
+
if kind == "planning_pin" {
|
|
42
|
+
let name = required_non_empty_str(field, "name")?;
|
|
43
|
+
if pin_field.is_some() {
|
|
44
|
+
return Err(py_err(format!(
|
|
45
|
+
"entity `{type_name}` declares more than one planning_pin field"
|
|
46
|
+
)));
|
|
47
|
+
}
|
|
48
|
+
pin_field = Some(name);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
40
51
|
if kind == "planning_variable" || kind == "planning_list_variable" {
|
|
41
52
|
let list_metadata = if kind == "planning_list_variable" {
|
|
42
53
|
Some(parse_list_metadata(field)?)
|
|
@@ -98,10 +109,19 @@ pub fn parse_schema(schema: &Bound<'_, PyDict>) -> PyResult<DynamicSchema> {
|
|
|
98
109
|
});
|
|
99
110
|
}
|
|
100
111
|
}
|
|
112
|
+
if let Some(pin_field) = pin_field.as_deref() {
|
|
113
|
+
if variables.iter().any(|variable| variable.name == pin_field) {
|
|
114
|
+
return Err(py_err(format!(
|
|
115
|
+
"entity `{type_name}` declares `{pin_field}` as both a planning \
|
|
116
|
+
variable and a planning_pin field"
|
|
117
|
+
)));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
101
120
|
entities.push(EntitySchema {
|
|
102
121
|
type_name,
|
|
103
122
|
collection,
|
|
104
123
|
variables,
|
|
124
|
+
pin_field,
|
|
105
125
|
});
|
|
106
126
|
}
|
|
107
127
|
let facts = parse_facts(schema)?;
|
|
@@ -7,6 +7,10 @@ pub struct EntitySchema {
|
|
|
7
7
|
pub type_name: String,
|
|
8
8
|
pub collection: String,
|
|
9
9
|
pub variables: Vec<VariableSchema>,
|
|
10
|
+
/// The declared entity attribute holding this entity's pinned flag. It is
|
|
11
|
+
/// read once per instance at import and enforced by the compiled
|
|
12
|
+
/// SolverForge runtime, never by wrapper-owned phase code.
|
|
13
|
+
pub pin_field: Option<String>,
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
#[derive(Debug)]
|
|
@@ -76,6 +76,11 @@ pub struct DynamicEntityRow {
|
|
|
76
76
|
pub scalar_values: Vec<Option<usize>>,
|
|
77
77
|
pub candidate_values: Vec<Option<Arc<[usize]>>>,
|
|
78
78
|
pub list_values: Vec<Option<Vec<usize>>>,
|
|
79
|
+
/// The entity's declared pinned flag, resolved once at import. It is the
|
|
80
|
+
/// only input the compiled runtime reads through the descriptor's pin
|
|
81
|
+
/// predicate. It is a plain row field, so every copy of a row that clones
|
|
82
|
+
/// (candidate, snapshot, or detached solution) carries the resolved value.
|
|
83
|
+
pub pinned: bool,
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
impl DynamicEntityRow {
|
|
@@ -2,7 +2,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
|
|
2
2
|
use std::sync::Arc;
|
|
3
3
|
|
|
4
4
|
use pyo3::prelude::*;
|
|
5
|
-
use pyo3::types::{PyDict, PyInt, PyList, PyString};
|
|
5
|
+
use pyo3::types::{PyBool, PyDict, PyInt, PyList, PyString};
|
|
6
6
|
use solverforge_config::SolverConfig;
|
|
7
7
|
|
|
8
8
|
use crate::schema::runtime_plan::CompiledRuntimePlan;
|
|
@@ -116,6 +116,9 @@ pub fn import_solution(
|
|
|
116
116
|
Some(&row_metadata_fields),
|
|
117
117
|
)?;
|
|
118
118
|
import_named_fields(&item, &extra_field_names, &mut row)?;
|
|
119
|
+
if let Some(pin_field) = entity.pin_field.as_deref() {
|
|
120
|
+
row.pinned = import_pin_value(&item, pin_field, &entity.type_name)?;
|
|
121
|
+
}
|
|
119
122
|
rows.push(row);
|
|
120
123
|
objects.push(item.unbind());
|
|
121
124
|
}
|
|
@@ -1176,6 +1179,40 @@ fn import_named_fields(
|
|
|
1176
1179
|
Ok(())
|
|
1177
1180
|
}
|
|
1178
1181
|
|
|
1182
|
+
/// Reads one entity's declared pinned flag.
|
|
1183
|
+
///
|
|
1184
|
+
/// The declaration is a contract on the instance, not a hint: an unset, missing,
|
|
1185
|
+
/// or non-`bool` value is an error rather than an implicit "not pinned", because
|
|
1186
|
+
/// a silently unpinned declaration would look identical to a working one while
|
|
1187
|
+
/// preserving nothing. The declared attribute is a `planning_pin()` descriptor,
|
|
1188
|
+
/// so an instance that never sets it reads back as `None` rather than raising.
|
|
1189
|
+
fn import_pin_value(item: &Bound<'_, PyAny>, pin_field: &str, type_name: &str) -> PyResult<bool> {
|
|
1190
|
+
let value = match item.getattr(pin_field) {
|
|
1191
|
+
Ok(value) => value,
|
|
1192
|
+
Err(cause) => {
|
|
1193
|
+
// Keep the original failure as the cause: a declared attribute that
|
|
1194
|
+
// exists but raises is a different problem from one that is absent,
|
|
1195
|
+
// and the operator needs the original traceback to tell them apart.
|
|
1196
|
+
let error = crate::error::py_err(format!(
|
|
1197
|
+
"pinned entity `{type_name}` has no readable `{pin_field}` attribute"
|
|
1198
|
+
));
|
|
1199
|
+
error.set_cause(item.py(), Some(cause));
|
|
1200
|
+
return Err(error);
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
if value.is_none() {
|
|
1204
|
+
return Err(crate::error::py_err(format!(
|
|
1205
|
+
"pinned entity `{type_name}` must set a bool `{pin_field}` attribute per instance"
|
|
1206
|
+
)));
|
|
1207
|
+
}
|
|
1208
|
+
if !value.is_instance_of::<PyBool>() {
|
|
1209
|
+
return Err(crate::error::py_err(format!(
|
|
1210
|
+
"pinned entity `{type_name}` attribute `{pin_field}` must be a bool"
|
|
1211
|
+
)));
|
|
1212
|
+
}
|
|
1213
|
+
value.extract::<bool>()
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1179
1216
|
fn is_read_only_property(
|
|
1180
1217
|
item: &Bound<'_, PyAny>,
|
|
1181
1218
|
name: &str,
|
|
@@ -741,6 +741,7 @@ mod tests {
|
|
|
741
741
|
entities: vec![EntitySchema {
|
|
742
742
|
type_name: "Route".to_string(),
|
|
743
743
|
collection: "routes".to_string(),
|
|
744
|
+
pin_field: None,
|
|
744
745
|
variables: vec![VariableSchema {
|
|
745
746
|
name: "visits".to_string(),
|
|
746
747
|
storage_name: "__solverforge_visits".to_string(),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|