Skip to content

feat!: run partitioned_search in the framework - #200

Merged
blackopsrepl merged 4 commits into
mainfrom
feature/199-partitioned-search
Nov 5, 2026
Merged

blackopsrepl merged 4 commits into
mainfrom
feature/199-partitioned-search

Conversation

@blackopsrepl

Copy link
Copy Markdown
Collaborator

Closes #199. Base main @ 958303a4, four commits:

  • 5f143255 feat(scoring): fork a director over another solution (Director::fork_for_solution)
  • 0c470c01 refactor(partitioned): run a partition pass through one shared function
  • 11a40691 feat(solver)!: run partitioned_search in the framework (SearchBuilder::partitioner replaces partitioned_phase; child_phases compiled and run, thread_count, log_progress)
  • 6a9f27c9 feat(partitioned)!: bound partitions by the phase termination

Gate on 6a9f27c9 (fmt, clippy, doc-names file-length facade-reachability public-api deps, rustdoc, examples, workspace all-feature tests, doctests): EXIT 0, 1554 passed, 0 failed, 69 result lines. Slice 1: scoring clippy/rustdoc/doc-names/tests 349 passed. Slice 3 tree: solver, facade, config and scoring tests 1250 passed, 0 failed.

Mutation checks, each failing its test: child phases not run; thread_count ignored; log_progress ignored; child configuration not compiled up front; phase termination not passed; a reached bound treated as a solve stop; merged-score limits not rejected.

`Director::fork_for_solution` builds an independent director of the same
kind over a given solution: the constraint set is built for that solution
with the recorded constructor, and the descriptor, weight mode, entity
counter and score assertion are shared with the source. Unlike
`fork_replica` it neither clones the working solution nor initializes the
incremental state, so a sub-problem such as one partition of partitioned
search initializes on the thread that solves it. Directors without a
constraint-set constructor return `None`.
`PartitionedSearchPhase` solved its partitions inline. The pass — split,
solve each partition in its own child scope on the resolved thread count,
merge — now lives in `solve_partitioned`, which takes the partitioner, the
thread and logging settings, a director maker and the child-phase runner,
so the configured runtime can run the same pass.

Partition directors are now built on the solving thread, from the parent's
director, before the partitions are dispatched; only `Send` directors cross
to the worker threads, and each partition still initializes its score on
the thread that solves it. `log_progress` now also logs the partition and
thread counts at the start of a pass.
A `partitioned_search` phase only looked up a user closure registered with
`partitioned_phase` and ran whatever phase it built, so `child_phases` was
validated and never run, and `thread_count` and `log_progress` took effect
only if the closure forwarded them.

The framework now runs the phase. A search declaration registers just the
partitioner, with `SearchBuilder::partitioner(name, |context| partitioner)`;
`partitioner = "name"` selects it. Each partition is solved by a runner
compiled from the parent configuration with `child_phases` as its phases,
an empty list meaning the default phases, over a director forked from the
solve's own with `Director::fork_for_solution`, on `thread_count` threads,
with `log_progress` logging the pass. The graph compiler compiles every
child configuration up front, so a child phase that cannot compile fails
the solve with its `phases[i].partitioned_search.child_phases[j]` path.

A child runner borrows its parent's declaration, so a partitioned phase
nested in `child_phases` compiles to the same runner type, and extension
registries are `Send + Sync` because partition runners compile on the
threads that solve them.

BREAKING CHANGE: `SearchBuilder::partitioned_phase` is removed. Register a
`SolutionPartitioner` with `SearchBuilder::partitioner` instead; the
framework builds and runs the phase from the `partitioned_search`
configuration.
A `partitioned_search` phase's `termination` was validated and never
applied: the runtime ran the phase without it. It now bounds the
partitions. Its time limit, counted from the phase's start, holds for every
partition, and its step count limit for each partition's solve. A
partition that reaches either ends like a phase: it keeps its best
solution, the partitions are merged, and the solve goes on to the next
phase instead of terminating. A bound made inside a partition chains to the
enclosing one.

A partition sees only its part of the solution, so `best_score_limit`,
`unimproved_step_count_limit` and `unimproved_seconds_spent_limit` on a
partitioned phase are rejected at compile time with their key path. Each
partition's configuration carries the earliest of the solve's and the
phase's time and step count limits, so the default local search runs in a
partition exactly when something bounds it there.

Closes #199.

BREAKING CHANGE: a `partitioned_search` phase whose `termination` sets
`best_score_limit`, `unimproved_step_count_limit` or
`unimproved_seconds_spent_limit` is a configuration error.
@blackopsrepl
blackopsrepl merged commit 6a9f27c into main Nov 5, 2026
12 checks passed
@blackopsrepl
blackopsrepl deleted the feature/199-partitioned-search branch November 5, 2026 04:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

partitioned_search: child_phases, thread_count, log_progress and termination are not run by the framework

1 participant