use crate::composition::{Composition, Element};
use crate::error::ProviderError;
use crate::evidence::{EvidenceKind, EvidenceScope, EvidenceStrength};
use crate::precursor::{PrecursorId, PrecursorSelection};
use crate::process::{
Atmosphere, ConditionPrecedent, DurationRange, HeatingPurpose, ProcessPrecedent,
TemperatureRange,
};
use crate::provider::ProcessEvidenceProvider;
use crate::target::TargetSpecification;
use std::collections::BTreeSet;
#[derive(Debug, Clone)]
pub struct CuratedConditionRecord {
pub target: Composition,
pub precursor_ids: BTreeSet<PrecursorId>,
pub conditions: Vec<ConditionPrecedent>,
}
pub struct InMemoryLiteratureConditionProvider {
records: Vec<CuratedConditionRecord>,
}
impl InMemoryLiteratureConditionProvider {
pub fn from_curated_records() -> Self {
Self {
records: curated_records(),
}
}
}
impl ProcessEvidenceProvider for InMemoryLiteratureConditionProvider {
fn precedents(
&self,
target: &TargetSpecification,
precursors: &[PrecursorSelection],
) -> std::result::Result<Vec<ProcessPrecedent>, ProviderError> {
let queried_ids: BTreeSet<PrecursorId> =
precursors.iter().map(|p| p.precursor.clone()).collect();
let mut out = Vec::new();
for record in &self.records {
if record.target != target.composition {
continue;
}
let exact_route = record.precursor_ids == queried_ids;
let conditions: Vec<ConditionPrecedent> = record
.conditions
.iter()
.cloned()
.map(|mut c| {
c.applicable_to = if exact_route {
EvidenceScope::ExactTarget
} else {
EvidenceScope::SimilarMaterial
};
c
})
.collect();
out.push(ProcessPrecedent {
description: String::new(),
conditions,
});
}
Ok(out)
}
}
fn element(symbol: &str) -> Element {
Element::new(symbol).expect("curated record uses a valid IUPAC element symbol")
}
fn composition(pairs: &[(&str, f64)]) -> Composition {
Composition::new(pairs.iter().map(|&(sym, amt)| (element(sym), amt)))
.expect("curated record uses a valid, finite composition")
}
fn precursor_ids(ids: &[&str]) -> BTreeSet<PrecursorId> {
ids.iter().map(|id| PrecursorId(id.to_string())).collect()
}
fn curated_records() -> Vec<CuratedConditionRecord> {
vec![
CuratedConditionRecord {
target: composition(&[("Mg", 1.0), ("Al", 2.0), ("O", 4.0)]),
precursor_ids: precursor_ids(&["MgO", "Al2O3"]),
conditions: vec![ConditionPrecedent {
purpose: HeatingPurpose::Sintering,
temperature: Some(TemperatureRange::new(1725.0, 1725.0).unwrap()),
duration: Some(DurationRange::new(6.0, 6.0).unwrap()),
atmosphere: None,
ramp: None,
evidence_kind: EvidenceKind::CuratedLiteratureRecord,
source_id: Some("10.1007/s11663-014-0207-8".to_string()),
statement: "MgAl2O4 spinel reaction-sintered from MgO + Al2O3: pressed \
and sintered at 1600 C for 24 h, crushed, then re-sintered at \
1725 C for 6 h (this record uses the second, completing stage; \
the first stage's 1600 C/24 h is not separately represented \
since this route's template has only one Heat step)"
.to_string(),
strength: EvidenceStrength::Moderate,
applicable_to: EvidenceScope::ExactTarget,
}],
},
CuratedConditionRecord {
target: composition(&[("Ca", 1.0), ("O", 1.0)]),
precursor_ids: precursor_ids(&["CaCO3"]),
conditions: vec![ConditionPrecedent {
purpose: HeatingPurpose::Calcination,
temperature: Some(TemperatureRange::new(900.0, 900.0).unwrap()),
duration: Some(DurationRange::new(1.0, 1.0).unwrap()),
atmosphere: None,
ramp: None,
evidence_kind: EvidenceKind::CuratedLiteratureRecord,
source_id: Some("10.3390/ma17153875".to_string()),
statement: "CaCO3 calcined to CaO + CO2 at 900 C for 1 h".to_string(),
strength: EvidenceStrength::Strong,
applicable_to: EvidenceScope::ExactTarget,
}],
},
CuratedConditionRecord {
target: composition(&[("La", 1.0), ("Al", 1.0), ("O", 3.0)]),
precursor_ids: precursor_ids(&["La2O3", "Al2O3"]),
conditions: vec![ConditionPrecedent {
purpose: HeatingPurpose::Sintering,
temperature: Some(TemperatureRange::new(1500.0, 1500.0).unwrap()),
duration: Some(DurationRange::new(5.0, 5.0).unwrap()),
atmosphere: None,
ramp: None,
evidence_kind: EvidenceKind::CuratedLiteratureRecord,
source_id: Some("10.1039/d3ra03241h".to_string()),
statement: "LaAlO3 solid-state reaction from La2O3 + Al2O3 (undoped x=0,y=0 \
sample), sintered at 1500 C for 5 h in a muffle furnace"
.to_string(),
strength: EvidenceStrength::Moderate,
applicable_to: EvidenceScope::ExactTarget,
}],
},
CuratedConditionRecord {
target: composition(&[("Zn", 3.0), ("P", 2.0), ("O", 8.0)]),
precursor_ids: precursor_ids(&["ZnO", "(NH4)2HPO4"]),
conditions: vec![
ConditionPrecedent {
purpose: HeatingPurpose::Calcination,
temperature: Some(TemperatureRange::new(500.0, 500.0).unwrap()),
duration: Some(DurationRange::new(3.0, 3.0).unwrap()),
atmosphere: Some(Atmosphere::Air),
ramp: None,
evidence_kind: EvidenceKind::CuratedLiteratureRecord,
source_id: Some("10.3390/engproc2024067018".to_string()),
statement: "Zn3(PO4)2 from ZnO + (NH4)2HPO4 (undoped x=0 host), fired \
at 500 C for 3 h in air; source is a short conference-proceedings \
paper with an internally inconsistent reported space group -- \
weighed accordingly"
.to_string(),
strength: EvidenceStrength::Weak,
applicable_to: EvidenceScope::ExactTarget,
},
ConditionPrecedent {
purpose: HeatingPurpose::Sintering,
temperature: Some(TemperatureRange::new(950.0, 950.0).unwrap()),
duration: Some(DurationRange::new(5.0, 5.0).unwrap()),
atmosphere: Some(Atmosphere::Air),
ramp: None,
evidence_kind: EvidenceKind::CuratedLiteratureRecord,
source_id: Some("10.3390/engproc2024067018".to_string()),
statement: "Zn3(PO4)2 from ZnO + (NH4)2HPO4 (undoped x=0 host), \
reground and sintered at 950 C for 5 h in air; source is a short \
conference-proceedings paper with an internally inconsistent \
reported space group -- weighed accordingly"
.to_string(),
strength: EvidenceStrength::Weak,
applicable_to: EvidenceScope::ExactTarget,
},
],
},
CuratedConditionRecord {
target: composition(&[("Ba", 1.0), ("Ti", 1.0), ("O", 3.0)]),
precursor_ids: precursor_ids(&["BaCO3", "TiO2"]),
conditions: vec![
ConditionPrecedent {
purpose: HeatingPurpose::Calcination,
temperature: Some(TemperatureRange::new(1000.0, 1000.0).unwrap()),
duration: Some(DurationRange::new(2.0, 2.0).unwrap()),
atmosphere: None,
ramp: None,
evidence_kind: EvidenceKind::CuratedLiteratureRecord,
source_id: Some("10.3390/cryst14040304".to_string()),
statement: "BaCO3 + TiO2 (1:1 molar) calcined at 1000 C for 2 h in a \
muffle furnace"
.to_string(),
strength: EvidenceStrength::Moderate,
applicable_to: EvidenceScope::ExactTarget,
},
ConditionPrecedent {
purpose: HeatingPurpose::Sintering,
temperature: Some(TemperatureRange::new(1200.0, 1350.0).unwrap()),
duration: None,
atmosphere: None,
ramp: None,
evidence_kind: EvidenceKind::CuratedLiteratureRecord,
source_id: Some("10.3390/cryst14040304".to_string()),
statement: "reaction product pressed into ceramic sputtering targets \
and sintered at 1200-1350 C (two parallel samples of different \
target density; duration not stated in source)"
.to_string(),
strength: EvidenceStrength::Moderate,
applicable_to: EvidenceScope::ExactTarget,
},
],
},
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn curated_records_have_no_duplicate_target_precursor_purpose_keys() {
let mut seen: BTreeSet<(String, Vec<String>, String)> = BTreeSet::new();
for record in curated_records() {
let target_key = format!("{:?}", record.target);
let precursor_key: Vec<String> =
record.precursor_ids.iter().map(|p| p.0.clone()).collect();
for condition in &record.conditions {
let key = (
target_key.clone(),
precursor_key.clone(),
format!("{:?}", condition.purpose),
);
assert!(
seen.insert(key.clone()),
"duplicate curated condition record for {key:?} -- two records \
claiming the same target/precursor-set/purpose makes resolution \
order-dependent"
);
}
}
}
}