use crate::composition::Composition;
use crate::error::ProviderError;
use crate::process::{Atmosphere, DurationRange, HeatingPurpose, RouteFamily, TemperatureRange};
use std::collections::BTreeSet;
pub const CORPUS_SNAPSHOT_SCHEMA_VERSION: &str = "gugen-literature-observation-snapshot-v1";
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct CorpusManifest {
pub source: String,
pub release: String,
pub schema_version: String,
pub checksum: String,
pub record_count: usize,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct CorpusHeatingObservation {
pub target: Composition,
pub precursors: BTreeSet<Composition>,
pub route_family: RouteFamily,
#[serde(skip)]
pub heating_purpose: Option<HeatingPurpose>,
pub operation_index: usize,
pub temperature: Option<TemperatureRange>,
pub duration: Option<DurationRange>,
pub atmosphere: Option<Atmosphere>,
pub doi: Option<String>,
pub corpus_record_index: usize,
}
impl<'de> serde::Deserialize<'de> for CorpusHeatingObservation {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct Raw {
target: Composition,
precursors: BTreeSet<Composition>,
route_family: RouteFamily,
operation_index: usize,
temperature: Option<TemperatureRange>,
duration: Option<DurationRange>,
atmosphere: Option<Atmosphere>,
doi: Option<String>,
corpus_record_index: usize,
}
let raw = Raw::deserialize(deserializer)?;
if raw.route_family != RouteFamily::ConventionalSolidState {
return Err(serde::de::Error::custom(format!(
"literature observation snapshots (schema {CORPUS_SNAPSHOT_SCHEMA_VERSION}) may \
only contain ConventionalSolidState observations, got {:?}",
raw.route_family
)));
}
Ok(CorpusHeatingObservation {
target: raw.target,
precursors: raw.precursors,
route_family: raw.route_family,
heating_purpose: None,
operation_index: raw.operation_index,
temperature: raw.temperature,
duration: raw.duration,
atmosphere: raw.atmosphere,
doi: raw.doi,
corpus_record_index: raw.corpus_record_index,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoadMode {
Strict,
Lenient,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RejectedObservation {
pub position: usize,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct LoadReport {
pub accepted: usize,
pub duplicates_collapsed: usize,
pub rejected: Vec<RejectedObservation>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct LiteratureObservationCorpus {
manifest: CorpusManifest,
observations: Vec<CorpusHeatingObservation>,
}
impl LiteratureObservationCorpus {
pub fn load(
json: &str,
mode: LoadMode,
) -> std::result::Result<(Self, LoadReport), ProviderError> {
#[derive(serde::Deserialize)]
struct SnapshotFile {
manifest: CorpusManifest,
observations: Vec<serde_json::Value>,
}
let snapshot: SnapshotFile = serde_json::from_str(json)
.map_err(|e| ProviderError::MalformedRecord(format!("snapshot file: {e}")))?;
if snapshot.manifest.schema_version != CORPUS_SNAPSHOT_SCHEMA_VERSION {
return Err(ProviderError::MalformedRecord(format!(
"manifest schema_version {:?} is not supported by this gugen version \
(expects {CORPUS_SNAPSHOT_SCHEMA_VERSION:?})",
snapshot.manifest.schema_version
)));
}
if snapshot.manifest.record_count != snapshot.observations.len() {
return Err(ProviderError::MalformedRecord(format!(
"manifest declares record_count={} but the snapshot contains {} observation \
entries",
snapshot.manifest.record_count,
snapshot.observations.len()
)));
}
let mut accepted: Vec<CorpusHeatingObservation> = Vec::new();
let mut rejected: Vec<RejectedObservation> = Vec::new();
for (position, value) in snapshot.observations.into_iter().enumerate() {
match serde_json::from_value::<CorpusHeatingObservation>(value) {
Ok(obs) => accepted.push(obs),
Err(e) => {
if mode == LoadMode::Strict {
return Err(ProviderError::MalformedRecord(format!(
"observation at position {position}: {e}"
)));
}
rejected.push(RejectedObservation {
position,
reason: e.to_string(),
});
}
}
}
let accepted_count = accepted.len();
accepted.sort_by(|a, b| Self::dedup_sort_key(a).cmp(&Self::dedup_sort_key(b)));
accepted.dedup_by(Self::same_content_ignoring_provenance);
let duplicates_collapsed = accepted_count - accepted.len();
let report = LoadReport {
accepted: accepted_count,
duplicates_collapsed,
rejected,
};
Ok((
Self {
manifest: snapshot.manifest,
observations: accepted,
},
report,
))
}
fn dedup_sort_key(
obs: &CorpusHeatingObservation,
) -> (
&Composition,
&BTreeSet<Composition>,
&Option<String>,
usize,
String,
String,
String,
usize,
) {
(
&obs.target,
&obs.precursors,
&obs.doi,
obs.operation_index,
format!("{:?}", obs.temperature),
format!("{:?}", obs.duration),
format!("{:?}", obs.atmosphere),
obs.corpus_record_index,
)
}
fn same_content_ignoring_provenance(
a: &mut CorpusHeatingObservation,
b: &mut CorpusHeatingObservation,
) -> bool {
a.target == b.target
&& a.precursors == b.precursors
&& a.route_family == b.route_family
&& a.operation_index == b.operation_index
&& a.temperature == b.temperature
&& a.duration == b.duration
&& a.atmosphere == b.atmosphere
&& a.doi == b.doi
}
pub fn manifest(&self) -> &CorpusManifest {
&self.manifest
}
pub fn observations(&self) -> &[CorpusHeatingObservation] {
&self.observations
}
pub fn len(&self) -> usize {
self.observations.len()
}
pub fn is_empty(&self) -> bool {
self.observations.is_empty()
}
pub fn find_exact(
&self,
route_family: RouteFamily,
target: &Composition,
precursors: &[Composition],
) -> Vec<&CorpusHeatingObservation> {
if route_family != RouteFamily::ConventionalSolidState {
return Vec::new();
}
let queried: BTreeSet<Composition> = precursors.iter().cloned().collect();
self.observations
.iter()
.filter(|obs| &obs.target == target && obs.precursors == queried)
.collect()
}
}