Commit 99b1fe0c authored by Wohlgemuth, Jason's avatar Wohlgemuth, Jason
Browse files

feat: Build out resources as attribute of ResearchActivityMetadata

parent 11ed14ef
Loading
Loading
Loading
Loading
Loading
+1 −6
Original line number Diff line number Diff line
@@ -17,7 +17,7 @@
use crate::schema::namespaces::{codemeta, schema_org};
use crate::schema::prompt::Model;
use crate::schema::TechnologyReadinessLevel;
use crate::util::{LinkedData, Resource};
use crate::util::LinkedData;
use bon::Builder;
use derive_more::Display;
use schemars::JsonSchema;
@@ -222,9 +222,6 @@ pub struct AspectFramework {
    pub motivity: Option<Motivity>,
    /// Human-machine teaming level
    pub autonomy: Option<Autonomy>,
    /// Hardware/compute resource requirements
    #[builder(default = Vec::new())]
    pub resources: Vec<Resource>,
    /// Technology maturity (e.g., technology readiness level)
    pub maturity: Option<TechnologyReadinessLevel>,
}
@@ -236,7 +233,6 @@ pub struct AspectFrameworkContext {
    pub(crate) portability: String,
    pub(crate) motivity: String,
    pub(crate) autonomy: String,
    pub(crate) resources: String,
    pub(crate) maturity: String,
}
/// Common attributes of data
@@ -280,7 +276,6 @@ impl Default for AspectFrameworkContext {
            .portability(schema_org("DefinedTerm"))
            .motivity(schema_org("DefinedTerm"))
            .autonomy(schema_org("DefinedTerm"))
            .resources(schema_org("DefinedTerm"))
            .maturity(schema_org("DefinedTerm"))
            .build()
    }
+13 −2
Original line number Diff line number Diff line
@@ -19,7 +19,7 @@ use crate::util::constants::{
    MAX_LENGTH_SECTION_MISSION,
};
use crate::util::constants::{MAX_LENGTH_APPROACH, MAX_LENGTH_CAPABILIY, MAX_LENGTH_IMPACT, MAX_LENGTH_RESEARCH_AREA};
use crate::util::{frontmatter_and_body, LinkedData, ToMarkdown, ToProse};
use crate::util::{frontmatter_and_body, LinkedData, Resource, ToMarkdown, ToProse};
#[cfg(feature = "std")]
use crate::util::{Constant, Label};
#[cfg(feature = "std")]
@@ -247,6 +247,14 @@ pub struct ResearchActivityMetadata {
    pub partners: Option<Vec<String>>,
    /// Related resarch activity data identifiers of related research activity data
    pub related: Option<Vec<String>>,
    /// Hardware/compute resource requirements
    /// ### Note
    /// Selected components should reflect the minimum hardware/compute resources required to perform the research activity
    ///
    /// ### Example
    /// For an AI/ML workflow that requires at least one GPU to perform training that cannot be executed on a CPU alone,
    /// the resources attribute should include GPU, but need not include CPU
    pub resources: Option<Vec<Resource>>,
}
/// Linked data (e.g., JSON-LD) context for metadata
///
@@ -267,7 +275,7 @@ pub struct ResearchActivityMetadataContext {
    pub identifier: String,
    /// Associated Digital Object Identifiers
    pub doi: String,
    /// Reseaerch Activity Identifier
    /// Research Activity Identifier
    pub raid: String,
    /// Research Organization Registry
    pub ror: String,
@@ -287,6 +295,8 @@ pub struct ResearchActivityMetadataContext {
    pub partners: String,
    /// Related research activity data
    pub related: String,
    /// Hardware/compute resource requirements
    pub resources: String,
}
/// Research activity prose components that describe the activity using natural language
#[skip_serializing_none]
@@ -406,6 +416,7 @@ impl Default for ResearchActivityMetadataContext {
            .sponsors(codemeta("sponsor"))
            .partners(schema_org("Text"))
            .related(schema_org("Text"))
            .resources(schema_org("DefinedTerm"))
            .build()
    }
}
+296 −0
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@ use crate::prelude::PathBuf;
use crate::schema::research_activity::*;
use crate::schema::standard::cff::{Agent, Cff, IdentifierType};
use crate::schema::*;
use crate::util::{Resource, Vendor};
use pretty_assertions::assert_eq;

fn fixtures_dir() -> PathBuf {
@@ -267,3 +268,298 @@ fn test_to_prose() {
    assert!(prose.contains("example.com"));
    insta::assert_snapshot!("to_prose_with_websites", prose);
}
#[test]
fn test_metadata_with_resources() {
    let meta = ResearchActivityMetadata::init()
        .identifier("gpu-project".to_string())
        .resources(vec![
            Resource::GPU {
                architecture: Some("Ampere".to_string()),
                compute_capability: Some(8.0),
                count: Some(4),
                memory: Some(80),
                vendor: Some(Vendor::NVIDIA),
            },
            Resource::CPU {
                architecture: Some("x86_64".to_string()),
                cores: Some(32),
                memory: Some(256),
                threads: Some(64),
                vendor: Some(Vendor::AMD),
            },
        ])
        .build();
    assert_eq!(meta.identifier, "gpu-project");
    let resources = meta.resources.expect("resources should be present");
    assert_eq!(resources.len(), 2);
    assert!(matches!(
        &resources[0],
        Resource::GPU {
            vendor: Some(Vendor::NVIDIA),
            ..
        }
    ));
    assert!(matches!(
        &resources[1],
        Resource::CPU {
            vendor: Some(Vendor::AMD),
            ..
        }
    ));
}
#[test]
fn test_metadata_with_minimal_resources() {
    let meta = ResearchActivityMetadata::init()
        .identifier("quantum-project".to_string())
        .resources(vec![Resource::Quantum, Resource::FPGA])
        .build();
    let resources = meta.resources.expect("resources should be present");
    assert_eq!(resources.len(), 2);
    assert!(matches!(resources[0], Resource::Quantum));
    assert!(matches!(resources[1], Resource::FPGA));
}
#[test]
fn test_metadata_without_resources() {
    let meta = ResearchActivityMetadata::init().identifier("basic-project".to_string()).build();
    assert!(meta.resources.is_none());
}
#[test]
fn test_metadata_resources_roundtrip() {
    let meta = ResearchActivityMetadata::init()
        .identifier("roundtrip-test".to_string())
        .resources(vec![Resource::GPU {
            architecture: None,
            compute_capability: None,
            count: Some(1),
            memory: Some(24),
            vendor: Some(Vendor::Intel),
        }])
        .build();
    let json = serde_json::to_string(&meta).expect("serialization should succeed");
    let parsed: ResearchActivityMetadata = serde_json::from_str(&json).expect("deserialization should succeed");
    let resources = parsed.resources.expect("resources should survive roundtrip");
    assert_eq!(resources.len(), 1);
    assert!(matches!(
        &resources[0],
        Resource::GPU {
            count: Some(1),
            memory: Some(24),
            vendor: Some(Vendor::Intel),
            ..
        }
    ));
}
#[test]
fn test_deserialize_metadata_with_gpu_resources() {
    let json = r#"{
        "identifier": "ml-training",
        "archive": false,
        "draft": false,
        "status": "active",
        "keywords": [],
        "technology": [],
        "resources": [
            {
                "GPU": {
                    "architecture": "Hopper",
                    "compute_capability": 9.0,
                    "count": 8,
                    "memory": 80,
                    "vendor": "NVIDIA"
                }
            }
        ]
    }"#;
    let meta: ResearchActivityMetadata = serde_json::from_str(json).expect("should deserialize GPU resources");
    assert_eq!(meta.identifier, "ml-training");
    let resources = meta.resources.expect("resources should be present");
    assert_eq!(resources.len(), 1);
    assert!(matches!(
        &resources[0],
        Resource::GPU {
            architecture: Some(arch),
            compute_capability: Some(cc),
            count: Some(8),
            memory: Some(80),
            vendor: Some(Vendor::NVIDIA),
        } if arch == "Hopper" && (*cc - 9.0_f32).abs() < f32::EPSILON
    ));
}
#[test]
fn test_deserialize_metadata_with_mixed_resources() {
    let json = r#"{
        "identifier": "hybrid-compute",
        "archive": false,
        "draft": true,
        "status": "active",
        "keywords": [],
        "technology": [],
        "resources": [
            {
                "CPU": {
                    "architecture": "x86_64",
                    "cores": 64,
                    "memory": 512,
                    "threads": 128,
                    "vendor": "Intel"
                }
            },
            "TPU",
            "FPGA",
            {
                "GPU": {
                    "count": 2,
                    "vendor": "AMD"
                }
            }
        ]
    }"#;
    let meta: ResearchActivityMetadata = serde_json::from_str(json).expect("should deserialize mixed resources");
    let resources = meta.resources.expect("resources should be present");
    assert_eq!(resources.len(), 4);
    assert!(matches!(
        &resources[0],
        Resource::CPU {
            cores: Some(64),
            threads: Some(128),
            vendor: Some(Vendor::Intel),
            ..
        }
    ));
    assert!(matches!(resources[1], Resource::TPU));
    assert!(matches!(resources[2], Resource::FPGA));
    assert!(matches!(
        &resources[3],
        Resource::GPU {
            count: Some(2),
            vendor: Some(Vendor::AMD),
            architecture: None,
            compute_capability: None,
            ..
        }
    ));
}
#[test]
fn test_deserialize_metadata_with_partial_gpu_fields() {
    let json = r#"{
        "identifier": "sparse-gpu",
        "archive": false,
        "draft": true,
        "status": "active",
        "keywords": [],
        "technology": [],
        "resources": [
            {
                "GPU": {
                    "memory": 24
                }
            }
        ]
    }"#;
    let meta: ResearchActivityMetadata = serde_json::from_str(json).expect("should deserialize partial GPU fields");
    let resources = meta.resources.expect("resources should be present");
    assert!(matches!(
        &resources[0],
        Resource::GPU {
            architecture: None,
            compute_capability: None,
            count: None,
            memory: Some(24),
            vendor: None,
        }
    ));
}
#[test]
fn test_deserialize_metadata_with_unit_resources() {
    let json = r#"{
        "identifier": "exotic-compute",
        "archive": false,
        "draft": true,
        "status": "active",
        "keywords": [],
        "technology": [],
        "resources": ["Quantum", "Neuromorphic", "NPU", "ASIC", "DSP", "unique-hardware"]
    }"#;
    let meta: ResearchActivityMetadata = serde_json::from_str(json).expect("should deserialize unit resource variants");
    let resources = meta.resources.expect("resources should be present");
    assert_eq!(resources.len(), 6);
    assert!(matches!(resources[0], Resource::Quantum));
    assert!(matches!(resources[1], Resource::Neuromorphic));
    assert!(matches!(resources[2], Resource::NPU));
    assert!(matches!(resources[3], Resource::ASIC));
    assert!(matches!(resources[4], Resource::DSP));
    assert!(matches!(&resources[5], Resource::Other(s) if s == "unique-hardware"));
}
#[test]
fn test_deserialize_metadata_with_empty_cpu_and_gpu() {
    let json = r#"{
        "identifier": "empty-resources",
        "archive": false,
        "draft": true,
        "status": "active",
        "keywords": [],
        "technology": [],
        "resources": [
            { "CPU": {} },
            { "GPU": {} }
        ]
    }"#;
    let meta: ResearchActivityMetadata = serde_json::from_str(json).expect("should deserialize empty CPU and GPU");
    let resources = meta.resources.expect("resources should be present");
    assert_eq!(resources.len(), 2);
    assert!(matches!(
        &resources[0],
        Resource::CPU {
            architecture: None,
            cores: None,
            memory: None,
            threads: None,
            vendor: None,
        }
    ));
    assert!(matches!(
        &resources[1],
        Resource::GPU {
            architecture: None,
            compute_capability: None,
            count: None,
            memory: None,
            vendor: None,
        }
    ));
}
#[test]
fn test_deserialize_metadata_with_string_cpu_and_gpu() {
    let json = r#"{
        "identifier": "string-resources",
        "archive": false,
        "draft": true,
        "status": "active",
        "keywords": [],
        "technology": [],
        "resources": ["CPU", "GPU"]
    }"#;
    let meta: ResearchActivityMetadata = serde_json::from_str(json).expect("should deserialize string CPU and GPU");
    let resources = meta.resources.expect("resources should be present");
    assert_eq!(resources.len(), 2);
    assert!(matches!(
        &resources[0],
        Resource::CPU {
            architecture: None,
            cores: None,
            memory: None,
            threads: None,
            vendor: None,
        }
    ));
    assert!(matches!(
        &resources[1],
        Resource::GPU {
            architecture: None,
            compute_capability: None,
            count: None,
            memory: None,
            vendor: None,
        }
    ));
}
+144 −4
Original line number Diff line number Diff line
@@ -277,12 +277,34 @@ pub enum MimeType {
    Unknown,
}
/// Enumeration for hardware resources used by technology
#[derive(Clone, Debug, Display, Deserialize, Serialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, JsonSchema)]
pub enum Resource {
    /// Central processing unit for "classical" computing
    CPU,
    CPU {
        /// Instruction set architecture (e.g., x86_64, ARM, RISC-V)
        architecture: Option<String>,
        /// Number of physical cores required (if applicable)
        cores: Option<u32>,
        /// Minimum RAM in GB (if applicable)
        memory: Option<u32>,
        /// Number of threads required (if applicable)
        threads: Option<u32>,
        /// Hardware vendor (e.g., Intel, AMD, ARM)
        vendor: Option<Vendor>,
    },
    /// Graphics processing unit
    GPU,
    GPU {
        /// GPU architecture (e.g., Ampere, Hopper, RDNA3)
        architecture: Option<String>,
        /// NVIDIA CUDA compute capability (e.g., 8.0 for A100)
        compute_capability: Option<f32>,
        /// Number of GPUs required (if applicable)
        count: Option<u32>,
        /// VRAM in GB (if applicable)
        memory: Option<u32>,
        /// Hardware vendor (e.g., NVIDIA, AMD, Intel)
        vendor: Option<Vendor>,
    },
    /// Tensor processing unit
    TPU,
    /// Neural processing unit
@@ -306,7 +328,35 @@ pub enum Resource {
    /// Quantum computing (e.g., NISQ, etc.)
    Quantum,
    /// Unknown, unspecified, or otherwise unclassified resource
    Other,
    Other(String),
}
/// Hardware vendor or manufacturer
#[derive(Clone, Debug, Display, Deserialize, Serialize, JsonSchema)]
pub enum Vendor {
    /// Advanced Micro Devices
    #[display("AMD")]
    AMD,
    /// Apple Inc.
    #[display("Apple")]
    Apple,
    /// Arm Holdings
    #[display("ARM")]
    ARM,
    /// Google LLC
    #[display("Google")]
    Google,
    /// Intel Corporation
    #[display("Intel")]
    Intel,
    /// NVIDIA Corporation
    #[display("NVIDIA")]
    NVIDIA,
    /// Qualcomm Incorporated
    #[display("Qualcomm")]
    Qualcomm,
    /// Other or unspecified vendor
    #[display("{}", _0)]
    Other(String),
}
/// Struct for using and sharing constants
///
@@ -676,6 +726,96 @@ impl MimeType {
        .to_string()
    }
}
impl<'de> Deserialize<'de> for Resource {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::de::Deserializer<'de>,
    {
        struct ResourceVisitor;
        impl<'de> serde::de::Visitor<'de> for ResourceVisitor {
            type Value = Resource;
            fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
                formatter.write_str(r#"a resource string (e.g. "CPU") or object (e.g. {"CPU": {...}})"#)
            }
            fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Resource, E> {
                match value {
                    | "CPU" => Ok(Resource::CPU {
                        architecture: None,
                        cores: None,
                        memory: None,
                        threads: None,
                        vendor: None,
                    }),
                    | "GPU" => Ok(Resource::GPU {
                        architecture: None,
                        compute_capability: None,
                        count: None,
                        memory: None,
                        vendor: None,
                    }),
                    | "TPU" => Ok(Resource::TPU),
                    | "NPU" => Ok(Resource::NPU),
                    | "ASIC" => Ok(Resource::ASIC),
                    | "DSP" => Ok(Resource::DSP),
                    | "FPGA" => Ok(Resource::FPGA),
                    | "Neuromorphic" => Ok(Resource::Neuromorphic),
                    | "Quantum" => Ok(Resource::Quantum),
                    | other => Ok(Resource::Other(other.to_string())),
                }
            }
            fn visit_map<M: serde::de::MapAccess<'de>>(self, mut map: M) -> Result<Resource, M::Error> {
                let tag: String = map.next_key()?.ok_or_else(|| serde::de::Error::custom("expected resource variant key"))?;
                match tag.as_str() {
                    | "CPU" => {
                        #[derive(Deserialize)]
                        struct F {
                            architecture: Option<String>,
                            cores: Option<u32>,
                            memory: Option<u32>,
                            threads: Option<u32>,
                            vendor: Option<Vendor>,
                        }
                        let f: F = map.next_value()?;
                        Ok(Resource::CPU {
                            architecture: f.architecture,
                            cores: f.cores,
                            memory: f.memory,
                            threads: f.threads,
                            vendor: f.vendor,
                        })
                    }
                    | "GPU" => {
                        #[derive(Deserialize)]
                        struct F {
                            architecture: Option<String>,
                            compute_capability: Option<f32>,
                            count: Option<u32>,
                            memory: Option<u32>,
                            vendor: Option<Vendor>,
                        }
                        let f: F = map.next_value()?;
                        Ok(Resource::GPU {
                            architecture: f.architecture,
                            compute_capability: f.compute_capability,
                            count: f.count,
                            memory: f.memory,
                            vendor: f.vendor,
                        })
                    }
                    | "TPU" | "NPU" | "ASIC" | "DSP" | "FPGA" | "Neuromorphic" | "Quantum" => {
                        let _: serde::de::IgnoredAny = map.next_value()?;
                        self.visit_str(&tag)
                    }
                    | other => {
                        let _: serde::de::IgnoredAny = map.next_value()?;
                        Ok(Resource::Other(other.to_string()))
                    }
                }
            }
        }
        deserializer.deserialize_any(ResourceVisitor)
    }
}
impl Default for SemanticVersion {
    fn default() -> Self {
        SemanticVersion::init().build()