molgfx-core 0.2.0

The semantic scene graph: columnar tables, GPU record layouts, the borrowed coordinate seam.
Documentation
use super::*;
use crate::{
    Annotation, AnnotationAnchor, EntityKind, EntityRef, ProvenanceDetail, SceneDescriptionSources,
    fixture,
};

#[test]
fn compact_pose_batches_round_trip_with_stable_identity_and_visibility() {
    let structure = fixture::structure();
    let mut scene = match Scene::from_structure(&structure) {
        Ok(value) => value,
        Err(error) => panic!("fixture scene builds: {error}"),
    };
    let Some((owner, _)) = scene.structures().next() else {
        panic!("fixture has a structure")
    };
    let template = match LicoriceTemplate::new(Vec3::ZERO, vec![Vec3::ZERO, Vec3::X], &[[0, 1]]) {
        Ok(value) => value,
        Err(error) => panic!("template validates: {error}"),
    };
    let pose = match LigandPose::new(
        Vec3::new(3.0, 2.0, 1.0),
        Quat::from_rotation_y(0.4),
        Rgba8::opaque(30, 140, 220),
        0.7,
    ) {
        Ok(value) => value,
        Err(error) => panic!("pose validates: {error}"),
    };
    let handle = match scene.add_licorice_poses(owner, template, vec![pose, pose]) {
        Ok(Some(value)) => value,
        Ok(None) => panic!("non-empty pose batch returns a handle"),
        Err(error) => panic!("pose batch stores: {error}"),
    };
    assert!(scene.hide_ligand_pose_batch(handle));
    let batch_entity = EntityRef {
        structure: owner,
        kind: EntityKind::LigandPoseBatch,
        index: Scene::ligand_pose_batch_entity_row(handle),
    };
    let anchor = match AnnotationAnchor::entity(Vec3::ZERO, batch_entity) {
        Ok(value) => value,
        Err(error) => panic!("batch anchor validates: {error}"),
    };
    let annotation = match Annotation::note(owner, anchor, "candidate batch") {
        Ok(value) => value,
        Err(error) => panic!("batch annotation validates: {error}"),
    };
    if let Err(error) = scene.add_annotation(annotation) {
        panic!("batch annotation stores: {error}")
    }

    let description = scene.describe();
    let sources = SceneDescriptionSources {
        structures: std::slice::from_ref(&structure),
        volumes: &[],
        segmentations: &[],
        atom_properties: &[],
        meshes: &[],
    };
    let restored = match Scene::from_description(&description, sources) {
        Ok(value) => value,
        Err(error) => panic!("pose batch manifest restores: {error}"),
    };

    let Some((restored_handle, batch)) = restored.ligand_pose_batches().next() else {
        panic!("restored pose batch exists")
    };
    assert_eq!(restored_handle.row(), handle.row());
    assert_eq!(restored_handle.generation(), handle.generation());
    assert_eq!(batch.pose_count(), 2);
    assert_eq!(batch.instance_count(), 6);
    assert!(!batch.visible());
    let restored_entity = EntityRef {
        structure: batch.owner(),
        kind: EntityKind::LigandPoseBatch,
        index: restored_handle.row(),
    };
    assert!(matches!(
        restored
            .provenance(restored_entity)
            .map(|value| value.detail),
        Some(ProvenanceDetail::LigandPoseBatch(_))
    ));
    assert_eq!(restored.describe(), description);
}

#[test]
fn manifests_reject_unpickable_or_empty_pose_batches() {
    let structure = fixture::structure();
    let mut scene = match Scene::from_structure(&structure) {
        Ok(value) => value,
        Err(error) => panic!("fixture scene builds: {error}"),
    };
    let Some((owner, _)) = scene.structures().next() else {
        panic!("fixture has a structure")
    };
    let template = match LicoriceTemplate::new(Vec3::ZERO, vec![Vec3::ZERO], &[]) {
        Ok(value) => value,
        Err(error) => panic!("template validates: {error}"),
    };
    let pose = match LigandPose::new(Vec3::ZERO, Quat::IDENTITY, Rgba8::WHITE, 1.0) {
        Ok(value) => value,
        Err(error) => panic!("pose validates: {error}"),
    };
    if let Err(error) = scene.add_licorice_poses(owner, template, vec![pose]) {
        panic!("pose batch stores: {error}")
    }
    let mut description = scene.describe();
    let sources = SceneDescriptionSources {
        structures: std::slice::from_ref(&structure),
        volumes: &[],
        segmentations: &[],
        atom_properties: &[],
        meshes: &[],
    };

    let Some(batch) = description.ligand_pose_batches.first_mut() else {
        panic!("manifest contains the pose batch")
    };
    let original_row = batch.row;
    batch.row = crate::EntityId::MAX_INDEX + 1;
    assert!(Scene::from_description(&description, sources).is_err());
    let Some(batch) = description.ligand_pose_batches.first_mut() else {
        panic!("manifest still contains the pose batch")
    };
    batch.row = original_row;
    description.tables.ligand_pose_batches = 0;
    assert!(Scene::from_description(&description, sources).is_err());
    description.tables.ligand_pose_batches = 1;
    let Some(batch) = description.ligand_pose_batches.first_mut() else {
        panic!("manifest still contains the pose batch")
    };
    batch.poses.clear();
    assert!(Scene::from_description(&description, sources).is_err());
}

#[test]
fn manifest_rejects_the_reserved_pose_batch_row_before_rehydration() {
    let structure = fixture::structure();
    let mut scene = match Scene::from_structure(&structure) {
        Ok(value) => value,
        Err(error) => panic!("fixture scene builds: {error}"),
    };
    let Some((owner, _)) = scene.structures().next() else {
        panic!("fixture has a structure")
    };
    let template = match LicoriceTemplate::new(Vec3::ZERO, vec![Vec3::ZERO], &[]) {
        Ok(value) => value,
        Err(error) => panic!("template validates: {error}"),
    };
    let pose = match LigandPose::new(Vec3::ZERO, Quat::IDENTITY, Rgba8::WHITE, 1.0) {
        Ok(value) => value,
        Err(error) => panic!("pose validates: {error}"),
    };
    if let Err(error) = scene.add_licorice_poses(owner, template, vec![pose]) {
        panic!("pose batch stores: {error}")
    }
    let mut description = scene.describe();
    let Some(batch) = description.ligand_pose_batches.first_mut() else {
        panic!("manifest contains the pose batch")
    };
    batch.row = u32::MAX;
    let result = Scene::from_description(
        &description,
        SceneDescriptionSources {
            structures: std::slice::from_ref(&structure),
            volumes: &[],
            segmentations: &[],
            atom_properties: &[],
            meshes: &[],
        },
    );
    assert!(matches!(
        result,
        Err(crate::CoreError::InvalidSceneDescription { summary })
            if summary.contains("identity range")
    ));
}

#[test]
fn sparse_pose_batch_rows_and_generations_round_trip() {
    let structure = fixture::structure();
    let mut scene = match Scene::from_structure(&structure) {
        Ok(value) => value,
        Err(error) => panic!("fixture scene builds: {error}"),
    };
    let Some((owner, _)) = scene.structures().next() else {
        panic!("fixture has a structure")
    };
    let template = match LicoriceTemplate::new(Vec3::ZERO, vec![Vec3::ZERO], &[]) {
        Ok(value) => value,
        Err(error) => panic!("template validates: {error}"),
    };
    let pose = match LigandPose::new(Vec3::ZERO, Quat::IDENTITY, Rgba8::WHITE, 1.0) {
        Ok(value) => value,
        Err(error) => panic!("pose validates: {error}"),
    };
    if let Err(error) = scene.add_licorice_poses(owner, template, vec![pose]) {
        panic!("pose batch stores: {error}")
    }
    let mut description = scene.describe();
    let Some(batch) = description.ligand_pose_batches.first_mut() else {
        panic!("manifest contains the pose batch")
    };
    batch.row = crate::EntityId::MAX_INDEX;
    batch.generation = 11;
    let sources = SceneDescriptionSources {
        structures: std::slice::from_ref(&structure),
        volumes: &[],
        segmentations: &[],
        atom_properties: &[],
        meshes: &[],
    };
    let restored = match Scene::from_description(&description, sources) {
        Ok(value) => value,
        Err(error) => panic!("sparse pose batch restores: {error}"),
    };
    let Some((handle, _)) = restored.ligand_pose_batches().next() else {
        panic!("restored pose batch exists")
    };
    assert_eq!(handle.row(), crate::EntityId::MAX_INDEX);
    assert_eq!(handle.generation(), 11);
    assert_eq!(restored.describe(), description);
}