solverforge 0.6.5__tar.gz → 0.6.7__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.7}/Cargo.lock +15 -15
- {solverforge-0.6.5 → solverforge-0.6.7}/Cargo.toml +9 -9
- {solverforge-0.6.5 → solverforge-0.6.7}/PKG-INFO +29 -20
- {solverforge-0.6.5 → solverforge-0.6.7}/README.md +28 -19
- {solverforge-0.6.5 → solverforge-0.6.7}/pyproject.toml +1 -1
- {solverforge-0.6.5 → solverforge-0.6.7}/python/solverforge/__init__.py +1 -1
- {solverforge-0.6.5 → solverforge-0.6.7}/python/solverforge/decorators.py +0 -1
- {solverforge-0.6.5 → solverforge-0.6.7}/python/solverforge/fields.py +10 -2
- {solverforge-0.6.5 → solverforge-0.6.7}/python/solverforge/groups.py +21 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/python/solverforge/model.py +10 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/runtime/dynamic_assignment_group.rs +79 -1
- {solverforge-0.6.5 → solverforge-0.6.7}/src/runtime/scalar_slots.rs +7 -1
- {solverforge-0.6.5 → solverforge-0.6.7}/src/schema/parse.rs +1 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/schema/types.rs +1 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/schema/validate.rs +22 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/state/marshal.rs +34 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/state/solution.rs +10 -1
- {solverforge-0.6.5 → solverforge-0.6.7}/LICENSE +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/python/solverforge/_native.pyi +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/python/solverforge/config.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/python/solverforge/console.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/python/solverforge/constraints.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/python/solverforge/errors.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/python/solverforge/events.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/python/solverforge/joiner.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/python/solverforge/manager.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/python/solverforge/py.typed +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/python/solverforge/score.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/python/solverforge/solver.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/python/solverforge/typing.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/python/solverforge/ui.py +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/rust-toolchain.toml +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/bindings.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/callbacks/call.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/callbacks/mod.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/callbacks/registry.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/callbacks/threading.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/config.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/constraints/compiled.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/constraints/evaluate.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/constraints/incremental.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/constraints/list_precedence.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/constraints/matches.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/constraints/mod.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/constraints/state.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/constraints/stream_plan.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/descriptor/extractor.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/descriptor/mod.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/descriptor/variable.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/error.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/intern.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/lib.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/manager/candidate_trace.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/manager/events.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/manager/jobs.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/manager/mod.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/manager/provenance.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/proxy/entity.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/proxy/list.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/proxy/mod.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/proxy/solution.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/runtime/candidate_metric.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/runtime/dynamic_provider.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/runtime/list_slots.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/runtime/mod.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/schema/build.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/schema/compiled.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/schema/mod.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/schema/python.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/schema/runtime_plan.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/score.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/solver/api.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/solver/console.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/solver/mod.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/solver/progress.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/solver/result.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/solver/solvable.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/state/callback_view.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/state/clone.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/state/entity_table.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/state/mod.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/state/variables.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/src/ui.rs +0 -0
- {solverforge-0.6.5 → solverforge-0.6.7}/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.7"
|
|
564
564
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
565
|
-
checksum = "
|
|
565
|
+
checksum = "6257a13961202890dad23729ccceae45afd14c156e046c60b74e739bf0a26f36"
|
|
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.7"
|
|
576
576
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
577
|
-
checksum = "
|
|
577
|
+
checksum = "0801115100979d1c4558918277d6a2f2992f843f90f67b9fc0cf78699a5bde06"
|
|
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.7"
|
|
589
589
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
590
|
-
checksum = "
|
|
590
|
+
checksum = "2728833bbaf987be4058e60d77a013408b8c577570a552adecddd0461ac56f40"
|
|
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.7"
|
|
601
601
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
602
|
-
checksum = "
|
|
602
|
+
checksum = "897a977c2b4af1ceb8bb01707bd23783ffa1363d7c70950767718b3daf767e48"
|
|
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.7"
|
|
610
610
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
611
|
-
checksum = "
|
|
611
|
+
checksum = "18f932ada3a948f09e3234197dc4696626264c73e89f2cf520fe3e87ad61f445"
|
|
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.7"
|
|
620
620
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
621
|
-
checksum = "
|
|
621
|
+
checksum = "e2f21595deb21733528ffbb95c076dceee638aa93bafad812676aa53ca7af4b1"
|
|
622
622
|
dependencies = [
|
|
623
623
|
"rand",
|
|
624
624
|
"rand_chacha",
|
|
@@ -635,16 +635,16 @@ dependencies = [
|
|
|
635
635
|
|
|
636
636
|
[[package]]
|
|
637
637
|
name = "solverforge-ui"
|
|
638
|
-
version = "0.
|
|
638
|
+
version = "0.9.0"
|
|
639
639
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
640
|
-
checksum = "
|
|
640
|
+
checksum = "87dc89ca92af46ff7c55eb29d4072d98b2414047fd84cd8c77458e60468d53f6"
|
|
641
641
|
dependencies = [
|
|
642
642
|
"include_dir",
|
|
643
643
|
]
|
|
644
644
|
|
|
645
645
|
[[package]]
|
|
646
646
|
name = "solverforge_py"
|
|
647
|
-
version = "0.6.
|
|
647
|
+
version = "0.6.7"
|
|
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.7"
|
|
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.7"
|
|
30
30
|
|
|
31
31
|
[dependencies]
|
|
32
32
|
indexmap = "2.12.1"
|
|
@@ -42,13 +42,13 @@ 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.
|
|
51
|
-
solverforge-ui = { version = "=0.
|
|
45
|
+
solverforge-config = { version = "=0.19.7" }
|
|
46
|
+
solverforge-bridge = { version = "=0.19.7" }
|
|
47
|
+
solverforge-console = { version = "=0.19.7", features = ["verbose-logging"] }
|
|
48
|
+
solverforge-core = { version = "=0.19.7" }
|
|
49
|
+
solverforge-scoring = { version = "=0.19.7" }
|
|
50
|
+
solverforge-solver = { version = "=0.19.7" }
|
|
51
|
+
solverforge-ui = { version = "=0.9.0", default-features = false }
|
|
52
52
|
|
|
53
53
|
[dev-dependencies]
|
|
54
54
|
pretty_assertions = "1.4.1"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: solverforge
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.7
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -39,23 +39,24 @@ 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
|
-
|
|
49
|
-
|
|
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.7` and
|
|
44
|
+
`solverforge-ui` to `0.9.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.
|
|
49
|
+
Those older artifacts exposed `SolverFactory`, `PlanningVariable`, Java service
|
|
50
|
+
requirements, and 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 Rust `0.19.
|
|
54
|
+
The package version prepared by this checkout is `solverforge` `0.6.7`, targeting
|
|
55
|
+
the published and registry-locked SolverForge Rust `0.19.7` dependency base.
|
|
55
56
|
Install the matching package after its Python release with:
|
|
56
57
|
|
|
57
58
|
```sh
|
|
58
|
-
python3.14 -m pip install "solverforge==0.6.
|
|
59
|
+
python3.14 -m pip install "solverforge==0.6.7"
|
|
59
60
|
```
|
|
60
61
|
|
|
61
62
|
The installable wheel contains the core `solverforge` package, native extension,
|
|
@@ -74,13 +75,13 @@ make hospital-run
|
|
|
74
75
|
make deliveries-run PORT=7861
|
|
75
76
|
```
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
The same source-checkout examples can be run against the matching published
|
|
79
|
+
package once `0.6.7` is released, without `PYTHONPATH` or an editable checkout:
|
|
79
80
|
|
|
80
81
|
```sh
|
|
81
82
|
python3.14 -m venv .venv-examples
|
|
82
83
|
. .venv-examples/bin/activate
|
|
83
|
-
python -m pip install "solverforge[examples]==0.6.
|
|
84
|
+
python -m pip install "solverforge[examples]==0.6.7"
|
|
84
85
|
python examples/nqueens.py
|
|
85
86
|
python -m examples.solverforge_hospital
|
|
86
87
|
python -m examples.solverforge_deliveries
|
|
@@ -240,8 +241,13 @@ Run `make help` for focused targets such as `make test-hospital`,
|
|
|
240
241
|
either a Python callback or a row field name. Dual raw-schema sources are
|
|
241
242
|
rejected at compilation, while immutable row metadata can be supplied without
|
|
242
243
|
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
|
|
244
|
+
variable and shared across rows in Rust-owned state. Row candidate callbacks
|
|
245
|
+
remain row-specific and define move legality throughout construction and local
|
|
246
|
+
search, including assignment-group swaps and rematches. `pinning=True` is
|
|
247
|
+
rejected with `ModelValidationError` rather than ignored: SolverForge pins a
|
|
248
|
+
whole planning entity through an entity-level descriptor predicate, and no
|
|
249
|
+
public bridge seam exposes entity pinning to the Python binding yet. Pin an
|
|
250
|
+
input value by restricting `candidate_values` instead.
|
|
245
251
|
- `scalar_assignment_group(...)` declares assignment-aware scalar groups for
|
|
246
252
|
grouped scalar local search and assignment-group construction. Group metadata
|
|
247
253
|
covers required entities, capacity keys, assignment rules, ordering callbacks,
|
|
@@ -252,9 +258,12 @@ Run `make help` for focused targets such as `make test-hospital`,
|
|
|
252
258
|
metadata, `required_entity_field`, `capacity_key_field`,
|
|
253
259
|
`position_key_field`, and `sequence_key_field` are explicit alternatives to
|
|
254
260
|
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
|
-
|
|
261
|
+
integers, and capacity is a row list indexed by candidate value.
|
|
262
|
+
`same_value_conflict_field` is the native alternative for a static
|
|
263
|
+
`assignment_rule`: each row lists adjacent-sequence entity indexes that
|
|
264
|
+
cannot hold the same assigned value, and the group still requires sequence
|
|
265
|
+
metadata. Field alternatives avoid per-candidate Python transitions without
|
|
266
|
+
caching or changing callback behavior.
|
|
258
267
|
The compiled schema retains one immutable runtime plan (schema, descriptor,
|
|
259
268
|
and model), reused by direct solves, manager jobs, snapshots, and resumes;
|
|
260
269
|
instance rows, callback views, seeds, and moves remain per solve.
|
|
@@ -8,23 +8,24 @@ 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
|
-
|
|
18
|
-
|
|
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.7` and
|
|
13
|
+
`solverforge-ui` to `0.9.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.
|
|
18
|
+
Those older artifacts exposed `SolverFactory`, `PlanningVariable`, Java service
|
|
19
|
+
requirements, and 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 Rust `0.19.
|
|
23
|
+
The package version prepared by this checkout is `solverforge` `0.6.7`, targeting
|
|
24
|
+
the published and registry-locked SolverForge Rust `0.19.7` dependency base.
|
|
24
25
|
Install the matching package after its Python release with:
|
|
25
26
|
|
|
26
27
|
```sh
|
|
27
|
-
python3.14 -m pip install "solverforge==0.6.
|
|
28
|
+
python3.14 -m pip install "solverforge==0.6.7"
|
|
28
29
|
```
|
|
29
30
|
|
|
30
31
|
The installable wheel contains the core `solverforge` package, native extension,
|
|
@@ -43,13 +44,13 @@ make hospital-run
|
|
|
43
44
|
make deliveries-run PORT=7861
|
|
44
45
|
```
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
The same source-checkout examples can be run against the matching published
|
|
48
|
+
package once `0.6.7` is released, without `PYTHONPATH` or an editable checkout:
|
|
48
49
|
|
|
49
50
|
```sh
|
|
50
51
|
python3.14 -m venv .venv-examples
|
|
51
52
|
. .venv-examples/bin/activate
|
|
52
|
-
python -m pip install "solverforge[examples]==0.6.
|
|
53
|
+
python -m pip install "solverforge[examples]==0.6.7"
|
|
53
54
|
python examples/nqueens.py
|
|
54
55
|
python -m examples.solverforge_hospital
|
|
55
56
|
python -m examples.solverforge_deliveries
|
|
@@ -209,8 +210,13 @@ Run `make help` for focused targets such as `make test-hospital`,
|
|
|
209
210
|
either a Python callback or a row field name. Dual raw-schema sources are
|
|
210
211
|
rejected at compilation, while immutable row metadata can be supplied without
|
|
211
212
|
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
|
|
213
|
+
variable and shared across rows in Rust-owned state. Row candidate callbacks
|
|
214
|
+
remain row-specific and define move legality throughout construction and local
|
|
215
|
+
search, including assignment-group swaps and rematches. `pinning=True` is
|
|
216
|
+
rejected with `ModelValidationError` rather than ignored: SolverForge pins a
|
|
217
|
+
whole planning entity through an entity-level descriptor predicate, and no
|
|
218
|
+
public bridge seam exposes entity pinning to the Python binding yet. Pin an
|
|
219
|
+
input value by restricting `candidate_values` instead.
|
|
214
220
|
- `scalar_assignment_group(...)` declares assignment-aware scalar groups for
|
|
215
221
|
grouped scalar local search and assignment-group construction. Group metadata
|
|
216
222
|
covers required entities, capacity keys, assignment rules, ordering callbacks,
|
|
@@ -221,9 +227,12 @@ Run `make help` for focused targets such as `make test-hospital`,
|
|
|
221
227
|
metadata, `required_entity_field`, `capacity_key_field`,
|
|
222
228
|
`position_key_field`, and `sequence_key_field` are explicit alternatives to
|
|
223
229
|
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
|
-
|
|
230
|
+
integers, and capacity is a row list indexed by candidate value.
|
|
231
|
+
`same_value_conflict_field` is the native alternative for a static
|
|
232
|
+
`assignment_rule`: each row lists adjacent-sequence entity indexes that
|
|
233
|
+
cannot hold the same assigned value, and the group still requires sequence
|
|
234
|
+
metadata. Field alternatives avoid per-candidate Python transitions without
|
|
235
|
+
caching or changing callback behavior.
|
|
227
236
|
The compiled schema retains one immutable runtime plan (schema, descriptor,
|
|
228
237
|
and model), reused by direct solves, manager jobs, snapshots, and resumes;
|
|
229
238
|
instance rows, callback views, seeds, and moves remain per solve.
|
|
@@ -40,7 +40,6 @@ def _collect_fields(cls: type[object]) -> list[dict[str, object]]:
|
|
|
40
40
|
"nearby_entity_distance_field": value.metadata.nearby_entity_distance_field,
|
|
41
41
|
"allows_unassigned": value.metadata.allows_unassigned,
|
|
42
42
|
"element_collection": value.metadata.element_collection,
|
|
43
|
-
"pinning": value.metadata.pinning,
|
|
44
43
|
"element_owner": value.metadata.element_owner,
|
|
45
44
|
"element_owner_field": value.metadata.element_owner_field,
|
|
46
45
|
"construction_element_order_key": value.metadata.construction_element_order_key,
|
|
@@ -5,6 +5,8 @@ from dataclasses import dataclass
|
|
|
5
5
|
from inspect import Parameter, signature
|
|
6
6
|
from typing import Any, NamedTuple, TypeAlias
|
|
7
7
|
|
|
8
|
+
from .errors import ModelValidationError
|
|
9
|
+
|
|
8
10
|
MetadataSource = Callable[..., object] | str
|
|
9
11
|
|
|
10
12
|
|
|
@@ -143,7 +145,6 @@ class FieldMetadata:
|
|
|
143
145
|
nearby_entity_distance_field: str | None = None
|
|
144
146
|
allows_unassigned: bool = False
|
|
145
147
|
element_collection: str | None = None
|
|
146
|
-
pinning: bool = False
|
|
147
148
|
element_owner: Callable[..., object] | None = None
|
|
148
149
|
element_owner_field: str | None = None
|
|
149
150
|
construction_element_order_key: Callable[..., object] | None = None
|
|
@@ -191,6 +192,14 @@ def planning_variable(
|
|
|
191
192
|
allows_unassigned: bool = False,
|
|
192
193
|
pinning: bool = False,
|
|
193
194
|
) -> PlanningField:
|
|
195
|
+
if pinning:
|
|
196
|
+
msg = (
|
|
197
|
+
"planning_variable(pinning=True) is not supported: SolverForge pins a whole "
|
|
198
|
+
"planning entity through an entity-level descriptor predicate, and no public "
|
|
199
|
+
"bridge seam exposes entity pinning to the Python binding yet. Pin an input "
|
|
200
|
+
"value by restricting candidate_values instead."
|
|
201
|
+
)
|
|
202
|
+
raise ModelValidationError(msg)
|
|
194
203
|
value_candidates = _resolve_metadata_source(
|
|
195
204
|
nearby_value_candidates, name="nearby_value_candidates"
|
|
196
205
|
)
|
|
@@ -217,7 +226,6 @@ def planning_variable(
|
|
|
217
226
|
nearby_entity_distance_meter=entity_distance.callback,
|
|
218
227
|
nearby_entity_distance_field=entity_distance.field_name,
|
|
219
228
|
allows_unassigned=allows_unassigned,
|
|
220
|
-
pinning=pinning,
|
|
221
229
|
)
|
|
222
230
|
)
|
|
223
231
|
|
|
@@ -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
|