conspire 0.7.1

The Rust interface to conspire.
Documentation
use crate::math::{
    Rank2, TensorArray, TensorRank2, TensorRank4,
    tensor::test::assert_eq_within,
    test::{TestError, assert_eq_within_tols},
};

fn get_rotation() -> TensorRank2<3, 1, 1> {
    [
        [
            0.781_639_173_907_025,
            -0.482_929_284_214_212_2,
            0.394_739_798_173_799_8,
        ],
        [
            0.550_117_230_704_358_4,
            0.832_030_133_774_634_6,
            -0.071_392_499_417_875_86,
        ],
        [
            -0.29395787843858057,
            0.27295633888831433,
            0.916_015_066_887_317_3,
        ],
    ]
    .into()
}

fn from_eigenvalues(eigenvalues: [f64; 3]) -> TensorRank2<3, 1, 1> {
    let rotation = get_rotation();
    let diagonal = TensorRank2::from([
        [eigenvalues[0], 0.0, 0.0],
        [0.0, eigenvalues[1], 0.0],
        [0.0, 0.0, eigenvalues[2]],
    ]);
    let tensor = &(&rotation * &diagonal) * &rotation.transpose();
    (tensor.clone() + tensor.transpose()) * 0.5
}

fn get_symmetric_tensor() -> TensorRank2<3, 1, 1> {
    from_eigenvalues([2.0, 0.5, 1.5])
}

fn get_non_symmetric_tensor() -> TensorRank2<3, 1, 1> {
    TensorRank2::from([[1.0, 4.0, 6.0], [7.0, 2.0, 5.0], [9.0, 8.0, 3.0]])
}

fn get_symmetric_tensor_logm() -> TensorRank2<3, 1, 1> {
    from_eigenvalues([2.0_f64.ln(), 0.5_f64.ln(), 1.5_f64.ln()])
}

fn contract_third_fourth_indices(
    dlogm: &TensorRank4<3, 1, 1, 1, 1>,
    tensor: &TensorRank2<3, 1, 1>,
) -> TensorRank2<3, 1, 1> {
    let mut result = TensorRank2::zero();
    (0..3).for_each(|i| {
        (0..3).for_each(|j| {
            result[i][j] = (0..3)
                .map(|k| {
                    (0..3)
                        .map(|l| dlogm[i][j][k][l] * tensor[k][l])
                        .sum::<f64>()
                })
                .sum();
        })
    });
    result
}

#[test]
fn logm_identity() -> Result<(), TestError> {
    assert_eq_within_tols(
        &TensorRank2::<3, 1, 1>::identity().logm()?,
        &TensorRank2::zero(),
    )
}

#[test]
fn logm_diagonal() -> Result<(), TestError> {
    let tensor = TensorRank2::<3, 1, 1>::from([[2.0, 0.0, 0.0], [0.0, 0.5, 0.0], [0.0, 0.0, 1.5]]);
    let expected = TensorRank2::from([
        [2.0_f64.ln(), 0.0, 0.0],
        [0.0, 0.5_f64.ln(), 0.0],
        [0.0, 0.0, 1.5_f64.ln()],
    ]);
    assert_eq_within_tols(&tensor.logm()?, &expected)
}

#[test]
fn logm_symmetric() -> Result<(), TestError> {
    assert_eq_within(
        &get_symmetric_tensor().logm()?,
        &get_symmetric_tensor_logm(),
        1e-10,
        1e-10,
    )
}

#[test]
fn logm_repeated_eigenvalue_first_pair() -> Result<(), TestError> {
    assert_eq_within(
        &from_eigenvalues([2.0, 2.0, 0.5]).logm()?,
        &from_eigenvalues([2.0_f64.ln(), 2.0_f64.ln(), 0.5_f64.ln()]),
        1e-10,
        1e-10,
    )
}

#[test]
fn logm_repeated_eigenvalue_second_pair() -> Result<(), TestError> {
    assert_eq_within(
        &from_eigenvalues([2.0, 0.5, 0.5]).logm()?,
        &from_eigenvalues([2.0_f64.ln(), 0.5_f64.ln(), 0.5_f64.ln()]),
        1e-10,
        1e-10,
    )
}

#[test]
fn logm_repeated_eigenvalue_triple() -> Result<(), TestError> {
    assert_eq_within(
        &from_eigenvalues([3.0, 3.0, 3.0]).logm()?,
        &from_eigenvalues([3.0_f64.ln(), 3.0_f64.ln(), 3.0_f64.ln()]),
        1e-10,
        1e-10,
    )
}

#[test]
fn logm_symmetric_trace_equals_ln_determinant() -> Result<(), TestError> {
    let tensor = get_symmetric_tensor();
    let determinant = tensor.determinant();
    assert_eq_within(&tensor.logm()?.trace(), &determinant.ln(), 1e-10, 1e-10)
}

#[test]
fn logm_near_identity_two_terms() -> Result<(), TestError> {
    let tensor = from_eigenvalues([1.00003, 0.999985, 1.00002]);
    let expected = from_eigenvalues([1.00003_f64.ln(), 0.999985_f64.ln(), 1.00002_f64.ln()]);
    assert_eq_within(&tensor.logm()?, &expected, 1e-9, 1e-9)
}

#[test]
fn logm_near_identity_three_terms() -> Result<(), TestError> {
    let tensor = from_eigenvalues([1.0005, 0.9996, 1.0002]);
    let expected = from_eigenvalues([1.0005_f64.ln(), 0.9996_f64.ln(), 1.0002_f64.ln()]);
    assert_eq_within(&tensor.logm()?, &expected, 1e-9, 1e-9)
}

#[test]
fn logm_near_identity_five_terms() -> Result<(), TestError> {
    let tensor = from_eigenvalues([1.003, 0.9985, 1.002]);
    let expected = from_eigenvalues([1.003_f64.ln(), 0.9985_f64.ln(), 1.002_f64.ln()]);
    assert_eq_within(&tensor.logm()?, &expected, 1e-9, 1e-9)
}

#[test]
fn dlogm_diagonal_matches_finite_difference_of_logm() -> Result<(), TestError> {
    let tensor = TensorRank2::<3, 1, 1>::from([[2.0, 0.0, 0.0], [0.0, 0.5, 0.0], [0.0, 0.0, 1.5]]);
    let dlogm = tensor.dlogm()?;
    let epsilon = 1e-6;
    let directions = [
        TensorRank2::from([[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]),
        TensorRank2::from([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 0.0]]),
        TensorRank2::from([[0.3, 0.2, 0.1], [0.2, -0.4, 0.05], [0.1, 0.05, 0.1]]),
    ];
    for direction in directions.iter() {
        let perturbation = direction * epsilon;
        let tensor_plus = tensor.clone() + perturbation.clone();
        let tensor_minus = tensor.clone() - perturbation;
        let finite_difference = (tensor_plus.logm()? - tensor_minus.logm()?) / (2.0 * epsilon);
        let predicted = contract_third_fourth_indices(&dlogm, direction);
        assert_eq_within(&finite_difference, &predicted, 1e-6, 1e-6)?;
    }
    Ok(())
}

#[test]
fn dlogm_symmetric_matches_finite_difference_of_logm() -> Result<(), TestError> {
    let tensor = get_symmetric_tensor();
    let dlogm = tensor.dlogm()?;
    let epsilon = 1e-6;
    let directions = [
        TensorRank2::from([[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]),
        TensorRank2::from([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 0.0]]),
        TensorRank2::from([[0.3, 0.2, 0.1], [0.2, -0.4, 0.05], [0.1, 0.05, 0.1]]),
    ];
    for direction in directions.iter() {
        let perturbation = direction * epsilon;
        let tensor_plus = tensor.clone() + perturbation.clone();
        let tensor_minus = tensor.clone() - perturbation;
        let finite_difference = (tensor_plus.logm()? - tensor_minus.logm()?) / (2.0 * epsilon);
        let predicted = contract_third_fourth_indices(&dlogm, direction);
        assert_eq_within(&finite_difference, &predicted, 1e-6, 1e-6)?;
    }
    Ok(())
}

#[test]
fn dlogm_repeated_eigenvalue_matches_finite_difference_of_logm() -> Result<(), TestError> {
    for eigenvalues in [[2.0, 2.0, 0.5], [2.0, 0.5, 0.5]] {
        let tensor = from_eigenvalues(eigenvalues);
        let dlogm = tensor.dlogm()?;
        let epsilon = 1e-6;
        let directions = [
            TensorRank2::from([[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]),
            TensorRank2::from([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 0.0]]),
            TensorRank2::from([[0.3, 0.2, 0.1], [0.2, -0.4, 0.05], [0.1, 0.05, 0.1]]),
        ];
        for direction in directions.iter() {
            let perturbation = direction * epsilon;
            let tensor_plus = tensor.clone() + perturbation.clone();
            let tensor_minus = tensor.clone() - perturbation;
            let finite_difference = (tensor_plus.logm()? - tensor_minus.logm()?) / (2.0 * epsilon);
            let predicted = contract_third_fourth_indices(&dlogm, direction);
            assert_eq_within(&finite_difference, &predicted, 1e-3, 1e-3)?;
        }
    }
    Ok(())
}

#[test]
#[should_panic(expected = "Symmetric matrix has a non-positive eigenvalue")]
fn logm_non_positive_eigenvalue_panics() {
    let _ = from_eigenvalues([-1.0, 2.0, 0.5]).logm();
}

#[test]
#[should_panic(expected = "Matrix logarithm only implemented for symmetric cases")]
fn logm_non_symmetric_panics() {
    let _ = get_non_symmetric_tensor().logm();
}

#[test]
#[should_panic(expected = "Symmetric matrix has a non-positive eigenvalue")]
fn dlogm_non_positive_eigenvalue_panics() {
    let _ = from_eigenvalues([-1.0, 2.0, 0.5]).dlogm();
}

#[test]
#[should_panic(expected = "Matrix logarithm only implemented for symmetric cases")]
fn dlogm_non_symmetric_panics() {
    let _ = get_non_symmetric_tensor().dlogm();
}