Loading acorn-lib/src/schema/research_activity/aspect.rs +1 −6 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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>, } Loading @@ -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 Loading Loading @@ -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() } Loading acorn-lib/src/schema/research_activity/mod.rs +13 −2 Original line number Diff line number Diff line Loading @@ -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")] Loading Loading @@ -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 /// Loading @@ -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, Loading @@ -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] Loading Loading @@ -406,6 +416,7 @@ impl Default for ResearchActivityMetadataContext { .sponsors(codemeta("sponsor")) .partners(schema_org("Text")) .related(schema_org("Text")) .resources(schema_org("DefinedTerm")) .build() } } Loading acorn-lib/src/schema/research_activity/tests/mod.rs +296 −0 Original line number Diff line number Diff line Loading @@ -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 { Loading Loading @@ -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, } )); } acorn-lib/src/util/mod.rs +144 −4 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 /// Loading Loading @@ -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() Loading Loading
acorn-lib/src/schema/research_activity/aspect.rs +1 −6 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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>, } Loading @@ -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 Loading Loading @@ -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() } Loading
acorn-lib/src/schema/research_activity/mod.rs +13 −2 Original line number Diff line number Diff line Loading @@ -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")] Loading Loading @@ -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 /// Loading @@ -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, Loading @@ -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] Loading Loading @@ -406,6 +416,7 @@ impl Default for ResearchActivityMetadataContext { .sponsors(codemeta("sponsor")) .partners(schema_org("Text")) .related(schema_org("Text")) .resources(schema_org("DefinedTerm")) .build() } } Loading
acorn-lib/src/schema/research_activity/tests/mod.rs +296 −0 Original line number Diff line number Diff line Loading @@ -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 { Loading Loading @@ -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, } )); }
acorn-lib/src/util/mod.rs +144 −4 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 /// Loading Loading @@ -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() Loading