solverforge-scoring 0.19.10

Incremental constraint scoring for SolverForge
Documentation
use crate::api::constraint_set::IncrementalConstraint;
use crate::constraint::relational::OperatorTerminal;
use crate::stream::collection_extract::{source, ChangeSource};
use crate::stream::collector::{count, CountAccumulator};
use crate::stream::joiner::equal_bi;
use crate::stream::relational::{
    operator::{CollectionNode, ComplementNode, ComplementView, GroupNode, GroupView, Operator},
    Leaf,
};
use solverforge_core::{score::SoftScore, ConstraintRef, ImpactType};
#[derive(Clone, Debug)]
struct Entity {
    key: u32,
}
struct Payload(i64);
struct Model {
    entities: Vec<Entity>,
    targets: Vec<u32>,
}
fn entities(m: &Model) -> &[Entity] {
    &m.entities
}
fn targets(m: &Model) -> &[u32] {
    &m.targets
}
fn key(row: &Leaf<'_, Entity>) -> u32 {
    row.entity.key
}
fn target_key(row: &Leaf<'_, u32>) -> u32 {
    *row.entity
}
fn group_key<O: Operator<Model>>(
    row: &GroupView<'_, Model, O, u32, CountAccumulator, (), usize>,
) -> u32 {
    *row.key
}
fn default(_: &Model, _: &Leaf<'_, u32>) -> Payload {
    Payload(-10)
}
fn weight<O: Operator<Model>>(
    _: &Model,
    row: &ComplementView<
        '_,
        Leaf<'_, u32>,
        GroupView<'_, Model, O, u32, CountAccumulator, (), usize>,
        Payload,
    >,
) -> SoftScore {
    match row {
        ComplementView::Real(pair) => pair.right.with_result(|r| SoftScore::of(*r as i64 * 5)),
        ComplementView::Default(projected) => SoftScore::of(projected.value.0),
    }
}
#[test]
fn complement_replaces_defaults_and_real_group_rows_without_clone_or_duplicate_scores() {
    let group = GroupNode::new(
        CollectionNode::new(
            source(
                entities as fn(&Model) -> &[Entity],
                ChangeSource::Descriptor(0),
            ),
            0,
        ),
        key,
        count(),
    );
    let targets = CollectionNode::new(
        source(targets as fn(&Model) -> &[u32], ChangeSource::Descriptor(1)),
        1,
    );
    let tree = ComplementNode::new(targets, group, equal_bi(target_key, group_key), default);
    let mut c = OperatorTerminal::new(
        ConstraintRef::new("test", "complement"),
        ImpactType::Reward,
        tree,
        weight,
        false,
    );
    let mut m = Model {
        entities: vec![Entity { key: 1 }],
        targets: vec![1, 1, 2],
    };
    assert_eq!(c.evaluate(&m), SoftScore::of(0));
    assert_eq!(c.match_count(&m), 3);
    let mut score = c.initialize(&m);
    score = score + c.on_retract(&m, 0, 0);
    assert_eq!(score, SoftScore::of(-30));
    assert_eq!(c.on_retract(&m, 0, 0), SoftScore::of(0));
    m.entities[0].key = 2;
    assert_eq!(c.evaluate(&m), SoftScore::of(-15));
    score = score + c.on_insert(&m, 0, 0);
    assert_eq!(score, SoftScore::of(-15));
    let matches = c.get_matches(&m);
    assert_eq!(matches.len(), 3);
    let mut lineage_sizes = matches
        .iter()
        .map(|m| m.justification.entities.len())
        .collect::<Vec<_>>();
    lineage_sizes.sort();
    assert_eq!(lineage_sizes, vec![1, 1, 2]);
    score = score + c.on_retract(&m, 0, 1);
    m.targets[0] = 2;
    score = score + c.on_insert(&m, 0, 1);
    assert_eq!(score, SoftScore::of(0));
    assert_eq!(score, c.evaluate(&m));
    c.reset();
    assert_eq!(c.evaluate(&m), score);
    assert_eq!(c.initialize(&m), score);
}

fn output_key<O: Operator<Model>>(
    row: &ComplementView<
        '_,
        Leaf<'_, u32>,
        GroupView<'_, Model, O, u32, CountAccumulator, (), usize>,
        Payload,
    >,
) -> u32 {
    match row {
        ComplementView::Real(pair) => *pair.left.entity,
        ComplementView::Default(projected) => *projected.input.entity,
    }
}
#[test]
fn complemented_rows_remain_joinable_through_repeated_target_descriptor_churn() {
    use crate::stream::relational::operator::{JoinNode, Pair};
    fn joined_weight<O: Operator<Model>>(
        m: &Model,
        row: &Pair<
            ComplementView<
                '_,
                Leaf<'_, u32>,
                GroupView<'_, Model, O, u32, CountAccumulator, (), usize>,
                Payload,
            >,
            Leaf<'_, u32>,
        >,
    ) -> SoftScore {
        weight(m, &row.left)
    }
    let group = GroupNode::new(
        CollectionNode::new(
            source(
                entities as fn(&Model) -> &[Entity],
                ChangeSource::Descriptor(0),
            ),
            0,
        ),
        key,
        count(),
    );
    let target = |binding| {
        CollectionNode::new(
            source(targets as fn(&Model) -> &[u32], ChangeSource::Descriptor(1)),
            binding,
        )
    };
    let complemented =
        ComplementNode::new(target(1), group, equal_bi(target_key, group_key), default);
    let tree = JoinNode::new(complemented, target(2), equal_bi(output_key, target_key));
    let mut c = OperatorTerminal::new(
        ConstraintRef::new("test", "joined complement"),
        ImpactType::Reward,
        tree,
        joined_weight,
        false,
    );
    let mut m = Model {
        entities: vec![Entity { key: 1 }, Entity { key: 1 }],
        targets: vec![1, 1, 2],
    };
    let oracle = |m: &Model| {
        let mut total = 0;
        let mut matches = 0;
        for a in &m.targets {
            let count = m.entities.iter().filter(|e| e.key == *a).count();
            let score = if count == 0 { -10 } else { count as i64 * 5 };
            for b in &m.targets {
                if a == b {
                    total += score;
                    matches += 1;
                }
            }
        }
        (SoftScore::of(total), matches)
    };
    let mut score = c.initialize(&m);
    for step in 0..90 {
        let descriptor = step % 2;
        let index = (step / 2) % if descriptor == 0 { 2 } else { 3 };
        score = score + c.on_retract(&m, index, descriptor);
        if descriptor == 0 {
            m.entities[index].key = (step % 4) as u32;
        } else {
            m.targets[index] = (step % 4) as u32;
        }
        score = score + c.on_insert(&m, index, descriptor);
        assert_eq!(score, oracle(&m).0);
        assert_eq!(c.evaluate(&m), score);
        assert_eq!(c.match_count(&m), oracle(&m).1);
        assert_eq!(
            c.get_matches(&m)
                .iter()
                .fold(SoftScore::of(0), |s, m| s + m.score),
            score
        );
    }
}