use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use indexmap::IndexMap;
use crate::curation::identity::{SplitClaim, PARTITIONS};
use crate::dataset::manifest::{Entry, Manifest};
use crate::dataset::split::{locator, Split};
use crate::json::{repr_list, repr_str};
use crate::{Error, Result};
use super::source::{SourceAccess, SourceRef};
use super::task::{Subject, TaskManifest};
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum Partitions<'a> {
Split { split: &'a Split, fold: Option<u32> },
Claims { set_id: &'a str },
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct CohortTask<'a> {
pub task_id: &'a str,
pub version: &'a str,
pub identity_namespace: &'a str,
pub base: Option<&'a Path>,
pub training_partition: &'a str,
}
impl<'a> CohortTask<'a> {
pub fn new(task_id: &'a str, version: &'a str, identity_namespace: &'a str) -> CohortTask<'a> {
CohortTask { task_id, version, identity_namespace, base: None, training_partition: "train" }
}
pub fn with_base(mut self, base: Option<&'a Path>) -> CohortTask<'a> {
self.base = base;
self
}
pub fn with_training_partition(mut self, partition: &'a str) -> CohortTask<'a> {
self.training_partition = partition;
self
}
}
impl TaskManifest {
pub fn from_cohort(cohort: &Manifest, partitions: Partitions<'_>, task: &CohortTask<'_>) -> Result<TaskManifest> {
if task.training_partition.is_empty() || task.training_partition == "unassigned" {
return Err(Error::invalid(format!(
"the training partition cannot be {}",
repr_str(task.training_partition)
)));
}
let place = Placer::new(cohort, partitions)?;
let base = task.base.map(crate::dataset::manifest::absolute).transpose()?;
let mut subjects: IndexMap<&str, (String, Vec<&Entry>)> = IndexMap::new();
for entry in &cohort.entries {
let partition = place.partition(entry)?;
match subjects.get_mut(entry.subject_id.as_str()) {
Some((held, entries)) => {
if *held != partition {
return Err(Error::invalid(format!(
"subject {} is in two partitions, {} and {} ({}): a task places a subject whole",
repr_str(&entry.subject_id),
repr_str(held),
repr_str(&partition),
locator(entry)
)));
}
entries.push(entry);
}
None => {
subjects.insert(&entry.subject_id, (partition, vec![entry]));
}
}
}
let mut out = TaskManifest::new(task.task_id, task.version, task.identity_namespace);
let mut present: BTreeSet<String> = BTreeSet::new();
for (subject_id, (partition, entries)) in &subjects {
let mut sources = Vec::with_capacity(entries.len());
for entry in entries {
sources.push(pin_entry(entry, base.as_deref())?);
}
present.insert(partition.clone());
out.add_subject(Subject {
subject_id: subject_id.to_string(),
clock_id: None,
partition: Some(partition.clone()),
sources,
reconciled: Vec::new(),
});
}
present.remove(task.training_partition);
let rank = |p: &String| (PARTITIONS.iter().position(|q| q == p).unwrap_or(PARTITIONS.len()), p.clone());
let mut others: Vec<String> = present.into_iter().collect();
others.sort_by_key(rank);
let mut listed = vec![task.training_partition.to_string()];
listed.extend(others);
out.split = Some((place.set_id.to_string(), listed));
out.reconcile_all(&SourceAccess::new(base.as_deref()))?;
Ok(out)
}
}
fn pin_entry(entry: &Entry, base: Option<&Path>) -> Result<SourceRef> {
let path = PathBuf::from(&entry.path);
let key = entry.key.clone().filter(|k| !k.is_empty());
let sample = SourceRef::new(entry.path.clone(), key.clone(), "").open(None)?;
let uri = match base {
Some(base) => {
crate::dataset::manifest::rebase(&entry.path, base, crate::dataset::manifest::PathStyle::Relative)?
}
None => crate::dataset::manifest::absolute(&path)?.to_string_lossy().into_owned(),
};
let pinned = SourceRef::pin(uri, key, &sample)?;
if let Some(recorded) = entry.content_id.as_deref().filter(|c| !c.is_empty()) {
if recorded != pinned.content_id {
return Err(Error::invalid(format!(
"{} records content_id {} but the file holds {}: the cohort manifest is stale --- re-index it \
(`medh5 dataset index`)",
locator(entry),
repr_str(recorded),
repr_str(&pinned.content_id)
)));
}
}
Ok(pinned)
}
struct Placer<'a> {
set_id: &'a str,
how: How<'a>,
}
enum How<'a> {
Split { by_locator: BTreeMap<&'a str, (&'a str, Option<i64>)>, fold: Option<i64> },
Claims { sha256: String },
}
impl<'a> Placer<'a> {
fn new(cohort: &'a Manifest, partitions: Partitions<'a>) -> Result<Placer<'a>> {
Ok(match partitions {
Partitions::Split { split, fold } => {
let fold = fold.map(i64::from);
match (split.k_folds, fold) {
(Some(_), None) => {
return Err(Error::invalid("a k-fold split needs a fold to say which fold is validation"));
}
(Some(k), Some(chosen)) if !(0..k).contains(&chosen) => {
return Err(Error::invalid(format!(
"fold {chosen} is not a fold of this {k}-fold split, which numbers them 0 to {}",
k - 1
)));
}
(None, Some(chosen)) => {
return Err(Error::invalid(format!(
"fold {chosen} names a fold, but this split has none: it was made with ratios, not \
k_folds"
)));
}
_ => {}
}
let sha256 = cohort.sha256();
if split.manifest_sha256 != sha256 {
return Err(Error::invalid(format!(
"split {} was made from another cohort manifest (digest {}, this one's is {}): re-split this \
manifest, or pass the one the split was made from",
repr_str(&split.set_id),
&split.manifest_sha256[..12.min(split.manifest_sha256.len())],
&sha256[..12]
)));
}
let leaks = split.leaks();
if !leaks.is_empty() {
return Err(Error::invalid(format!(
"the split places {} in more than one partition",
repr_list(&leaks)
)));
}
let mut by_locator = BTreeMap::new();
for a in &split.assignments {
for e in &a.entries {
by_locator.insert(e.as_str(), (a.partition.as_str(), a.fold));
}
}
Placer { set_id: &split.set_id, how: How::Split { by_locator, fold } }
}
Partitions::Claims { set_id } => Placer { set_id, how: How::Claims { sha256: cohort.sha256() } },
})
}
fn partition(&self, entry: &Entry) -> Result<String> {
let found = match &self.how {
How::Split { by_locator, fold } => {
let placed = by_locator.get(locator(entry).as_str()).or_else(|| by_locator.get(entry.path.as_str()));
placed.map(|(partition, f)| match fold {
Some(chosen) => if *f == Some(*chosen) { "val" } else { "train" }.to_string(),
None => partition.to_string(),
})
}
How::Claims { sha256 } => {
let mut found = None;
for doc in &entry.splits {
let claim = SplitClaim::from_json(&serde_json::Value::Object(doc.clone()))?;
if claim.set_id != self.set_id {
continue;
}
if claim.manifest_sha256.as_deref() != Some(sha256.as_str()) {
return Err(Error::invalid(format!(
"{} claims split {} against another manifest (digest {}, this one's is {}): re-claim \
it from this manifest, or pass the one it was made from",
locator(entry),
repr_str(self.set_id),
claim.manifest_sha256.as_deref().map(|d| &d[..12.min(d.len())]).unwrap_or("none"),
&sha256[..12]
)));
}
found = Some(claim.partition);
break;
}
found
}
};
match found {
Some(p) if p == "unassigned" => Err(Error::invalid(format!(
"{} is unassigned in split {}: a task places every source",
locator(entry),
repr_str(self.set_id)
))),
Some(p) => Ok(p),
None => Err(Error::invalid(format!(
"{} has no partition in split {}: a task places every source",
locator(entry),
repr_str(self.set_id)
))),
}
}
}