ecological-model-core 0.4.1

Shared ecological initial states, interactions, trajectory observation, and terminal products
Documentation
use ecological_model_core::interaction::{
    InteractionMatrixRecipe, MatrixNormalization, SignStructure,
};
use physics_in_parallel::rng::{IndexedRng, RngConfig};
use scientific_workflow::execution::ExecutionScope;

#[test]
fn antisymmetric_recipe_matches_dispatcher_coordinates_exactly() {
    let config = RngConfig::new(Some(17), None, None);
    let matrix = InteractionMatrixRecipe::AntisymmetricGaussian {
        scale: 2.0,
        normalization: MatrixNormalization::SqrtSpecies,
        rng: config,
    }
    .generate(7)
    .unwrap();
    let rng = IndexedRng::new(config).unwrap();
    for row in 0..7 {
        assert_eq!(matrix.coefficient(row, row), 0.0);
        for column in (row + 1)..7 {
            let expected =
                rng.standard_normal(0, 7, row as u64, column as u64) * 2.0 / (7.0_f64).sqrt();
            assert_eq!(matrix.coefficient(row, column), expected);
            assert_eq!(matrix.coefficient(column, row), -expected);
        }
    }
}

#[test]
fn sign_structured_recipes_enforce_ecological_pair_signs() {
    for structure in [
        SignStructure::Competition,
        SignStructure::Mutualism,
        SignStructure::ConsumerResource,
    ] {
        let matrix = InteractionMatrixRecipe::SignStructuredGaussian {
            structure,
            scale: 1.0,
            connectance: 1.0,
            diagonal: ecological_model_core::interaction::DiagonalPolicy::Zero,
            normalization: MatrixNormalization::SqrtSpecies,
            rng: RngConfig::new(Some(91), None, None),
        }
        .generate(5)
        .unwrap();
        for row in 0..5 {
            for column in (row + 1)..5 {
                let pair = (
                    matrix.coefficient(row, column),
                    matrix.coefficient(column, row),
                );
                match structure {
                    SignStructure::Competition => assert!(pair.0 <= 0.0 && pair.1 <= 0.0),
                    SignStructure::Mutualism => assert!(pair.0 >= 0.0 && pair.1 >= 0.0),
                    SignStructure::ConsumerResource => assert!(pair.0 * pair.1 <= 0.0),
                }
            }
        }
    }
}

#[test]
fn generated_provenance_survives_verified_artifact_round_trip() {
    let directory = tempfile::tempdir().unwrap();
    let scope = ExecutionScope::create_named(directory.path(), "execution").unwrap();
    let matrix = InteractionMatrixRecipe::AntisymmetricGaussian {
        scale: 1.0,
        normalization: MatrixNormalization::SqrtSpecies,
        rng: RngConfig::new(Some(104), None, None),
    }
    .generate(4)
    .unwrap();
    let persisted =
        ecological_model_core::interaction::persist_interaction_matrix(&scope, &matrix).unwrap();
    let loaded = ecological_model_core::interaction::load_verified_interaction_matrix(
        scope.directory(),
        persisted.descriptor(),
    )
    .unwrap();
    for row in 0..4 {
        for column in 0..4 {
            assert_eq!(
                loaded.coefficient(row, column),
                matrix.coefficient(row, column)
            );
        }
    }
    assert_eq!(
        loaded.provenance().generator(),
        matrix.provenance().generator()
    );
}