Skip to main content

solverforge_scoring/stream/
existence_stream.rs

1/* Semi/anti existence streams over the composable operator tree.
2
3An existence constraint is a semi/anti join: it retains only left rows whose
4matching-right count is nonzero (or zero, for `NotExists`). There is no third
5retention engine — the relationship is a compiled `EqualJoiner` over the two
6operators' views, the left is a collection over the left stream (whose
7membership is the stream's own filter), the right is a collection over the
8right stream or a `FlattenNode` over it, and scoring is the generic
9`OperatorTerminal`.
10*/
11
12use std::marker::PhantomData;
13
14use solverforge_core::score::Score;
15use solverforge_core::{ConstraintRef, ImpactType};
16
17use super::relational::operator::{ExistenceNode, Operator};
18use super::relational::Leaf;
19use super::weighting_support::ConstraintWeight;
20use crate::constraint::relational::OperatorTerminal;
21use crate::stream::joiner::plan::{CompileCondition, ExecutablePlan, IndexedPlan};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum ExistenceMode {
25    Exists,
26    NotExists,
27}
28
29/* Zero-erasure existence stream: a left operator, a right operator, and the
30compiled relationship between them. `named()` builds the `ExistenceNode` and
31its generic terminal. */
32pub struct ExistsConstraintStream<S, A, L, R, C, Sc>
33where
34    C: CompileCondition,
35    Sc: Score,
36{
37    pub(super) mode: ExistenceMode,
38    pub(super) left: L,
39    pub(super) right: R,
40    pub(super) condition: C,
41    pub(super) _phantom: PhantomData<(fn() -> S, fn() -> A, fn() -> Sc)>,
42}
43
44impl<S, A, L, R, C, Sc> ExistsConstraintStream<S, A, L, R, C, Sc>
45where
46    S: Send + Sync + 'static,
47    A: Clone + Send + Sync + 'static,
48    L: Operator<S>,
49    for<'a> L: Operator<S, View<'a> = Leaf<'a, A>>,
50    C: CompileCondition,
51    Sc: Score + 'static,
52{
53    pub(super) fn new(mode: ExistenceMode, left: L, right: R, condition: C) -> Self {
54        Self {
55            mode,
56            left,
57            right,
58            condition,
59            _phantom: PhantomData,
60        }
61    }
62
63    fn into_weighted_builder<W>(
64        self,
65        impact_type: ImpactType,
66        weight: W,
67        is_hard: bool,
68    ) -> ExistsConstraintBuilder<S, A, L, R, C, W, Sc>
69    where
70        W: for<'a> Fn(&S, &Leaf<'a, A>) -> Sc,
71    {
72        ExistsConstraintBuilder {
73            mode: self.mode,
74            left: self.left,
75            right: self.right,
76            condition: self.condition,
77            impact_type,
78            weight,
79            is_hard,
80            _phantom: PhantomData,
81        }
82    }
83
84    pub fn penalize<W>(
85        self,
86        weight: W,
87    ) -> ExistsConstraintBuilder<
88        S,
89        A,
90        L,
91        R,
92        C,
93        impl for<'a> Fn(&S, &Leaf<'a, A>) -> Sc + Send + Sync,
94        Sc,
95    >
96    where
97        W: for<'w> ConstraintWeight<(&'w A,), Sc> + Send + Sync,
98    {
99        let is_hard = weight.is_hard();
100        self.into_weighted_builder(
101            ImpactType::Penalty,
102            move |_s: &S, row: &Leaf<'_, A>| weight.score((row.entity,)),
103            is_hard,
104        )
105    }
106
107    pub fn reward<W>(
108        self,
109        weight: W,
110    ) -> ExistsConstraintBuilder<
111        S,
112        A,
113        L,
114        R,
115        C,
116        impl for<'a> Fn(&S, &Leaf<'a, A>) -> Sc + Send + Sync,
117        Sc,
118    >
119    where
120        W: for<'w> ConstraintWeight<(&'w A,), Sc> + Send + Sync,
121    {
122        let is_hard = weight.is_hard();
123        self.into_weighted_builder(
124            ImpactType::Reward,
125            move |_s: &S, row: &Leaf<'_, A>| weight.score((row.entity,)),
126            is_hard,
127        )
128    }
129}
130
131pub struct ExistsConstraintBuilder<S, A, L, R, C, W, Sc>
132where
133    C: CompileCondition,
134    Sc: Score,
135{
136    mode: ExistenceMode,
137    left: L,
138    right: R,
139    condition: C,
140    impact_type: ImpactType,
141    weight: W,
142    is_hard: bool,
143    _phantom: PhantomData<(fn() -> S, fn() -> A, fn() -> Sc)>,
144}
145
146impl<S, A, L, R, C, W, Sc> ExistsConstraintBuilder<S, A, L, R, C, W, Sc>
147where
148    S: Send + Sync + 'static,
149    A: 'static,
150    L: Operator<S>,
151    for<'a> L: Operator<S, View<'a> = Leaf<'a, A>>,
152    R: Operator<S>,
153    C: CompileCondition + 'static,
154    C::Plan: IndexedPlan + 'static,
155    for<'a> C::Plan: ExecutablePlan<L::View<'a>, R::View<'a>>,
156    W: for<'a> Fn(&S, &Leaf<'a, A>) -> Sc + Send + Sync,
157    Sc: Score + 'static,
158{
159    pub fn named(
160        self,
161        name: &str,
162    ) -> OperatorTerminal<
163        S,
164        ExistenceNode<S, L, R, C::Plan>,
165        impl for<'a> Fn(&S, &L::View<'a>) -> Sc + Send + Sync,
166        Sc,
167    > {
168        let exists = matches!(self.mode, ExistenceMode::Exists);
169        let node = ExistenceNode::new(self.left, self.right, self.condition, exists);
170        OperatorTerminal::new(
171            ConstraintRef::new("", name),
172            self.impact_type,
173            node,
174            self.weight,
175            self.is_hard,
176        )
177    }
178}
179
180impl<S, A, L, R, C, Sc: Score> std::fmt::Debug for ExistsConstraintStream<S, A, L, R, C, Sc>
181where
182    C: CompileCondition,
183{
184    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185        f.debug_struct("ExistsConstraintStream")
186            .field("mode", &self.mode)
187            .finish()
188    }
189}
190
191impl<S, A, L, R, C, W, Sc: Score> std::fmt::Debug for ExistsConstraintBuilder<S, A, L, R, C, W, Sc>
192where
193    C: CompileCondition,
194{
195    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        f.debug_struct("ExistsConstraintBuilder")
197            .field("mode", &self.mode)
198            .field("impact_type", &self.impact_type)
199            .finish()
200    }
201}