oximo-core 0.7.0

Core modeling types (Variable, Set, Constraint, Model) for oximo
Documentation
#![allow(clippy::float_cmp, reason = "These sums of small integers are exactly representable")]

use std::cell::Cell;
use std::panic::{AssertUnwindSafe, catch_unwind};

use oximo_core::prelude::*;
use oximo_expr::{evaluate, extract_linear};

fn value(expr: Expr<'_>) -> f64 {
    evaluate(&expr.arena.borrow(), expr.id, &&[1.0, 2.0, 3.0][..]).unwrap()
}

#[test]
fn empty_domains_and_filters_do_not_evaluate_the_body() {
    fn forbidden<'a>() -> Expr<'a> {
        panic!("empty sum body must not run");
    }
    let m = Model::new("empty");
    let empty: &[usize] = &[];
    let terms = [
        sum!(m, forbidden() for _i in 0..0),
        sum!(&m, forbidden() for _i in empty),
        sum!(m, forbidden() for i in 0..3 if i > 10),
    ];
    for expr in terms {
        assert_eq!(value(expr), 0.0);
        assert!(std::ptr::eq(expr.arena, m.__sum_context()));
    }
}

#[test]
fn anchored_sums_reject_foreign_terms_before_emitting_nodes() {
    let m = Model::new("local");
    let other = Model::new("foreign");
    variable!(m, x);
    variable!(other, y);
    for filtered in [false, true] {
        for mixed in [false, true] {
            let before_local = m.arena().len();
            let before_foreign = other.arena().len();
            let mut visited = Vec::new();
            let result = catch_unwind(AssertUnwindSafe(|| {
                if filtered {
                    sum!(m, { visited.push(i); if mixed && i == 0 { x } else { y } }
                        for i in 0..3 if true)
                } else {
                    sum!(m, { visited.push(i); if mixed && i == 0 { x } else { y } }
                        for i in 0..3)
                }
            }));
            assert!(result.is_err());
            assert_eq!(visited, [0, 1, 2]);
            assert_eq!(m.arena().len(), before_local);
            assert_eq!(other.arena().len(), before_foreign);
        }
    }
    assert!(catch_unwind(AssertUnwindSafe(|| sum!(m, y for _i in 0..1))).is_err());
}

#[test]
fn generated_identifiers_do_not_capture_caller_bindings() {
    let m = Model::new("hygiene");
    variable!(m, x);
    let __oximo_sum_model = x;
    let __terms = x;
    let __oximo_model_arena = x;
    let __oximo_model_receiver = x;
    assert_eq!(value(sum!(m, __oximo_sum_model for _i in 0..2)), 2.0);
    assert_eq!(value(sum!(m, __terms for i in 0..3 if i < 2)), 2.0);
    assert_eq!(value(sum!(__terms for i in 0..3 if i < 2)), 2.0);
    objective!(m, Min, sum!(__oximo_model_arena + __oximo_model_receiver for _i in 0..2));
    let objective = m.try_objective().unwrap();
    assert_eq!(evaluate(&m.arena(), objective.expr, &&[1.0][..]).unwrap(), 4.0);
}

#[test]
fn nested_and_multi_index_sums_inherit_the_anchor() {
    let m = Model::new("nested");
    variable!(m, x[j in 0..3]);
    let flat = sum!(m, x[j] for i in 0..4, j in 0..i);
    let nested = sum!(m, sum!(x[j] for j in 0..i) for i in 0..4);
    let explicit = sum!(m, sum!(&m, x[j] for j in 0..i) for i in 0..4);
    let block = sum!(m, { let end = i; sum!(x[j] for j in 0..end) } for i in 0..4);
    let filtered = sum!(m, sum!(x[j] for j in 0..3 if j < i) for i in 0..4);
    for expr in [flat, nested, explicit, filtered, block] {
        assert_eq!(value(expr), 10.0);
    }
    assert_eq!(value(sum!(m, x[j] for _i in 0..3, j in 0..0)), 0.0);
}

#[test]
fn model_macros_supply_context_including_qualified_sums() {
    let m = Model::new("implicit");
    variable!(m, x);
    let named = constraint!(m, named, sum!(x for _i in 0..0) <= 3.0);
    let anonymous = constraint!(m, sum!(x for i in 0..2 if i > 10) == 0.0);
    let computed = constraint!(m, name = "computed", sum!(x for _i in 0..0) >= -1.0);
    let range = constraint!(m, band, 0.0 <= sum!(x for _i in 0..0) <= 1.0);
    objective!(m, Min, 2.0 + oximo_core::sum!(sum!(x for _j in 0..0) for _i in 0..2));
    let arena = m.arena();
    let constraints = m.constraints();
    for id in [named, anonymous, computed] {
        let terms = extract_linear(&arena, constraints.algebraic()[id.index()].lhs).unwrap();
        assert!(terms.coeffs.is_empty());
    }
    assert!(matches!(range, RangeConstraintHandles::Interval(_)));
    assert_eq!(evaluate(&arena, m.try_objective().unwrap().expr, &&[][..]).unwrap(), 2.0);
}

#[test]
fn indexed_sums_work_with_serial_and_parallel_arena_forks() {
    for threads in [1, 4] {
        rayon::ThreadPoolBuilder::new().num_threads(threads).build().unwrap().install(|| {
            let m = Model::new("families");
            variable!(m, x[j in 0..3]);
            let alias = &m;
            let implicit = constraint!(m, implicit[i in 0..1024],
                sum!(x[j] for j in 0..i % 4) <= 10.0);
            let explicit = constraint!(m, explicit[i in 0..1024],
                sum!(&m, x[j] for j in 0..i % 4) <= 10.0);
            let aliased = constraint!(m, aliased[i in 0..1024],
                sum!(alias, x[j] for j in 0..i % 4) <= 10.0);
            let ranged = constraint!(m, ranged[i in 0..1024],
                0.0 <= sum!(x[j] for j in 0..3 if j < i % 4) <= 10.0);
            soc_constraint!(m, cones[i in 0..512],
                [sum!(m, x[j] for j in 0..i % 4)] <= 10.0 * x[2]);
            let arena = m.arena();
            let constraints = m.constraints();
            for i in 0..1024_usize {
                let RangeConstraintHandles::Interval(range) = ranged.get(i).unwrap() else {
                    panic!("expected an interval row");
                };
                for id in [
                    implicit.get(i).unwrap(),
                    explicit.get(i).unwrap(),
                    aliased.get(i).unwrap(),
                    range,
                ] {
                    let terms =
                        extract_linear(&arena, constraints.algebraic()[id.index()].lhs).unwrap();
                    assert_eq!(terms.coeffs.len(), i % 4);
                    assert!(terms.coeffs.iter().all(|(_, coefficient)| *coefficient == 1.0));
                }
            }
        });
    }
}

#[test]
fn model_expressions_are_evaluated_once_and_bodies_keep_order() {
    let m = Model::new("evaluation_order");
    variable!(m, x);
    let calls = Cell::new(0);
    let get_model = || {
        calls.set(calls.get() + 1);
        &m
    };
    let mut visited = Vec::new();
    let total = sum!(get_model(), { visited.push(i); x } for i in 0..3);
    assert_eq!(calls.get(), 1);
    assert_eq!(visited, [0, 1, 2]);
    assert_eq!(value(total), 3.0);
    constraint!(get_model(), c[i in 0..4], sum!(x for _j in 0..i) <= 10.0);
    assert_eq!(calls.get(), 2);
    objective!(get_model(), Min, sum!(x for _j in 0..0));
    assert_eq!(calls.get(), 3);
}

#[test]
fn explicit_model_anchors_respect_local_shadowing() {
    let m = Model::new("outer");
    let other = Model::new("inner");
    variable!(m, x);
    variable!(other, y);
    let inner_owned_by_other = Cell::new(false);
    let result = catch_unwind(AssertUnwindSafe(|| {
        sum!(m, {
            let m = &other;
            let inner = sum!(m, y for _j in 0..1);
            inner_owned_by_other.set(std::ptr::eq(inner.arena, y.arena));
            inner
        } for _i in 0..1)
    }));
    let panic = result.expect_err("the local m must not resolve to the outer model");
    assert!(inner_owned_by_other.get());
    let message = panic.downcast_ref::<&str>().copied().unwrap_or("");
    assert!(message.contains("different model"));
    assert_eq!(value(sum!(m, x for _i in 0..1)), 1.0);
}

#[test]
fn pattern_bindings_do_not_shadow_let_initializers_or_for_iterators() {
    let m = Model::new("pattern_scope");
    variable!(m, x);

    let total = sum!(m, {
        let from_let = {
            let m = sum!(m, x for _j in 0..1);
            m
        };
        for m in std::iter::once(sum!(m, x for _j in 0..1)) {
            let _ = m;
        }
        from_let
    } for _i in 0..1);

    assert_eq!(value(total), 1.0);
}