solverforge 0.6.5__tar.gz → 0.6.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {solverforge-0.6.5 → solverforge-0.6.6}/Cargo.lock +13 -13
- {solverforge-0.6.5 → solverforge-0.6.6}/Cargo.toml +8 -8
- {solverforge-0.6.5 → solverforge-0.6.6}/PKG-INFO +27 -21
- {solverforge-0.6.5 → solverforge-0.6.6}/README.md +26 -20
- {solverforge-0.6.5 → solverforge-0.6.6}/pyproject.toml +1 -1
- {solverforge-0.6.5 → solverforge-0.6.6}/python/solverforge/__init__.py +1 -1
- {solverforge-0.6.5 → solverforge-0.6.6}/python/solverforge/groups.py +21 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/python/solverforge/model.py +10 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/runtime/dynamic_assignment_group.rs +79 -1
- {solverforge-0.6.5 → solverforge-0.6.6}/src/runtime/scalar_slots.rs +7 -1
- {solverforge-0.6.5 → solverforge-0.6.6}/src/schema/parse.rs +1 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/schema/types.rs +1 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/schema/validate.rs +22 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/state/marshal.rs +34 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/state/solution.rs +10 -1
- {solverforge-0.6.5 → solverforge-0.6.6}/LICENSE +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/python/solverforge/_native.pyi +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/python/solverforge/config.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/python/solverforge/console.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/python/solverforge/constraints.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/python/solverforge/decorators.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/python/solverforge/errors.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/python/solverforge/events.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/python/solverforge/fields.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/python/solverforge/joiner.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/python/solverforge/manager.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/python/solverforge/py.typed +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/python/solverforge/score.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/python/solverforge/solver.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/python/solverforge/typing.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/python/solverforge/ui.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/rust-toolchain.toml +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/bindings.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/callbacks/call.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/callbacks/mod.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/callbacks/registry.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/callbacks/threading.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/config.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/constraints/compiled.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/constraints/evaluate.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/constraints/incremental.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/constraints/list_precedence.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/constraints/matches.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/constraints/mod.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/constraints/state.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/constraints/stream_plan.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/descriptor/extractor.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/descriptor/mod.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/descriptor/variable.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/error.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/intern.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/lib.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/manager/candidate_trace.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/manager/events.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/manager/jobs.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/manager/mod.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/manager/provenance.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/proxy/entity.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/proxy/list.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/proxy/mod.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/proxy/solution.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/runtime/candidate_metric.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/runtime/dynamic_provider.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/runtime/list_slots.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/runtime/mod.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/schema/build.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/schema/compiled.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/schema/mod.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/schema/python.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/schema/runtime_plan.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/score.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/solver/api.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/solver/console.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/solver/mod.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/solver/progress.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/solver/result.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/solver/solvable.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/state/callback_view.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/state/clone.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/state/entity_table.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/state/mod.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/state/variables.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/ui.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.6}/src/value.rs +0 -0
|
@@ -560,9 +560,9 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
|
|
560
560
|
|
|
561
561
|
[[package]]
|
|
562
562
|
name = "solverforge-bridge"
|
|
563
|
-
version = "0.19.
|
|
563
|
+
version = "0.19.4"
|
|
564
564
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
565
|
-
checksum = "
|
|
565
|
+
checksum = "12530829741dfb79552c5cb4ae70ac621503b5e9438947cbf9b50257f712a997"
|
|
566
566
|
dependencies = [
|
|
567
567
|
"solverforge-config",
|
|
568
568
|
"solverforge-core",
|
|
@@ -572,9 +572,9 @@ dependencies = [
|
|
|
572
572
|
|
|
573
573
|
[[package]]
|
|
574
574
|
name = "solverforge-config"
|
|
575
|
-
version = "0.19.
|
|
575
|
+
version = "0.19.4"
|
|
576
576
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
577
|
-
checksum = "
|
|
577
|
+
checksum = "5cf7440a6bbc97606dfae1be85e55de541d30cc9a25635ba58eb4e78395faf1c"
|
|
578
578
|
dependencies = [
|
|
579
579
|
"serde",
|
|
580
580
|
"serde_yaml",
|
|
@@ -585,9 +585,9 @@ dependencies = [
|
|
|
585
585
|
|
|
586
586
|
[[package]]
|
|
587
587
|
name = "solverforge-console"
|
|
588
|
-
version = "0.19.
|
|
588
|
+
version = "0.19.4"
|
|
589
589
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
590
|
-
checksum = "
|
|
590
|
+
checksum = "ccca538afe6f31d83bb699e7d6ec7f679033b9713828b8139a897d81faf2ef9e"
|
|
591
591
|
dependencies = [
|
|
592
592
|
"num-format",
|
|
593
593
|
"owo-colors",
|
|
@@ -597,18 +597,18 @@ dependencies = [
|
|
|
597
597
|
|
|
598
598
|
[[package]]
|
|
599
599
|
name = "solverforge-core"
|
|
600
|
-
version = "0.19.
|
|
600
|
+
version = "0.19.4"
|
|
601
601
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
602
|
-
checksum = "
|
|
602
|
+
checksum = "79b244e1efabe25575798d8d53a1d4d43492167d5bc004bff9c7a9505f5aae1d"
|
|
603
603
|
dependencies = [
|
|
604
604
|
"thiserror",
|
|
605
605
|
]
|
|
606
606
|
|
|
607
607
|
[[package]]
|
|
608
608
|
name = "solverforge-scoring"
|
|
609
|
-
version = "0.19.
|
|
609
|
+
version = "0.19.4"
|
|
610
610
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
611
|
-
checksum = "
|
|
611
|
+
checksum = "4c097aee46dd2e6f09be86392e85fe28cb5eeb18c575f835e3c7ce768236ad79"
|
|
612
612
|
dependencies = [
|
|
613
613
|
"solverforge-core",
|
|
614
614
|
"thiserror",
|
|
@@ -616,9 +616,9 @@ dependencies = [
|
|
|
616
616
|
|
|
617
617
|
[[package]]
|
|
618
618
|
name = "solverforge-solver"
|
|
619
|
-
version = "0.19.
|
|
619
|
+
version = "0.19.4"
|
|
620
620
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
621
|
-
checksum = "
|
|
621
|
+
checksum = "7cb0d393e0c42c3e6e88c71e0a07a09b245225f20cfca05d4a5a01f1ddda2d8c"
|
|
622
622
|
dependencies = [
|
|
623
623
|
"rand",
|
|
624
624
|
"rand_chacha",
|
|
@@ -644,7 +644,7 @@ dependencies = [
|
|
|
644
644
|
|
|
645
645
|
[[package]]
|
|
646
646
|
name = "solverforge_py"
|
|
647
|
-
version = "0.6.
|
|
647
|
+
version = "0.6.6"
|
|
648
648
|
dependencies = [
|
|
649
649
|
"indexmap",
|
|
650
650
|
"once_cell",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "solverforge_py"
|
|
3
|
-
version = "0.6.
|
|
3
|
+
version = "0.6.6"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
license = "Apache-2.0"
|
|
6
6
|
rust-version = "1.95"
|
|
@@ -26,7 +26,7 @@ name = "solverforge_py"
|
|
|
26
26
|
crate-type = ["cdylib", "rlib"]
|
|
27
27
|
|
|
28
28
|
[package.metadata.solverforge]
|
|
29
|
-
version = "0.19.
|
|
29
|
+
version = "0.19.4"
|
|
30
30
|
|
|
31
31
|
[dependencies]
|
|
32
32
|
indexmap = "2.12.1"
|
|
@@ -42,12 +42,12 @@ toml = "0.9.8"
|
|
|
42
42
|
tracing = "0.1.44"
|
|
43
43
|
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
|
44
44
|
|
|
45
|
-
solverforge-config = { version = "=0.19.
|
|
46
|
-
solverforge-bridge = { version = "=0.19.
|
|
47
|
-
solverforge-console = { version = "=0.19.
|
|
48
|
-
solverforge-core = { version = "=0.19.
|
|
49
|
-
solverforge-scoring = { version = "=0.19.
|
|
50
|
-
solverforge-solver = { version = "=0.19.
|
|
45
|
+
solverforge-config = { version = "=0.19.4" }
|
|
46
|
+
solverforge-bridge = { version = "=0.19.4" }
|
|
47
|
+
solverforge-console = { version = "=0.19.4", features = ["verbose-logging"] }
|
|
48
|
+
solverforge-core = { version = "=0.19.4" }
|
|
49
|
+
solverforge-scoring = { version = "=0.19.4" }
|
|
50
|
+
solverforge-solver = { version = "=0.19.4" }
|
|
51
51
|
solverforge-ui = { version = "=0.7.0", default-features = false }
|
|
52
52
|
|
|
53
53
|
[dev-dependencies]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: solverforge
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.6
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -39,23 +39,25 @@ The native extension owns the working solution state in Rust so SolverForge can
|
|
|
39
39
|
clone, mutate, and snapshot solutions safely. Python callbacks are the single
|
|
40
40
|
constraint authoring surface.
|
|
41
41
|
|
|
42
|
-
The package targets CPython 3.14 and Rust 1.95.0. This
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
There is no local path or
|
|
46
|
-
`0.6.x` line is the current dynamic
|
|
47
|
-
|
|
48
|
-
namespace. Those older artifacts
|
|
49
|
-
Java service requirements, and
|
|
42
|
+
The package targets CPython 3.14 and Rust 1.95.0. This release-preparation
|
|
43
|
+
checkout pins the six published SolverForge Rust dependencies to `0.19.4` and
|
|
44
|
+
`solverforge-ui` to `0.7.0` through exact crates.io requirements in
|
|
45
|
+
`Cargo.toml` and registry checksums in `Cargo.lock`. There is no local path or
|
|
46
|
+
Git dependency override. The `solverforge` `0.6.x` line is the current dynamic
|
|
47
|
+
binding architecture and intentionally supersedes the older incompatible
|
|
48
|
+
`0.2.x` and `0.3.0` artifacts in the same PyPI namespace. Those older artifacts
|
|
49
|
+
exposed `SolverFactory`, `PlanningVariable`, Java service requirements, and
|
|
50
|
+
other APIs that are not part of this package.
|
|
50
51
|
|
|
51
52
|
## Installation
|
|
52
53
|
|
|
53
|
-
The package version prepared by this checkout is `solverforge` `0.6.
|
|
54
|
-
the published and locked SolverForge
|
|
55
|
-
Install the matching package after its Python
|
|
54
|
+
The package version prepared by this checkout is `solverforge` `0.6.6`. Its
|
|
55
|
+
release-quality build requires the published and registry-locked SolverForge
|
|
56
|
+
Rust `0.19.4` dependency base. Install the matching package after its Python
|
|
57
|
+
release with:
|
|
56
58
|
|
|
57
59
|
```sh
|
|
58
|
-
python3.14 -m pip install "solverforge==0.6.
|
|
60
|
+
python3.14 -m pip install "solverforge==0.6.6"
|
|
59
61
|
```
|
|
60
62
|
|
|
61
63
|
The installable wheel contains the core `solverforge` package, native extension,
|
|
@@ -74,13 +76,13 @@ make hospital-run
|
|
|
74
76
|
make deliveries-run PORT=7861
|
|
75
77
|
```
|
|
76
78
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
The same source-checkout examples can be run against the published package
|
|
80
|
+
without `PYTHONPATH` or an editable checkout:
|
|
79
81
|
|
|
80
82
|
```sh
|
|
81
83
|
python3.14 -m venv .venv-examples
|
|
82
84
|
. .venv-examples/bin/activate
|
|
83
|
-
python -m pip install "solverforge[examples]==0.6.
|
|
85
|
+
python -m pip install "solverforge[examples]==0.6.6"
|
|
84
86
|
python examples/nqueens.py
|
|
85
87
|
python -m examples.solverforge_hospital
|
|
86
88
|
python -m examples.solverforge_deliveries
|
|
@@ -240,8 +242,9 @@ Run `make help` for focused targets such as `make test-hospital`,
|
|
|
240
242
|
either a Python callback or a row field name. Dual raw-schema sources are
|
|
241
243
|
rejected at compilation, while immutable row metadata can be supplied without
|
|
242
244
|
per-query Python callbacks. Provider-backed value ranges are imported once per
|
|
243
|
-
variable and shared across rows in Rust-owned state
|
|
244
|
-
remain row-specific
|
|
245
|
+
variable and shared across rows in Rust-owned state. Row candidate callbacks
|
|
246
|
+
remain row-specific and define move legality throughout construction and local
|
|
247
|
+
search, including assignment-group swaps and rematches.
|
|
245
248
|
- `scalar_assignment_group(...)` declares assignment-aware scalar groups for
|
|
246
249
|
grouped scalar local search and assignment-group construction. Group metadata
|
|
247
250
|
covers required entities, capacity keys, assignment rules, ordering callbacks,
|
|
@@ -252,9 +255,12 @@ Run `make help` for focused targets such as `make test-hospital`,
|
|
|
252
255
|
metadata, `required_entity_field`, `capacity_key_field`,
|
|
253
256
|
`position_key_field`, and `sequence_key_field` are explicit alternatives to
|
|
254
257
|
their callbacks: required is a row bool, position and sequence are row
|
|
255
|
-
integers, and capacity is a row list indexed by candidate value.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
+
integers, and capacity is a row list indexed by candidate value.
|
|
259
|
+
`same_value_conflict_field` is the native alternative for a static
|
|
260
|
+
`assignment_rule`: each row lists adjacent-sequence entity indexes that
|
|
261
|
+
cannot hold the same assigned value, and the group still requires sequence
|
|
262
|
+
metadata. Field alternatives avoid per-candidate Python transitions without
|
|
263
|
+
caching or changing callback behavior.
|
|
258
264
|
The compiled schema retains one immutable runtime plan (schema, descriptor,
|
|
259
265
|
and model), reused by direct solves, manager jobs, snapshots, and resumes;
|
|
260
266
|
instance rows, callback views, seeds, and moves remain per solve.
|
|
@@ -8,23 +8,25 @@ The native extension owns the working solution state in Rust so SolverForge can
|
|
|
8
8
|
clone, mutate, and snapshot solutions safely. Python callbacks are the single
|
|
9
9
|
constraint authoring surface.
|
|
10
10
|
|
|
11
|
-
The package targets CPython 3.14 and Rust 1.95.0. This
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
There is no local path or
|
|
15
|
-
`0.6.x` line is the current dynamic
|
|
16
|
-
|
|
17
|
-
namespace. Those older artifacts
|
|
18
|
-
Java service requirements, and
|
|
11
|
+
The package targets CPython 3.14 and Rust 1.95.0. This release-preparation
|
|
12
|
+
checkout pins the six published SolverForge Rust dependencies to `0.19.4` and
|
|
13
|
+
`solverforge-ui` to `0.7.0` through exact crates.io requirements in
|
|
14
|
+
`Cargo.toml` and registry checksums in `Cargo.lock`. There is no local path or
|
|
15
|
+
Git dependency override. The `solverforge` `0.6.x` line is the current dynamic
|
|
16
|
+
binding architecture and intentionally supersedes the older incompatible
|
|
17
|
+
`0.2.x` and `0.3.0` artifacts in the same PyPI namespace. Those older artifacts
|
|
18
|
+
exposed `SolverFactory`, `PlanningVariable`, Java service requirements, and
|
|
19
|
+
other APIs that are not part of this package.
|
|
19
20
|
|
|
20
21
|
## Installation
|
|
21
22
|
|
|
22
|
-
The package version prepared by this checkout is `solverforge` `0.6.
|
|
23
|
-
the published and locked SolverForge
|
|
24
|
-
Install the matching package after its Python
|
|
23
|
+
The package version prepared by this checkout is `solverforge` `0.6.6`. Its
|
|
24
|
+
release-quality build requires the published and registry-locked SolverForge
|
|
25
|
+
Rust `0.19.4` dependency base. Install the matching package after its Python
|
|
26
|
+
release with:
|
|
25
27
|
|
|
26
28
|
```sh
|
|
27
|
-
python3.14 -m pip install "solverforge==0.6.
|
|
29
|
+
python3.14 -m pip install "solverforge==0.6.6"
|
|
28
30
|
```
|
|
29
31
|
|
|
30
32
|
The installable wheel contains the core `solverforge` package, native extension,
|
|
@@ -43,13 +45,13 @@ make hospital-run
|
|
|
43
45
|
make deliveries-run PORT=7861
|
|
44
46
|
```
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
The same source-checkout examples can be run against the published package
|
|
49
|
+
without `PYTHONPATH` or an editable checkout:
|
|
48
50
|
|
|
49
51
|
```sh
|
|
50
52
|
python3.14 -m venv .venv-examples
|
|
51
53
|
. .venv-examples/bin/activate
|
|
52
|
-
python -m pip install "solverforge[examples]==0.6.
|
|
54
|
+
python -m pip install "solverforge[examples]==0.6.6"
|
|
53
55
|
python examples/nqueens.py
|
|
54
56
|
python -m examples.solverforge_hospital
|
|
55
57
|
python -m examples.solverforge_deliveries
|
|
@@ -209,8 +211,9 @@ Run `make help` for focused targets such as `make test-hospital`,
|
|
|
209
211
|
either a Python callback or a row field name. Dual raw-schema sources are
|
|
210
212
|
rejected at compilation, while immutable row metadata can be supplied without
|
|
211
213
|
per-query Python callbacks. Provider-backed value ranges are imported once per
|
|
212
|
-
variable and shared across rows in Rust-owned state
|
|
213
|
-
remain row-specific
|
|
214
|
+
variable and shared across rows in Rust-owned state. Row candidate callbacks
|
|
215
|
+
remain row-specific and define move legality throughout construction and local
|
|
216
|
+
search, including assignment-group swaps and rematches.
|
|
214
217
|
- `scalar_assignment_group(...)` declares assignment-aware scalar groups for
|
|
215
218
|
grouped scalar local search and assignment-group construction. Group metadata
|
|
216
219
|
covers required entities, capacity keys, assignment rules, ordering callbacks,
|
|
@@ -221,9 +224,12 @@ Run `make help` for focused targets such as `make test-hospital`,
|
|
|
221
224
|
metadata, `required_entity_field`, `capacity_key_field`,
|
|
222
225
|
`position_key_field`, and `sequence_key_field` are explicit alternatives to
|
|
223
226
|
their callbacks: required is a row bool, position and sequence are row
|
|
224
|
-
integers, and capacity is a row list indexed by candidate value.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
+
integers, and capacity is a row list indexed by candidate value.
|
|
228
|
+
`same_value_conflict_field` is the native alternative for a static
|
|
229
|
+
`assignment_rule`: each row lists adjacent-sequence entity indexes that
|
|
230
|
+
cannot hold the same assigned value, and the group still requires sequence
|
|
231
|
+
metadata. Field alternatives avoid per-candidate Python transitions without
|
|
232
|
+
caching or changing callback behavior.
|
|
227
233
|
The compiled schema retains one immutable runtime plan (schema, descriptor,
|
|
228
234
|
and model), reused by direct solves, manager jobs, snapshots, and resumes;
|
|
229
235
|
instance rows, callback views, seeds, and moves remain per solve.
|
|
@@ -51,6 +51,7 @@ class ScalarAssignmentGroup:
|
|
|
51
51
|
capacity_key: Callable[..., object] | None = None
|
|
52
52
|
capacity_key_field: str | None = None
|
|
53
53
|
assignment_rule: Callable[..., object] | None = None
|
|
54
|
+
same_value_conflict_field: str | None = None
|
|
54
55
|
position_key: Callable[..., object] | None = None
|
|
55
56
|
position_key_field: str | None = None
|
|
56
57
|
sequence_key: Callable[..., object] | None = None
|
|
@@ -95,6 +96,20 @@ class ScalarAssignmentGroup:
|
|
|
95
96
|
raise TypeError(
|
|
96
97
|
f"{field_name} must be a non-empty string when provided"
|
|
97
98
|
)
|
|
99
|
+
if (
|
|
100
|
+
self.assignment_rule is not None
|
|
101
|
+
and self.same_value_conflict_field is not None
|
|
102
|
+
):
|
|
103
|
+
raise TypeError(
|
|
104
|
+
"assignment_rule and same_value_conflict_field cannot both be configured"
|
|
105
|
+
)
|
|
106
|
+
if self.same_value_conflict_field is not None and (
|
|
107
|
+
not isinstance(self.same_value_conflict_field, str)
|
|
108
|
+
or not self.same_value_conflict_field
|
|
109
|
+
):
|
|
110
|
+
raise TypeError(
|
|
111
|
+
"same_value_conflict_field must be a non-empty string when provided"
|
|
112
|
+
)
|
|
98
113
|
|
|
99
114
|
def to_native(self) -> dict[str, Any]:
|
|
100
115
|
return {
|
|
@@ -107,6 +122,7 @@ class ScalarAssignmentGroup:
|
|
|
107
122
|
"capacity_key": self.capacity_key,
|
|
108
123
|
"capacity_key_field": self.capacity_key_field,
|
|
109
124
|
"assignment_rule": self.assignment_rule,
|
|
125
|
+
"same_value_conflict_field": self.same_value_conflict_field,
|
|
110
126
|
"position_key": self.position_key,
|
|
111
127
|
"position_key_field": self.position_key_field,
|
|
112
128
|
"sequence_key": self.sequence_key,
|
|
@@ -128,6 +144,7 @@ def scalar_assignment_group(
|
|
|
128
144
|
capacity_key: Callable[..., object] | None = None,
|
|
129
145
|
capacity_key_field: str | None = None,
|
|
130
146
|
assignment_rule: Callable[..., object] | None = None,
|
|
147
|
+
same_value_conflict_field: str | None = None,
|
|
131
148
|
position_key: Callable[..., object] | None = None,
|
|
132
149
|
position_key_field: str | None = None,
|
|
133
150
|
sequence_key: Callable[..., object] | None = None,
|
|
@@ -145,6 +162,9 @@ def scalar_assignment_group(
|
|
|
145
162
|
callback transitions while preserving the declared metadata values. The declaration compiles
|
|
146
163
|
into one immutable native runtime plan rather than a Python-side assignment search path. Its
|
|
147
164
|
target variable is exclusively searched through the resulting grouped selector.
|
|
165
|
+
``same_value_conflict_field`` names a row list of adjacent-sequence entity indices that may
|
|
166
|
+
not share the same assigned value, providing a native conflict-graph alternative to
|
|
167
|
+
``assignment_rule``.
|
|
148
168
|
"""
|
|
149
169
|
|
|
150
170
|
return ScalarAssignmentGroup(
|
|
@@ -156,6 +176,7 @@ def scalar_assignment_group(
|
|
|
156
176
|
capacity_key=capacity_key,
|
|
157
177
|
capacity_key_field=capacity_key_field,
|
|
158
178
|
assignment_rule=assignment_rule,
|
|
179
|
+
same_value_conflict_field=same_value_conflict_field,
|
|
159
180
|
position_key=position_key,
|
|
160
181
|
position_key_field=position_key_field,
|
|
161
182
|
sequence_key=sequence_key,
|
|
@@ -222,6 +222,16 @@ def _scalar_groups(callbacks: object) -> list[dict[str, object]]:
|
|
|
222
222
|
"assignment-rule groups need sequence metadata"
|
|
223
223
|
)
|
|
224
224
|
raise ModelValidationError(msg)
|
|
225
|
+
if (
|
|
226
|
+
item.same_value_conflict_field is not None
|
|
227
|
+
and item.sequence_key is None
|
|
228
|
+
and item.sequence_key_field is None
|
|
229
|
+
):
|
|
230
|
+
msg = (
|
|
231
|
+
f"{item!r} declares a same_value_conflict_field but no sequence_key; "
|
|
232
|
+
"same-value conflict groups need sequence metadata"
|
|
233
|
+
)
|
|
234
|
+
raise ModelValidationError(msg)
|
|
225
235
|
if item.name in names:
|
|
226
236
|
msg = f"scalar group name `{item.name}` is declared more than once"
|
|
227
237
|
raise ModelValidationError(msg)
|
|
@@ -100,6 +100,18 @@ fn build_assignment_group(
|
|
|
100
100
|
group.name
|
|
101
101
|
));
|
|
102
102
|
}
|
|
103
|
+
if group.assignment_rule.is_some() && group.same_value_conflict_field.is_some() {
|
|
104
|
+
return Err(format!(
|
|
105
|
+
"assignment scalar group `{}` cannot declare both assignment_rule and same_value_conflict_field",
|
|
106
|
+
group.name
|
|
107
|
+
));
|
|
108
|
+
}
|
|
109
|
+
if group.same_value_conflict_field.is_some() && group.sequence_key.is_none() {
|
|
110
|
+
return Err(format!(
|
|
111
|
+
"assignment scalar group `{}` declares same_value_conflict_field but no sequence_key",
|
|
112
|
+
group.name
|
|
113
|
+
));
|
|
114
|
+
}
|
|
103
115
|
|
|
104
116
|
let slot = DynamicScalarVariableSlot::new(
|
|
105
117
|
EntityClassId(entity_index),
|
|
@@ -368,6 +380,14 @@ impl PyDynamicAssignmentMetadata {
|
|
|
368
380
|
right_entity: usize,
|
|
369
381
|
right_value: usize,
|
|
370
382
|
) -> bool {
|
|
383
|
+
if let Some(field_name) = self.group().same_value_conflict_field.as_deref() {
|
|
384
|
+
if left_value != right_value {
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
return !self
|
|
388
|
+
.same_value_conflicts(solution, field_name, left_entity, right_entity)
|
|
389
|
+
.unwrap_or_else(panic_with_py_err);
|
|
390
|
+
}
|
|
371
391
|
self.callback_value(
|
|
372
392
|
solution,
|
|
373
393
|
"assignment_rule",
|
|
@@ -377,6 +397,63 @@ impl PyDynamicAssignmentMetadata {
|
|
|
377
397
|
.unwrap_or_else(panic_with_py_err)
|
|
378
398
|
.unwrap_or(true)
|
|
379
399
|
}
|
|
400
|
+
|
|
401
|
+
fn same_value_conflicts(
|
|
402
|
+
&self,
|
|
403
|
+
solution: &PyDynamicSolution,
|
|
404
|
+
field_name: &str,
|
|
405
|
+
left_entity: usize,
|
|
406
|
+
right_entity: usize,
|
|
407
|
+
) -> PyResult<bool> {
|
|
408
|
+
Ok(
|
|
409
|
+
self.row_conflicts_with(solution, field_name, left_entity, right_entity)?
|
|
410
|
+
|| self.row_conflicts_with(solution, field_name, right_entity, left_entity)?,
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
fn row_conflicts_with(
|
|
415
|
+
&self,
|
|
416
|
+
solution: &PyDynamicSolution,
|
|
417
|
+
field_name: &str,
|
|
418
|
+
entity_index: usize,
|
|
419
|
+
other_entity_index: usize,
|
|
420
|
+
) -> PyResult<bool> {
|
|
421
|
+
let value = solution
|
|
422
|
+
.state
|
|
423
|
+
.entities
|
|
424
|
+
.get(self.entity_index)
|
|
425
|
+
.and_then(|rows| rows.get(entity_index))
|
|
426
|
+
.and_then(|row| row.fields.get(field_name))
|
|
427
|
+
.ok_or_else(|| {
|
|
428
|
+
py_err(format!(
|
|
429
|
+
"assignment scalar group `{}` field `{field_name}` is missing on row {entity_index}",
|
|
430
|
+
self.group().name
|
|
431
|
+
))
|
|
432
|
+
})?;
|
|
433
|
+
let DynamicValue::List(conflicts) = value else {
|
|
434
|
+
return Err(py_err(format!(
|
|
435
|
+
"assignment scalar group `{}` same_value_conflict_field `{field_name}` must be a list of entity indices",
|
|
436
|
+
self.group().name
|
|
437
|
+
)));
|
|
438
|
+
};
|
|
439
|
+
let other_entity_index = i64::try_from(other_entity_index).map_err(|_| {
|
|
440
|
+
py_err(format!(
|
|
441
|
+
"assignment entity index `{other_entity_index}` does not fit in a field integer"
|
|
442
|
+
))
|
|
443
|
+
})?;
|
|
444
|
+
for conflict in conflicts {
|
|
445
|
+
let DynamicValue::Int(conflict) = conflict else {
|
|
446
|
+
return Err(py_err(format!(
|
|
447
|
+
"assignment scalar group `{}` same_value_conflict_field `{field_name}` must contain only entity indices",
|
|
448
|
+
self.group().name
|
|
449
|
+
)));
|
|
450
|
+
};
|
|
451
|
+
if *conflict == other_entity_index {
|
|
452
|
+
return Ok(true);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
Ok(false)
|
|
456
|
+
}
|
|
380
457
|
}
|
|
381
458
|
|
|
382
459
|
impl DynamicScalarAssignmentMetadata<PyDynamicSolution> for PyDynamicAssignmentMetadata {
|
|
@@ -389,7 +466,8 @@ impl DynamicScalarAssignmentMetadata<PyDynamicSolution> for PyDynamicAssignmentM
|
|
|
389
466
|
sequence_key: group.sequence_key.is_some(),
|
|
390
467
|
entity_order: group.entity_order.is_some(),
|
|
391
468
|
value_order: group.value_order.is_some(),
|
|
392
|
-
assignment_rule: group.assignment_rule.is_some()
|
|
469
|
+
assignment_rule: group.assignment_rule.is_some()
|
|
470
|
+
|| group.same_value_conflict_field.is_some(),
|
|
393
471
|
}
|
|
394
472
|
}
|
|
395
473
|
|
|
@@ -216,7 +216,13 @@ impl DynamicScalarAccess<PyDynamicSolution> for PyDynamicScalarAccess {
|
|
|
216
216
|
.unwrap_or(&[])
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
fn value_is_legal(&self, solution: &PyDynamicSolution,
|
|
219
|
+
fn value_is_legal(&self, solution: &PyDynamicSolution, row: usize, value: usize) -> bool {
|
|
220
|
+
if let Some(candidates) = self
|
|
221
|
+
.row(solution, row)
|
|
222
|
+
.and_then(|row| row.candidates_at(self.variable.0))
|
|
223
|
+
{
|
|
224
|
+
return candidates.contains(&value);
|
|
225
|
+
}
|
|
220
226
|
solution
|
|
221
227
|
.state
|
|
222
228
|
.scalar_value_range_at(self.entity.0, self.variable.0)
|
|
@@ -159,6 +159,7 @@ fn parse_assignment_scalar_groups(
|
|
|
159
159
|
)?,
|
|
160
160
|
capacity_key: optional_metadata_source(group, "capacity_key", "capacity_key_field")?,
|
|
161
161
|
assignment_rule: optional_callable(group, "assignment_rule")?,
|
|
162
|
+
same_value_conflict_field: optional_str(group, "same_value_conflict_field")?,
|
|
162
163
|
position_key: optional_metadata_source(group, "position_key", "position_key_field")?,
|
|
163
164
|
sequence_key: optional_metadata_source(group, "sequence_key", "sequence_key_field")?,
|
|
164
165
|
entity_order: optional_callable(group, "entity_order")?,
|
|
@@ -200,6 +200,7 @@ pub struct AssignmentScalarGroupSchema {
|
|
|
200
200
|
pub required_entity: Option<MetadataSourceSchema>,
|
|
201
201
|
pub capacity_key: Option<MetadataSourceSchema>,
|
|
202
202
|
pub assignment_rule: Option<Py<PyAny>>,
|
|
203
|
+
pub same_value_conflict_field: Option<String>,
|
|
203
204
|
pub position_key: Option<MetadataSourceSchema>,
|
|
204
205
|
pub sequence_key: Option<MetadataSourceSchema>,
|
|
205
206
|
pub entity_order: Option<Py<PyAny>>,
|
|
@@ -58,6 +58,28 @@ pub fn validate_dynamic_schema(schema: &DynamicSchema) -> Result<(), pyo3::PyErr
|
|
|
58
58
|
group.name
|
|
59
59
|
)));
|
|
60
60
|
}
|
|
61
|
+
if group.assignment_rule.is_some() && group.same_value_conflict_field.is_some() {
|
|
62
|
+
return Err(py_err(format!(
|
|
63
|
+
"assignment scalar group `{}` cannot declare both assignment_rule and same_value_conflict_field",
|
|
64
|
+
group.name
|
|
65
|
+
)));
|
|
66
|
+
}
|
|
67
|
+
if group
|
|
68
|
+
.same_value_conflict_field
|
|
69
|
+
.as_ref()
|
|
70
|
+
.is_some_and(String::is_empty)
|
|
71
|
+
{
|
|
72
|
+
return Err(py_err(format!(
|
|
73
|
+
"assignment scalar group `{}` has an empty same_value_conflict_field",
|
|
74
|
+
group.name
|
|
75
|
+
)));
|
|
76
|
+
}
|
|
77
|
+
if group.same_value_conflict_field.is_some() && group.sequence_key.is_none() {
|
|
78
|
+
return Err(py_err(format!(
|
|
79
|
+
"assignment scalar group `{}` declares same_value_conflict_field but no sequence_key",
|
|
80
|
+
group.name
|
|
81
|
+
)));
|
|
82
|
+
}
|
|
61
83
|
}
|
|
62
84
|
Ok(())
|
|
63
85
|
}
|
|
@@ -436,6 +436,9 @@ fn row_metadata_fields(schema: &DynamicSchema) -> BTreeSet<String> {
|
|
|
436
436
|
field_names.insert(field.clone());
|
|
437
437
|
}
|
|
438
438
|
}
|
|
439
|
+
if let Some(field) = group.same_value_conflict_field.as_ref() {
|
|
440
|
+
field_names.insert(field.clone());
|
|
441
|
+
}
|
|
439
442
|
}
|
|
440
443
|
field_names
|
|
441
444
|
}
|
|
@@ -499,10 +502,12 @@ fn validate_assignment_group_field_metadata(
|
|
|
499
502
|
.sequence_key
|
|
500
503
|
.as_ref()
|
|
501
504
|
.and_then(MetadataSourceSchema::row);
|
|
505
|
+
let same_value_conflict_field = group.same_value_conflict_field.as_deref();
|
|
502
506
|
if required_field.is_none()
|
|
503
507
|
&& capacity_field.is_none()
|
|
504
508
|
&& position_field.is_none()
|
|
505
509
|
&& sequence_field.is_none()
|
|
510
|
+
&& same_value_conflict_field.is_none()
|
|
506
511
|
{
|
|
507
512
|
continue;
|
|
508
513
|
}
|
|
@@ -560,6 +565,35 @@ fn validate_assignment_group_field_metadata(
|
|
|
560
565
|
)));
|
|
561
566
|
}
|
|
562
567
|
}
|
|
568
|
+
if let Some(field) = same_value_conflict_field {
|
|
569
|
+
let value = assignment_metadata_field(row, &group.name, row_index, field)?;
|
|
570
|
+
let DynamicValue::List(conflicts) = value else {
|
|
571
|
+
return Err(crate::error::py_err(format!(
|
|
572
|
+
"assignment scalar group `{}` field `{field}` on row {row_index} must be a list of entity indices",
|
|
573
|
+
group.name
|
|
574
|
+
)));
|
|
575
|
+
};
|
|
576
|
+
for conflict in conflicts {
|
|
577
|
+
let DynamicValue::Int(conflict) = conflict else {
|
|
578
|
+
return Err(crate::error::py_err(format!(
|
|
579
|
+
"assignment scalar group `{}` field `{field}` on row {row_index} must contain only entity indices",
|
|
580
|
+
group.name
|
|
581
|
+
)));
|
|
582
|
+
};
|
|
583
|
+
let Ok(conflict) = usize::try_from(*conflict) else {
|
|
584
|
+
return Err(crate::error::py_err(format!(
|
|
585
|
+
"assignment scalar group `{}` field `{field}` on row {row_index} contains invalid entity index {conflict}",
|
|
586
|
+
group.name
|
|
587
|
+
)));
|
|
588
|
+
};
|
|
589
|
+
if conflict >= rows.len() {
|
|
590
|
+
return Err(crate::error::py_err(format!(
|
|
591
|
+
"assignment scalar group `{}` field `{field}` on row {row_index} contains out-of-range entity index {conflict}",
|
|
592
|
+
group.name
|
|
593
|
+
)));
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
563
597
|
if let Some(field) = capacity_field {
|
|
564
598
|
let value = assignment_metadata_field(row, &group.name, row_index, field)?;
|
|
565
599
|
let DynamicValue::List(values) = value else {
|
|
@@ -671,10 +671,19 @@ impl DynamicModelBackend for PyDynamicSolution {
|
|
|
671
671
|
fn scalar_value_is_legal(
|
|
672
672
|
&self,
|
|
673
673
|
entity: EntityClassId,
|
|
674
|
-
|
|
674
|
+
row: usize,
|
|
675
675
|
variable: VariableId,
|
|
676
676
|
value: usize,
|
|
677
677
|
) -> bool {
|
|
678
|
+
if let Some(candidates) = self
|
|
679
|
+
.state
|
|
680
|
+
.entities
|
|
681
|
+
.get(entity.0)
|
|
682
|
+
.and_then(|rows| rows.get(row))
|
|
683
|
+
.and_then(|row| row.candidates_at(variable.0))
|
|
684
|
+
{
|
|
685
|
+
return candidates.contains(&value);
|
|
686
|
+
}
|
|
678
687
|
self.state
|
|
679
688
|
.scalar_value_range_at(entity.0, variable.0)
|
|
680
689
|
.is_some_and(|values| values.contains(&value))
|
|
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
|