diff --git a/crates/solverforge-macros/WIREFRAME.md b/crates/solverforge-macros/WIREFRAME.md index 0070e90f..df489aa1 100644 --- a/crates/solverforge-macros/WIREFRAME.md +++ b/crates/solverforge-macros/WIREFRAME.md @@ -201,7 +201,7 @@ runtime scalar surface is candidate-index based. - Hidden scalar metadata bridge: private indexed helpers for scalar variable count, name, allows-unassigned, value-source metadata, getter/setter, and entity-local value slices. Helper order matches `entity_descriptor()` genuine scalar variable order; the index is used for generated getter/setter dispatch, while manifest hook attachment resolves descriptor variables by descriptor index plus variable name. - Hidden list metadata bridge (when the entity has a `#[planning_list_variable]` field): public cross-module `__SOLVERFORGE_LIST_VARIABLE_COUNT` plus private `__SOLVERFORGE_LIST_VARIABLE_NAME`, `__SOLVERFORGE_LIST_ELEMENT_COLLECTION`, `__solverforge_list_field()`, `__solverforge_list_field_mut()`, `__solverforge_list_metadata()` - Hidden list metadata bridge implementation (when the entity has a `#[planning_list_variable]` field): `impl __internal::ListVariableEntity for Entity` -- Hidden unassigned bridge (when the entity has exactly one `Option<_>` planning variable): `impl __internal::UnassignedEntity for Entity`, enabling `.unassigned()` on `UniConstraintStream<_, Entity, ...>` without a generated public trait import +- Unassigned bridge (when the entity has exactly one `Option<_>` planning variable): `impl __internal::UnassignedEntity for Entity`, the public `stream::UnassignedEntity`, enabling `.unassigned()` on `UniConstraintStream<_, Entity, ...>` without a generated public trait import ### Planning Solution Support Derive diff --git a/crates/solverforge-macros/tests/ui/fail/incremental_constraint_without_marker.stderr b/crates/solverforge-macros/tests/ui/fail/incremental_constraint_without_marker.stderr index 576820f7..01bcfa24 100644 --- a/crates/solverforge-macros/tests/ui/fail/incremental_constraint_without_marker.stderr +++ b/crates/solverforge-macros/tests/ui/fail/incremental_constraint_without_marker.stderr @@ -14,7 +14,7 @@ help: the trait `SingleConstraint` is not implemented for `Count` IncrementalUniConstraint ListPrecedenceMakespanConstraint OperatorTerminal - SharedOperatorTerminal, Sc> + SharedOperatorTerminal, Sc> Uni solverforge::constraint::projected::Bi solverforge::constraint::projected::DirectedBi diff --git a/crates/solverforge-scoring/WIREFRAME.md b/crates/solverforge-scoring/WIREFRAME.md index ebdc91bd..e41a3a65 100644 --- a/crates/solverforge-scoring/WIREFRAME.md +++ b/crates/solverforge-scoring/WIREFRAME.md @@ -103,7 +103,10 @@ migration of the fluent stream families: - `constraint::relational::OperatorTerminal::new(constraint_ref, impact, operator, weight, hard)` scores a generic operator; its weight takes `(&S, &O::View<'a>)`. It implements incremental scoring, reset, full counts, - and cold explanations. `ExplainRow` is a hidden analysis bridge. + and cold explanations. `ExplainRow::explain(&self, entities)` appends the + entities a row view was built from; terminals call it only on the cold + explanation path, and a custom operator implements it for its `View` to make + its matches explainable. - `stream::joiner::plan::{CompileCondition, IndexedPlan, ExecutablePlan}` separates condition compilation, owned index state, and borrowed-row execution. Candidate probes return `Cow<'i, [RowHandle]>`: equality borrows an exact @@ -204,7 +207,8 @@ src/ │ ├── relational/ │ │ ├── mod.rs — Module declarations and selected re-exports │ │ ├── operator_terminal.rs — OperatorTerminal over one generic operator -│ │ ├── operator_scorer.rs — OperatorScorer, OperatorScorerSet, ScorerChain, and RowWeight/AdaptWeight adapters +│ │ ├── operator_scorer.rs — OperatorScorer, ScorerChain, and RowWeight/AdaptWeight adapters +│ │ ├── operator_scorer/set.rs — OperatorScorerSet, the scorer-set contract shared terminals score through │ │ ├── shared_terminal.rs — SharedOperatorTerminal/Builder and chained terminal sharing │ │ └── grouped_weights.rs — Group/direct-complement/optional-complement weight adapters │ ├── projected.rs — Projected retained scoring-row constraint module root and re-exports @@ -305,7 +309,7 @@ src/ │ ├── projected_stream/source/merged.rs — Merged projected sources with source-slot offsets │ ├── projected_stream/source/joined.rs — Cross-join `.project(...)` projected source │ ├── collection_extract.rs — CollectionExtract trait, hidden source metadata and FilteredExtract membership adapter, source() constructor -│ ├── unassigned.rs — Hidden UnassignedEntity hook and `.unassigned()` stream method +│ ├── unassigned.rs — UnassignedEntity and the `.unassigned()` stream method │ ├── weighting_support.rs — ConstraintWeight, FixedWeight, HardWeight, and dynamic closure-weight adapters │ ├── join_target.rs — JoinTarget/ToViewPlan dispatch for self-join, keyed cross-join, comparison, overlap, composed, and predicate cross-join │ ├── key_extract.rs — KeyExtract trait, EntityKeyAdapter struct @@ -752,8 +756,10 @@ Dynamic closure weights are non-hard metadata by default, even when their score - `new()`, `for_each()` → `UniConstraintStream` - Generated solution source methods pass `for_each()` hidden descriptor/static source metadata. +**`UnassignedEntity`** — `fn is_unassigned(solution: &S, entity: &Self) -> bool`; enables `UniConstraintStream::unassigned()`. `#[planning_entity]` implements it for an entity with exactly one `Option<_>` planning variable (unassigned while `None`); implement it by hand for any other notion of unassigned. + **`UniConstraintStream`** — Single collection stream. -- Operations: `filter()`, `unassigned()` when the entity implements hidden `UnassignedEntity`, `join(target)` (single dispatch via `JoinTarget`), `group_by()`, `balance()`, `project(projection)` → `stream::projected::Stream`, `flattened(flatten)` → `FlattenedCollectionTarget`, `if_exists(target)`, `if_not_exists(target)`, `penalize(weight_or_fn)`, `reward(weight_or_fn)` +- Operations: `filter()`, `unassigned()` when the entity implements `UnassignedEntity`, `join(target)` (single dispatch via `JoinTarget`), `group_by()`, `balance()`, `project(projection)` → `stream::projected::Stream`, `flattened(flatten)` → `FlattenedCollectionTarget`, `if_exists(target)`, `if_not_exists(target)`, `penalize(weight_or_fn)`, `reward(weight_or_fn)` - `UniConstraintStream` implements `CollectionExtract` by delegating extraction to its source and applying its accumulated filter through `contains(...)`. - Stream targets preserve their own source filters when passed to keyed or predicate cross-joins. This lets `.join((ConstraintFactory::new().for_each(source).filter(pred), equal_bi(...)))` keep the right-side source predicate inside the joined stream. - For the first unary keyed equality cross-join, the left unary filter is preserved through hidden source membership before key extraction and pair creation. Original source indexes and change metadata remain intact across incremental membership transitions. Post-join filters remain joined-row filters. This left-filter pushdown does not apply to comparison, overlap, composed-condition, predicate, or same-source self-join dispatch. @@ -789,8 +795,21 @@ callers obtain these opaque carriers through fluent operations and continue on **`stream::projected::DirectedBiBuilder`** — `named()` → `constraint::projected::DirectedBi` -**`RowCoordinate`** — Hidden support coordinate for projected rows: -`{ primary_owner, secondary_owner, emit_index }`. `primary_owner` is always +**`stream::projected::Source`** — The producer behind a projected +stream: `type State`, `const MAX_EMITS`, `source_count()`, `change_source(slot)`, +`build_state(solution)`, `collect_all(solution, state, visit)`, +`collect_entity(solution, state, slot, entity_index, visit)`, and +`insert_entity_state`/`retract_entity_state`. Single-source projections, +filters, merges and joined pairs implement it; the fluent `project`, `filter`, +`merge` and `join` methods build them. + +**`RowOwner`** — `{ source_slot, entity_index }`: one entity a projected row +derives from. + +**`RowCoordinate`** — The stable identity of a projected row: +`{ primary_owner, secondary_owner, emit_index }`, built with `single(...)` or +`pair(...)`, visited with `for_each_owner(...)`, shifted with +`offset_source_slots(offset)`. `primary_owner` is always present; `secondary_owner` is present for joined-pair rows from `stream::cross::Bi::project(...)`. It is used to keep projected self-join orientation stable across sparse row-slot reuse and to dedupe callbacks when diff --git a/crates/solverforge-scoring/public-api.txt b/crates/solverforge-scoring/public-api.txt index 4a5888d4..ddceb978 100644 --- a/crates/solverforge-scoring/public-api.txt +++ b/crates/solverforge-scoring/public-api.txt @@ -43,10 +43,12 @@ struct constraint::relational::GroupAdapter: pub struct GroupAdapter(/* pr constraint::relational::GroupAdapter: pub fn new() -> Self struct constraint::relational::GroupedJoinAdapter: pub struct GroupedJoinAdapter(/* private fields */); constraint::relational::GroupedJoinAdapter: pub fn new() -> Self +struct constraint::relational::OperatorScorer: pub struct OperatorScorer { /* private fields */ } struct constraint::relational::OperatorTerminal: pub struct OperatorTerminal { /* private fields */ } constraint::relational::OperatorTerminal: pub fn new( constraint_ref: ConstraintRef, impact: ImpactType, operator: O, weight: W, hard: bool, ) -> Self struct constraint::relational::OptionalComplementAdapter: pub struct OptionalComplementAdapter { /* private fields */ } constraint::relational::OptionalComplementAdapter: pub fn new(key: KB) -> Self +struct constraint::relational::ScorerChain: pub struct ScorerChain(/* private fields */); struct constraint::relational::SharedOperatorBuilder: pub struct SharedOperatorBuilder { /* private fields */ } constraint::relational::SharedOperatorBuilder: pub fn first(operator: O, adapter: A, impact: ImpactType, weight: W) -> Self constraint::relational::SharedOperatorBuilder: pub fn with_explainer( self, explain: fn(&O, &S, RowHandle, &mut Vec), ) -> Self @@ -374,6 +376,12 @@ struct stream::projected::Grouped: pub struct Grouped where Sc: Score,{ /* private fields */ } stream::projected::GroupedBuilder: pub fn named( self, name: &str, ) -> SharedOperatorTerminal, impl Fn(&S, &ProjectedRow<'_, Out>) -> bool + Send + Sync>, impl Fn(&ProjectedRow<'_, Out>) -> K + Send + Sync, ProjectedCollector, K, Acc, V, R>, GroupAdapter, OperatorScorer, Sc>, Sc> struct stream::projected::JoinedSource: pub struct JoinedSource { /* private fields */ } +struct stream::projected::RowCoordinate: pub struct RowCoordinate { pub primary_owner: RowOwner, pub secondary_owner: Option, pub emit_index: usize, } + stream::projected::RowCoordinate: pub fn single( source_slot: usize, entity_index: usize, emit_index: usize, ) -> Self + stream::projected::RowCoordinate: pub fn pair( left_slot: usize, left_index: usize, right_slot: usize, right_index: usize, emit_index: usize, ) -> Self + stream::projected::RowCoordinate: pub fn for_each_owner(&self, visit: V) where V: FnMut(RowOwner), + stream::projected::RowCoordinate: pub fn offset_source_slots(self, offset: usize) -> Self +struct stream::projected::RowOwner: pub struct RowOwner { pub source_slot: usize, pub entity_index: usize, } struct stream::projected::Stream: pub struct Stream where Sc: Score,{ /* private fields */ } stream::projected::Stream: pub fn filter

( self, predicate: P, ) -> Stream bool + Send + Sync>>, Sc> where P: Fn(&Out) -> bool + Send + Sync + 'static, stream::projected::Stream: pub fn merge( self, other: Stream, ) -> Stream, FilteredSource>, TrueFilter, Sc> where OtherSrc: Source, OtherF: UniFilter, @@ -476,6 +484,22 @@ trait constraint::relational::AdaptWeight: pub trait AdaptWeight constraint::relational::AdaptWeight: fn adapt(&self, weight: W) -> Self::RowWeight constraint::relational::AdaptWeight: fn is_hard(&self, weight: &W) -> bool +trait constraint::relational::OperatorScorerSet: pub trait OperatorScorerSet, Sc: Score>: Send + Sync { Show 15 methods // Required methods fn evaluate(&self, operator: &O, solution: &S) -> Sc; fn initialize(&mut self, operator: &O, solution: &S) -> Sc; fn apply_changes( &mut self, operator: &O, solution: &S, removed: &[RowHandle], inserted: &[RowHandle], ) -> (Sc, bool); fn constraint_count(&self) -> usize; fn metadata(&self) -> Vec>; fn evaluate_each<'a>( &'a self, operator: &O, solution: &S, ) -> Vec>; fn evaluate_detailed<'a>( &'a self, operator: &O, solution: &S, ) -> Vec>; fn reset(&mut self); fn weight_refs<'a>(&'a self, refs: &mut Vec<&'a ConstraintRef>); fn evaluate_weighted( &self, operator: &O, solution: &S, multipliers: &[u32], ) -> Sc; fn initialize_weighted( &mut self, operator: &O, solution: &S, multipliers: &[u32], ) -> Sc; fn apply_changes_weighted( &mut self, operator: &O, solution: &S, removed: &[RowHandle], inserted: &[RowHandle], multipliers: &[u32], ) -> (Sc, bool); fn evaluate_each_weighted<'a>( &'a self, operator: &O, solution: &S, multipliers: &[u32], ) -> Vec>; fn evaluate_detailed_weighted<'a>( &'a self, operator: &O, solution: &S, multipliers: &[u32], ) -> Vec>; // Provided method fn score_for(&self, _handle: RowHandle) -> Option { ... } } + constraint::relational::OperatorScorerSet: fn evaluate(&self, operator: &O, solution: &S) -> Sc + constraint::relational::OperatorScorerSet: fn initialize(&mut self, operator: &O, solution: &S) -> Sc + constraint::relational::OperatorScorerSet: fn apply_changes( &mut self, operator: &O, solution: &S, removed: &[RowHandle], inserted: &[RowHandle], ) -> (Sc, bool) + constraint::relational::OperatorScorerSet: fn constraint_count(&self) -> usize + constraint::relational::OperatorScorerSet: fn metadata(&self) -> Vec> + constraint::relational::OperatorScorerSet: fn evaluate_each<'a>( &'a self, operator: &O, solution: &S, ) -> Vec> + constraint::relational::OperatorScorerSet: fn evaluate_detailed<'a>( &'a self, operator: &O, solution: &S, ) -> Vec> + constraint::relational::OperatorScorerSet: fn reset(&mut self) + constraint::relational::OperatorScorerSet: fn weight_refs<'a>(&'a self, refs: &mut Vec<&'a ConstraintRef>) + constraint::relational::OperatorScorerSet: fn evaluate_weighted( &self, operator: &O, solution: &S, multipliers: &[u32], ) -> Sc + constraint::relational::OperatorScorerSet: fn initialize_weighted( &mut self, operator: &O, solution: &S, multipliers: &[u32], ) -> Sc + constraint::relational::OperatorScorerSet: fn apply_changes_weighted( &mut self, operator: &O, solution: &S, removed: &[RowHandle], inserted: &[RowHandle], multipliers: &[u32], ) -> (Sc, bool) + constraint::relational::OperatorScorerSet: fn evaluate_each_weighted<'a>( &'a self, operator: &O, solution: &S, multipliers: &[u32], ) -> Vec> + constraint::relational::OperatorScorerSet: fn evaluate_detailed_weighted<'a>( &'a self, operator: &O, solution: &S, multipliers: &[u32], ) -> Vec> + constraint::relational::OperatorScorerSet: fn score_for(&self, _handle: RowHandle) -> Option trait constraint::relational::RowWeight: pub trait RowWeight, Sc>: Send + Sync { // Required method fn score(&self, solution: &S, row: &O::View<'_>) -> Sc; } constraint::relational::RowWeight: fn score(&self, solution: &S, row: &O::View<'_>) -> Sc trait director::Director: pub trait Director: Send { Show 19 methods // Required methods fn working_solution(&self) -> &S; fn working_solution_mut(&mut self) -> &mut S; fn calculate_score(&mut self) -> S::Score; fn solution_descriptor(&self) -> &SolutionDescriptor; fn clone_working_solution(&self) -> S; fn before_variable_changed( &mut self, descriptor_index: usize, entity_index: usize, ); fn after_variable_changed( &mut self, descriptor_index: usize, entity_index: usize, ); fn entity_count(&self, descriptor_index: usize) -> Option; fn total_entity_count(&self) -> Option; fn constraint_metadata(&self) -> Vec>; // Provided methods fn fresh_score(&self) -> Option { ... } fn set_score_assertion(&mut self, enabled: bool) { ... } fn constraint_is_hard(&self, constraint_ref: &ConstraintRef) -> Option { ... } fn is_incremental(&self) -> bool { ... } fn snapshot_score_state(&self) -> DirectorScoreState { ... } fn restore_score_state(&mut self, state: DirectorScoreState) { ... } fn reset(&mut self) { ... } fn fork_replica(&self) -> Option where Self: Sized { ... } fn fork_for_solution(&self, _solution: S) -> Option where Self: Sized { ... } } @@ -518,6 +542,8 @@ trait stream::Projection: pub trait Projection: Send + Sync { type Out: Send stream::Projection: fn project(&self, input: &A, sink: &mut Sink) where Sink: ProjectionSink, trait stream::ProjectionSink: pub trait ProjectionSink { // Required method fn emit(&mut self, output: Out); } stream::ProjectionSink: fn emit(&mut self, output: Out) +trait stream::UnassignedEntity: pub trait UnassignedEntity: Clone + Send + Sync + 'static { // Required method fn is_unassigned(solution: &S, entity: &Self) -> bool; } + stream::UnassignedEntity: fn is_unassigned(solution: &S, entity: &Self) -> bool trait stream::collection_extract::CollectionExtract: pub trait CollectionExtract: Send + Sync { type Item; // Required methods fn extract<'s>(&self, s: &'s S) -> &'s [Self::Item]; fn change_source(&self) -> ChangeSource; // Provided method fn contains(&self, _s: &S, _item: &Self::Item) -> bool { ... } } stream::collection_extract::CollectionExtract: type Item stream::collection_extract::CollectionExtract: fn extract<'s>(&self, s: &'s S) -> &'s [Self::Item] @@ -576,6 +602,16 @@ trait stream::projected::Projection: pub trait Projection: Send + Sync { type stream::projected::Projection: fn project(&self, input: &A, sink: &mut Sink) where Sink: ProjectionSink, trait stream::projected::ProjectionSink: pub trait ProjectionSink { // Required method fn emit(&mut self, output: Out); } stream::projected::ProjectionSink: fn emit(&mut self, output: Out) +trait stream::projected::Source: pub trait Source: Send + Sync { type State: Send + Sync; const MAX_EMITS: usize; // Required methods fn source_count(&self) -> usize; fn change_source(&self, slot: usize) -> ChangeSource; fn build_state(&self, solution: &S) -> Self::State; fn collect_all(&self, solution: &S, state: &Self::State, visit: V) where V: FnMut(RowCoordinate, Out); fn collect_entity( &self, solution: &S, state: &Self::State, slot: usize, entity_index: usize, visit: V, ) where V: FnMut(RowCoordinate, Out); fn insert_entity_state( &self, solution: &S, state: &mut Self::State, slot: usize, entity_index: usize, ); fn retract_entity_state( &self, solution: &S, state: &mut Self::State, slot: usize, entity_index: usize, ); } + stream::projected::Source: const MAX_EMITS: usize + stream::projected::Source: type State: Send + Sync + stream::projected::Source: fn source_count(&self) -> usize + stream::projected::Source: fn change_source(&self, slot: usize) -> ChangeSource + stream::projected::Source: fn build_state(&self, solution: &S) -> Self::State + stream::projected::Source: fn collect_all(&self, solution: &S, state: &Self::State, visit: V) where V: FnMut(RowCoordinate, Out), + stream::projected::Source: fn collect_entity( &self, solution: &S, state: &Self::State, slot: usize, entity_index: usize, visit: V, ) where V: FnMut(RowCoordinate, Out), + stream::projected::Source: fn insert_entity_state( &self, solution: &S, state: &mut Self::State, slot: usize, entity_index: usize, ) + stream::projected::Source: fn retract_entity_state( &self, solution: &S, state: &mut Self::State, slot: usize, entity_index: usize, ) trait stream::relational::operator::FilterPredicate: pub trait FilterPredicate: Send + Sync { // Required method fn accepts(&self, solution: &S, row: &V) -> bool; } stream::relational::operator::FilterPredicate: fn accepts(&self, solution: &S, row: &V) -> bool trait stream::relational::operator::FlattenSource: pub trait FlattenSource> { type Item: 'static; // Required method fn flatten<'a>( &self, solution: &'a S, view: O::View<'a>, ) -> &'a [Self::Item]; } diff --git a/crates/solverforge-scoring/src/constraint/relational/mod.rs b/crates/solverforge-scoring/src/constraint/relational/mod.rs index 58d382db..add9f48c 100644 --- a/crates/solverforge-scoring/src/constraint/relational/mod.rs +++ b/crates/solverforge-scoring/src/constraint/relational/mod.rs @@ -15,6 +15,6 @@ pub use grouped_weights::{ DirectComplementAdapter, DirectComplementWeight, GroupAdapter, GroupWeight, GroupedJoinAdapter, GroupedJoinWeight, OptionalComplementAdapter, OptionalComplementWeight, }; -pub use operator_scorer::{AdaptWeight, OperatorScorer, RowWeight, ScorerChain}; +pub use operator_scorer::{AdaptWeight, OperatorScorer, OperatorScorerSet, RowWeight, ScorerChain}; pub use operator_terminal::OperatorTerminal; pub use shared_terminal::{SharedOperatorBuilder, SharedOperatorTerminal}; diff --git a/crates/solverforge-scoring/src/constraint/relational/operator_scorer.rs b/crates/solverforge-scoring/src/constraint/relational/operator_scorer.rs index 6e0ebacd..50963d03 100644 --- a/crates/solverforge-scoring/src/constraint/relational/operator_scorer.rs +++ b/crates/solverforge-scoring/src/constraint/relational/operator_scorer.rs @@ -16,6 +16,10 @@ use crate::api::{ use crate::stream::relational::operator::Operator; use crate::stream::relational::{HandleMap, RowHandle}; +mod set; + +pub use set::OperatorScorerSet; + /// Unsigned score of one row of operator `O`; the terminal applies the /// impact. pub trait RowWeight, Sc>: Send + Sync { @@ -35,7 +39,9 @@ pub trait AdaptWeight, W, Sc>: Send + Sync { fn is_hard(&self, weight: &W) -> bool; } -#[doc(hidden)] +/// Scores one named terminal over a shared operator: its constraint +/// identity, impact, row weight and the unsigned score it retained for each +/// row. Built by `SharedOperatorBuilder::first_named` and `named`. pub struct OperatorScorer { pub(super) constraint_ref: ConstraintRef, impact: ImpactType, @@ -71,58 +77,6 @@ impl OperatorScorer { } } -#[doc(hidden)] -pub trait OperatorScorerSet, Sc: Score>: Send + Sync { - fn evaluate(&self, operator: &O, solution: &S) -> Sc; - fn initialize(&mut self, operator: &O, solution: &S) -> Sc; - fn apply_changes( - &mut self, - operator: &O, - solution: &S, - removed: &[RowHandle], - inserted: &[RowHandle], - ) -> (Sc, bool); - fn constraint_count(&self) -> usize; - fn metadata(&self) -> Vec>; - fn evaluate_each<'a>(&'a self, operator: &O, solution: &S) -> Vec>; - fn evaluate_detailed<'a>( - &'a self, - operator: &O, - solution: &S, - ) -> Vec>; - fn reset(&mut self); - fn score_for(&self, _handle: RowHandle) -> Option { - None - } - - /// Weighted counterparts: one multiplier per scorer, in chain order. Each - /// scorer scales its own contribution, so terminals sharing one operator - /// keep independent multipliers while the operator state stays shared. - fn weight_refs<'a>(&'a self, refs: &mut Vec<&'a ConstraintRef>); - fn evaluate_weighted(&self, operator: &O, solution: &S, multipliers: &[u32]) -> Sc; - fn initialize_weighted(&mut self, operator: &O, solution: &S, multipliers: &[u32]) -> Sc; - fn apply_changes_weighted( - &mut self, - operator: &O, - solution: &S, - removed: &[RowHandle], - inserted: &[RowHandle], - multipliers: &[u32], - ) -> (Sc, bool); - fn evaluate_each_weighted<'a>( - &'a self, - operator: &O, - solution: &S, - multipliers: &[u32], - ) -> Vec>; - fn evaluate_detailed_weighted<'a>( - &'a self, - operator: &O, - solution: &S, - multipliers: &[u32], - ) -> Vec>; -} - impl OperatorScorerSet for () where S: Send + Sync + 'static, @@ -355,7 +309,8 @@ where } } -#[doc(hidden)] +/// Two scorer sets over one operator, scored left then right: the chain a +/// shared terminal grows by one [`OperatorScorer`] per named terminal. pub struct ScorerChain(pub(super) T); impl OperatorScorerSet for ScorerChain<(Left, Right)> diff --git a/crates/solverforge-scoring/src/constraint/relational/operator_scorer/set.rs b/crates/solverforge-scoring/src/constraint/relational/operator_scorer/set.rs new file mode 100644 index 00000000..309c29b5 --- /dev/null +++ b/crates/solverforge-scoring/src/constraint/relational/operator_scorer/set.rs @@ -0,0 +1,91 @@ +//! The scorer-set contract shared terminals score through. +//! +//! Weighted counterparts take one multiplier per scorer, in chain order. Each +//! scorer scales its own contribution, so terminals sharing one operator keep +//! independent multipliers while the operator state stays shared. + +use solverforge_core::{score::Score, ConstraintRef}; + +use crate::api::{ + analysis::ConstraintAnalysis, + constraint_set::{ConstraintMetadata, ConstraintResult}, +}; +use crate::stream::relational::operator::Operator; +use crate::stream::relational::RowHandle; + +/// The named terminals scoring one shared operator. +/// +/// A shared terminal advances its operator once per notification and hands +/// the removed and inserted row handles to its scorer set, which re-scores +/// exactly those rows for every terminal. `()` is the empty set, +/// [`OperatorScorer`](super::OperatorScorer) scores one terminal, and +/// [`ScorerChain`](super::ScorerChain) composes two sets in authored order. +/// The methods mirror [`ConstraintSet`](crate::api::constraint_set::ConstraintSet), +/// taking the shared operator as an argument. +pub trait OperatorScorerSet, Sc: Score>: Send + Sync { + /// Sums every terminal over the operator's current rows, without retained state. + fn evaluate(&self, operator: &O, solution: &S) -> Sc; + /// Scores every current row, retains each row's score, and returns the sum. + fn initialize(&mut self, operator: &O, solution: &S) -> Sc; + /// Retracts the retained scores of `removed`, scores `inserted`, and returns + /// the summed delta with an overflow flag. + fn apply_changes( + &mut self, + operator: &O, + solution: &S, + removed: &[RowHandle], + inserted: &[RowHandle], + ) -> (Sc, bool); + /// Number of named terminals in the set. + fn constraint_count(&self) -> usize; + /// One metadata entry per terminal, in authored order. + fn metadata(&self) -> Vec>; + /// Each terminal's total, in authored order. + fn evaluate_each<'a>(&'a self, operator: &O, solution: &S) -> Vec>; + /// Each terminal's total with its matches, in authored order. + fn evaluate_detailed<'a>( + &'a self, + operator: &O, + solution: &S, + ) -> Vec>; + /// Drops every retained row score. + fn reset(&mut self); + /// The retained signed score of row `handle` for a single-terminal set; + /// `None` when the set does not score rows on its own. + fn score_for(&self, _handle: RowHandle) -> Option { + None + } + + /// Appends each terminal's constraint reference, one weight slot each. + fn weight_refs<'a>(&'a self, refs: &mut Vec<&'a ConstraintRef>); + /// [`evaluate`](Self::evaluate) with each terminal scaled by its multiplier. + fn evaluate_weighted(&self, operator: &O, solution: &S, multipliers: &[u32]) -> Sc; + /// [`initialize`](Self::initialize) with each terminal scaled by its multiplier. + fn initialize_weighted(&mut self, operator: &O, solution: &S, multipliers: &[u32]) -> Sc; + /// [`apply_changes`](Self::apply_changes) with each terminal scaled by its + /// multiplier. + fn apply_changes_weighted( + &mut self, + operator: &O, + solution: &S, + removed: &[RowHandle], + inserted: &[RowHandle], + multipliers: &[u32], + ) -> (Sc, bool); + /// [`evaluate_each`](Self::evaluate_each) with each terminal scaled by its + /// multiplier. + fn evaluate_each_weighted<'a>( + &'a self, + operator: &O, + solution: &S, + multipliers: &[u32], + ) -> Vec>; + /// [`evaluate_detailed`](Self::evaluate_detailed) with each terminal scaled + /// by its multiplier. + fn evaluate_detailed_weighted<'a>( + &'a self, + operator: &O, + solution: &S, + multipliers: &[u32], + ) -> Vec>; +} diff --git a/crates/solverforge-scoring/src/stream/mod.rs b/crates/solverforge-scoring/src/stream/mod.rs index a2a37249..509b9d85 100644 --- a/crates/solverforge-scoring/src/stream/mod.rs +++ b/crates/solverforge-scoring/src/stream/mod.rs @@ -154,7 +154,6 @@ pub use penta_stream::{PentaConstraintBuilder, PentaConstraintStream}; pub use projected_stream::{Projection, ProjectionSink}; pub use quad_stream::{QuadConstraintBuilder, QuadConstraintStream}; pub use tri_stream::{TriConstraintBuilder, TriConstraintStream}; -#[doc(hidden)] pub use unassigned::UnassignedEntity; pub use uni_stream::{UniConstraintBuilder, UniConstraintStream}; pub use weighting_support::{fixed_weight, hard_weight, ConstraintWeight, FixedWeight, HardWeight}; diff --git a/crates/solverforge-scoring/src/stream/projected_stream/source.rs b/crates/solverforge-scoring/src/stream/projected_stream/source.rs index 7fb9b4eb..faf56e3e 100644 --- a/crates/solverforge-scoring/src/stream/projected_stream/source.rs +++ b/crates/solverforge-scoring/src/stream/projected_stream/source.rs @@ -88,22 +88,31 @@ fn emitted_past_bound(projection: &str, max_emits: usize) -> ! { ); } -#[doc(hidden)] +/// One entity a projected row is derived from: which of the source's +/// collections it lives in and its index there. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct RowOwner { + /// The source collection slot, `0..Source::source_count()`. pub source_slot: usize, + /// The entity's index in that collection. pub entity_index: usize, } -#[doc(hidden)] +/// The stable identity of one projected row: the entity or pair of entities +/// it comes from and its position among their emissions. Ordering is by +/// coordinate, which keeps projected self-joins independent of storage. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct RowCoordinate { + /// The owner the row's index-based filters see. pub primary_owner: RowOwner, + /// The second owner of a row emitted for a joined pair. pub secondary_owner: Option, + /// The row's position among the rows emitted for its owners. pub emit_index: usize, } impl RowCoordinate { + /// A row emitted for one entity. #[inline] pub fn single(source_slot: usize, entity_index: usize, emit_index: usize) -> Self { Self { @@ -116,6 +125,7 @@ impl RowCoordinate { } } + /// A row emitted for a joined pair of entities. #[inline] pub fn pair( left_slot: usize, @@ -137,6 +147,7 @@ impl RowCoordinate { } } + /// Visits each distinct owner, primary first. #[inline] pub fn for_each_owner(&self, mut visit: V) where @@ -150,6 +161,8 @@ impl RowCoordinate { } } + /// Shifts every owner's source slot by `offset`, as a merge does for its + /// right input. #[inline] pub fn offset_source_slots(mut self, offset: usize) -> Self { self.primary_owner.source_slot += offset; @@ -160,18 +173,32 @@ impl RowCoordinate { } } -#[doc(hidden)] +/// The producer behind a projected stream: one or more entity collections +/// and the rows they project to. +/// +/// A projected stream's operator tree retains rows by [`RowCoordinate`] and, +/// on an entity notification, asks the source for that entity's rows again. +/// Single-source projections, filters, merges and joined pairs implement it; +/// the fluent `project`, `filter`, `merge` and `join` methods build them. pub trait Source: Send + Sync { + /// State the source keeps beside the retained rows, such as a join index. type State: Send + Sync; + /// The most rows the source emits for one owner (see + /// [`Projection::MAX_EMITS`]). const MAX_EMITS: usize; + /// Number of entity collections the source reads. fn source_count(&self) -> usize; + /// How changes reach collection `slot`. fn change_source(&self, slot: usize) -> ChangeSource; + /// Builds the source state for `solution`. fn build_state(&self, solution: &S) -> Self::State; + /// Visits every row of `solution` with its coordinate. fn collect_all(&self, solution: &S, state: &Self::State, visit: V) where V: FnMut(RowCoordinate, Out); + /// Visits the rows owned by entity `entity_index` of collection `slot`. fn collect_entity( &self, solution: &S, @@ -181,6 +208,7 @@ pub trait Source: Send + Sync { visit: V, ) where V: FnMut(RowCoordinate, Out); + /// Adds entity `entity_index` of collection `slot` to the source state. fn insert_entity_state( &self, solution: &S, @@ -188,6 +216,8 @@ pub trait Source: Send + Sync { slot: usize, entity_index: usize, ); + /// Removes entity `entity_index` of collection `slot` from the source + /// state. fn retract_entity_state( &self, solution: &S, diff --git a/crates/solverforge-scoring/src/stream/relational/operator/analysis.rs b/crates/solverforge-scoring/src/stream/relational/operator/analysis.rs index 578a8e17..98bf33c4 100644 --- a/crates/solverforge-scoring/src/stream/relational/operator/analysis.rs +++ b/crates/solverforge-scoring/src/stream/relational/operator/analysis.rs @@ -3,8 +3,18 @@ use super::{super::Leaf, Pair}; use crate::api::analysis::EntityRef; use std::fmt::Debug; -#[doc(hidden)] +/// A row view that can name the entities it was built from, for score +/// explanation. +/// +/// Terminals call it on the cold explanation path only (`get_matches`, +/// detailed analysis), never while scoring. Leaves push their entity, pairs +/// and arrays push each part in order, and grouped, projected, flattened and +/// complemented views push the rows they derive from. Implement it for the +/// `View` of a custom [`Operator`](super::Operator) so terminals over that +/// operator can justify their matches. pub trait ExplainRow { + /// Appends a reference to every entity this row was built from to + /// `entities`, in row order. fn explain(&self, entities: &mut Vec); } impl ExplainRow for Leaf<'_, T> { diff --git a/crates/solverforge-scoring/src/stream/unassigned.rs b/crates/solverforge-scoring/src/stream/unassigned.rs index 6f2ca632..b7ceaffb 100644 --- a/crates/solverforge-scoring/src/stream/unassigned.rs +++ b/crates/solverforge-scoring/src/stream/unassigned.rs @@ -4,8 +4,57 @@ use super::collection_extract::CollectionExtract; use super::filter::{AndUniFilter, FnUniFilter, UniFilter}; use super::UniConstraintStream; -#[doc(hidden)] +/// An entity that knows whether its planning variable is unassigned, which +/// enables [`UniConstraintStream::unassigned`] on streams over it. +/// +/// `#[planning_entity]` implements it for an entity with exactly one +/// `Option<_>` planning variable: the entity is unassigned while that field is +/// `None`. Implement it by hand for an entity whose "unassigned" state is +/// something else, such as several optional variables or a sentinel value. +/// +/// # Examples +/// +/// ``` +/// use solverforge_core::score::SoftScore; +/// use solverforge_scoring::api::constraint_set::IncrementalConstraint; +/// use solverforge_scoring::stream::{ +/// source, ChangeSource, ConstraintFactory, UnassignedEntity, +/// }; +/// +/// /// A shift whose `employee` is `usize::MAX` until it is assigned. +/// #[derive(Clone, Debug)] +/// struct Shift { +/// employee: usize, +/// } +/// +/// #[derive(Clone)] +/// struct Roster { +/// shifts: Vec, +/// } +/// +/// impl UnassignedEntity for Shift { +/// fn is_unassigned(_roster: &Roster, shift: &Self) -> bool { +/// shift.employee == usize::MAX +/// } +/// } +/// +/// fn shifts(roster: &Roster) -> &[Shift] { +/// roster.shifts.as_slice() +/// } +/// +/// let constraint = ConstraintFactory::::new() +/// .for_each(source(shifts as fn(&Roster) -> &[Shift], ChangeSource::Descriptor(0))) +/// .unassigned() +/// .penalize(SoftScore::of(1)) +/// .named("unassigned shift"); +/// +/// let roster = Roster { +/// shifts: vec![Shift { employee: 0 }, Shift { employee: usize::MAX }], +/// }; +/// assert_eq!(constraint.evaluate(&roster), SoftScore::of(-1)); +/// ``` pub trait UnassignedEntity: Clone + Send + Sync + 'static { + /// Whether `entity` is unassigned in `solution`. fn is_unassigned(solution: &S, entity: &Self) -> bool; } @@ -18,7 +67,7 @@ where Sc: Score + 'static, { /// Keeps only entities whose planning variable is unassigned, as reported by - /// the macro-generated `UnassignedEntity` impl. + /// their [`UnassignedEntity`] implementation. pub fn unassigned( self, ) -> UniConstraintStream bool>>, Sc> { diff --git a/crates/solverforge-solver/src/heuristic/selector/move_selector/borrowed.rs b/crates/solverforge-solver/src/heuristic/selector/move_selector/borrowed.rs index 9ccf7c19..d482c815 100644 --- a/crates/solverforge-solver/src/heuristic/selector/move_selector/borrowed.rs +++ b/crates/solverforge-solver/src/heuristic/selector/move_selector/borrowed.rs @@ -434,43 +434,32 @@ pub trait MoveCursor> { /// ownership. Recursive composition therefore uses this private companion /// contract and lends the live solution and its resource only to the child /// selected by the canonical union scheduler. -#[doc(hidden)] +/// +/// The trait is not re-exported, so code outside the crate cannot name or +/// implement it; it is reachable only as the bound of the local-search +/// cursor source behind `local_search`. pub trait ResourceMoveCursor, Resources> { + /// Discovers the next candidate, lending `resources` to the child that + /// produces it; `None` once the stream is exhausted. fn next_candidate_with_resources>( &mut self, director: &mut D, resources: &mut Resources, ) -> Option; + /// Borrows the live candidate `id`. fn candidate(&self, id: CandidateId) -> Option>; + /// Takes ownership of candidate `id`. fn take_candidate(&mut self, id: CandidateId) -> M; - fn next_owned_candidate_inspected_with_resources>( - &mut self, - director: &mut D, - resources: &mut Resources, - mut inspect: F, - ) -> Option<(M, T)> - where - F: for<'a> FnMut(MoveCandidateRef<'a, S, M>) -> Option, - { - loop { - let id = self.next_candidate_with_resources(director, resources)?; - let candidate = self - .candidate(id) - .expect("newly generated resource cursor candidate must remain live"); - if let Some(metadata) = inspect(candidate) { - return Some((self.take_candidate(id), metadata)); - } - assert!(self.release_candidate(id)); - } - } - + /// Applies candidate `id` to `score_director` by ownership. fn apply_owned_candidate>(&mut self, id: CandidateId, score_director: &mut D); + /// Drops candidate `id`; `false` when it was not live. fn release_candidate(&mut self, id: CandidateId) -> bool; + /// Index of the child selector that produced `id`. fn selector_index(&self, id: CandidateId) -> Option; }