solverforge-scoring 0.8.6

Incremental constraint scoring for SolverForge
Documentation
use solverforge_core::score::SoftScore;

use crate::api::constraint_set::IncrementalConstraint;
use crate::stream::collection_extract::ChangeSource;
use crate::stream::joiner::equal_bi;
use crate::stream::ConstraintFactory;

#[derive(Clone)]
struct Task {
    assignee: Option<usize>,
}

#[derive(Clone)]
struct Worker {
    id: usize,
    available: bool,
}

#[derive(Clone)]
struct TaskSchedule {
    tasks: Vec<Task>,
    workers: Vec<Worker>,
}

fn tasks(schedule: &TaskSchedule) -> &[Task] {
    schedule.tasks.as_slice()
}

fn workers(schedule: &TaskSchedule) -> &[Worker] {
    schedule.workers.as_slice()
}

#[test]
fn test_exists_updates_all_matching_a_entities_when_b_descriptor_changes() {
    let mut constraint = ConstraintFactory::<TaskSchedule, SoftScore>::new()
        .for_each_tracked(tasks as fn(&TaskSchedule) -> &[Task], ChangeSource::Static)
        .filter(|task: &Task| task.assignee.is_some())
        .if_exists((
            ConstraintFactory::<TaskSchedule, SoftScore>::new()
                .for_each_tracked(
                    workers as fn(&TaskSchedule) -> &[Worker],
                    ChangeSource::Descriptor(0),
                )
                .filter(|worker: &Worker| !worker.available),
            equal_bi(
                |task: &Task| task.assignee,
                |worker: &Worker| Some(worker.id),
            ),
        ))
        .penalize(SoftScore::of(1))
        .named("unavailable worker");

    let mut schedule = TaskSchedule {
        tasks: vec![
            Task { assignee: Some(0) },
            Task { assignee: Some(0) },
            Task { assignee: Some(1) },
        ],
        workers: vec![
            Worker {
                id: 0,
                available: true,
            },
            Worker {
                id: 1,
                available: true,
            },
        ],
    };

    let mut total = constraint.initialize(&schedule);
    assert_eq!(total, SoftScore::of(0));

    total = total + constraint.on_retract(&schedule, 0, 0);
    schedule.workers[0].available = false;
    total = total + constraint.on_insert(&schedule, 0, 0);

    assert_eq!(total, constraint.evaluate(&schedule));
    assert_eq!(total, SoftScore::of(-2));
}

#[derive(Clone)]
struct CustomerState {
    customers: Vec<usize>,
    routes: Vec<Vec<usize>>,
}

fn customers(state: &CustomerState) -> &[usize] {
    state.customers.as_slice()
}

fn routes(state: &CustomerState) -> &[Vec<usize>] {
    state.routes.as_slice()
}

#[test]
fn test_flattened_not_exists_updates_all_matching_a_entities_when_route_changes() {
    let mut constraint = ConstraintFactory::<CustomerState, SoftScore>::new()
        .for_each_tracked(
            customers as fn(&CustomerState) -> &[usize],
            ChangeSource::Static,
        )
        .if_not_exists((
            ConstraintFactory::<CustomerState, SoftScore>::new()
                .for_each_tracked(
                    routes as fn(&CustomerState) -> &[Vec<usize>],
                    ChangeSource::Descriptor(0),
                )
                .flattened(|route: &Vec<usize>| route),
            equal_bi(|customer: &usize| *customer, |assigned: &usize| *assigned),
        ))
        .penalize(SoftScore::of(1))
        .named("missing assignment");

    let mut state = CustomerState {
        customers: vec![1, 2, 3],
        routes: vec![Vec::new()],
    };

    let mut total = constraint.initialize(&state);
    assert_eq!(total, SoftScore::of(-3));

    total = total + constraint.on_retract(&state, 0, 0);
    state.routes[0] = vec![1, 2, 3];
    total = total + constraint.on_insert(&state, 0, 0);

    assert_eq!(total, constraint.evaluate(&state));
    assert_eq!(total, SoftScore::of(0));
}

#[derive(Clone)]
struct TaggedItem {
    key: usize,
    enabled: bool,
}

#[derive(Clone)]
struct TaggedItems {
    items: Vec<TaggedItem>,
}

fn tagged_items(state: &TaggedItems) -> &[TaggedItem] {
    state.items.as_slice()
}

#[test]
fn test_exists_same_source_updates_consistently() {
    let mut constraint = ConstraintFactory::<TaggedItems, SoftScore>::new()
        .for_each_tracked(
            tagged_items as fn(&TaggedItems) -> &[TaggedItem],
            ChangeSource::Descriptor(0),
        )
        .if_exists((
            ConstraintFactory::<TaggedItems, SoftScore>::new()
                .for_each_tracked(
                    tagged_items as fn(&TaggedItems) -> &[TaggedItem],
                    ChangeSource::Descriptor(0),
                )
                .filter(|item: &TaggedItem| item.enabled),
            equal_bi(|item: &TaggedItem| item.key, |item: &TaggedItem| item.key),
        ))
        .penalize(SoftScore::of(1))
        .named("key has enabled peer");

    let mut state = TaggedItems {
        items: vec![
            TaggedItem {
                key: 1,
                enabled: false,
            },
            TaggedItem {
                key: 1,
                enabled: true,
            },
        ],
    };

    let mut total = constraint.initialize(&state);
    assert_eq!(total, SoftScore::of(-2));

    total = total + constraint.on_retract(&state, 1, 0);
    state.items[1].enabled = false;
    total = total + constraint.on_insert(&state, 1, 0);

    assert_eq!(total, constraint.evaluate(&state));
    assert_eq!(total, SoftScore::of(0));
}