use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::io::Write;
use std::path::Path;
use std::sync::Arc;
use ndarray::Axis;
use serde_json::{json, Value};
use super::source::{SourceAccess, SourceRef};
use super::task::{Row, Slot, Subject, TaskManifest};
use super::{fingerprint, Finding, Note};
use crate::annotations::Annotation;
use crate::clinical::model::{Bounds, Event, Link};
use crate::clinical::select::{names, Prepared, Selection};
use crate::clinical::Clinical;
use crate::geometry::Grid;
use crate::json::{pretty_at, repr_str};
use crate::sample::Sample;
use crate::{Error, Result};
pub const ROW_STATUSES: &[&str] = &[ELIGIBLE, UNCERTIFIABLE, EXCLUDED, ERROR];
pub const TARGET_STATUSES: &[&str] = &[POSITIVE, NEGATIVE, CENSORED, PREVALENT, NONE];
pub const ROI_FILLS: &[&str] = &[CENTER, ELIGIBLE_INSTANCES, CENTER_FALLBACK];
const ELIGIBLE: &str = "eligible";
const UNCERTIFIABLE: &str = "uncertifiable";
const EXCLUDED: &str = "excluded";
const ERROR: &str = "error";
const POSITIVE: &str = "positive";
const NEGATIVE: &str = "negative";
const CENSORED: &str = "censored";
const PREVALENT: &str = "prevalent";
const NONE: &str = "none";
const CENTER: &str = super::task::ROI[0];
const ELIGIBLE_INSTANCES: &str = super::task::ROI[1];
const CENTER_FALLBACK: &str = "center_fallback";
pub fn event_digest(event: &Event) -> String {
fingerprint(&event.to_json())
}
#[derive(Debug)]
pub struct Fragment {
pub source: SourceRef,
pub sample: Sample,
pub clinical: Option<Arc<Clinical>>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct SubjectHistory {
pub subject_id: String,
pub partition: Option<String>,
pub sources: Vec<SourceRef>,
pub events: Vec<Event>,
pub event_fragments: Vec<usize>,
pub links: Vec<(usize, Link)>,
pub documents: Vec<(usize, usize, String)>,
}
impl SubjectHistory {
pub fn link_refs(&self) -> Vec<(usize, &Link)> {
self.links.iter().map(|(f, l)| (*f, l)).collect()
}
pub fn to_json(&self) -> Value {
json!({
"subject_id": self.subject_id,
"partition": self.partition,
"sources": self.sources.iter().map(SourceRef::to_json).collect::<Vec<_>>(),
"events": self.events.iter().map(Event::to_json).collect::<Vec<_>>(),
"event_fragments": self.event_fragments,
"links": self.links.iter().map(|(f, l)| json!({"fragment": f, "link": l.to_json()})).collect::<Vec<_>>(),
"documents": self.documents.iter().map(|(e, f, d)| json!([e, f, d])).collect::<Vec<_>>(),
})
}
}
#[derive(Debug, Default)]
pub struct History {
pub fragments: Vec<Fragment>,
pub merged: SubjectHistory,
pub findings: Vec<Finding>,
}
pub fn load_subject(subject: &Subject, base: Option<&Path>, deep: bool) -> Result<History> {
load_subject_in(subject, &SourceAccess::new(base), deep)
}
pub fn load_subject_in(subject: &Subject, access: &SourceAccess, deep: bool) -> Result<History> {
let mut history = History::default();
history.merged.subject_id = subject.subject_id.clone();
history.merged.partition = subject.partition.clone();
let at = |src: &SourceRef| if src.source_id.is_empty() { src.locator() } else { src.source_id.clone() };
for source in &subject.sources {
let sample = match source.open_in(access) {
Ok(s) => s,
Err(e) if e.code() == Some("T301") => {
history.findings.push(Finding::new("T301", at(source), e.message()));
continue;
}
Err(e) => {
history.findings.push(Finding::new(
"T301",
at(source),
format!("{} does not open: {}", source.locator(), e),
));
continue;
}
};
let found = source.check(&sample, deep)?;
if !found.is_empty() {
history.findings.extend(found);
continue;
}
let found = sample.identity()?.subject_id.clone();
let local = source.local_subject_id.as_deref().unwrap_or(&subject.subject_id);
if found != local {
let mapped = match &source.local_subject_id {
Some(_) => format!(" as the manifest maps it to {}", repr_str(&subject.subject_id)),
None => ", the subject it is listed under (a source of another id records it as local_subject_id)"
.to_string(),
};
history.findings.push(Finding::new(
"T303",
at(source),
format!(
"{} is subject {} in its own identity, not {}{mapped}",
source.locator(),
repr_str(&found),
repr_str(local)
),
));
}
if sample.support()? != crate::version::Support::Full {
history.findings.push(Finding::new(
"T306",
at(source),
format!(
"{} is MEDH5 {}, read here only as a projection: it cannot supply certified inputs",
source.locator(),
sample.version()?
),
));
}
let clinical = if sample.profiles()?.contains(crate::clinical::PROFILE) {
match read_clinical(&sample, &source.locator()) {
Ok((problems, clinical)) => {
if let Some(first) = problems.first() {
history.findings.push(Finding::new(
"T306",
at(source),
format!(
"{}: its clinical tables are invalid ({} {}: {})",
source.locator(),
first.code,
first.location,
first.message
),
));
}
clinical
}
Err(e) => {
history.findings.push(Finding::new(
"T306",
at(source),
format!("{}: its clinical tables cannot be read: {e}", source.locator()),
));
None
}
}
} else {
None
};
history.fragments.push(Fragment { source: source.clone(), sample, clinical });
}
history.merged.sources = history.fragments.iter().map(|f| f.source.clone()).collect();
let clocks: BTreeSet<String> = history
.fragments
.iter()
.filter_map(|f| f.clinical.as_ref().map(|c| crate::json::canonical(&c.descriptor.clock.to_json())))
.collect();
if clocks.len() > 1 {
history.findings.push(Finding::new(
"T304",
&subject.subject_id,
format!("the subject's fragments declare {} different clocks; a temporal join needs one", clocks.len()),
));
}
if let Some(expected) = &subject.clock_id {
for f in &history.fragments {
if let Some(c) = &f.clinical {
if &c.descriptor.clock.id != expected {
history.findings.push(Finding::new(
"T304",
at(&f.source),
format!(
"clock {} is not the subject's clock {}",
repr_str(&c.descriptor.clock.id),
repr_str(expected)
),
));
}
}
}
}
let findings = merge(&mut history, subject)?;
history.findings.extend(findings);
Ok(history)
}
fn read_clinical(sample: &Sample, locator: &str) -> Result<(Vec<crate::validate::Diagnostic>, Option<Arc<Clinical>>)> {
let (problems, tables) = crate::validate::clinical_checked(&sample.root, locator)?;
let clinical = match tables {
Some(t) if t.sound() && t.events.is_some() && t.descriptor.is_some() => {
let projection = crate::version::is_projection(&sample.version()?);
Some(Arc::new(Clinical::from_tables(
&sample.root,
t.descriptor.clone().expect("checked"),
t.events.as_ref().expect("checked"),
t.documents.as_ref(),
t.links.as_ref(),
projection,
)?))
}
_ => None,
};
Ok((problems, clinical))
}
fn merge(history: &mut History, subject: &Subject) -> Result<Vec<Finding>> {
let mut findings = Vec::new();
let fragments = &history.fragments;
let merged = &mut history.merged;
let at = |f: &Fragment| if f.source.source_id.is_empty() { f.source.locator() } else { f.source.source_id.clone() };
let mut holders: HashMap<&str, Vec<(usize, usize)>> = HashMap::new();
let mut at_position: HashMap<&str, usize> = HashMap::new();
for (i, f) in fragments.iter().enumerate() {
let Some(c) = &f.clinical else { continue };
for (k, e) in c.events.iter().enumerate() {
let found = holders.entry(e.event_id.as_str()).or_default();
if found.is_empty() {
at_position.insert(e.event_id.as_str(), merged.events.len());
merged.events.push(e.clone());
merged.event_fragments.push(i);
}
found.push((i, k));
}
merged.links.extend(c.links.iter().map(|l| (i, l.clone())));
}
let event = |(f, k): (usize, usize)| &fragments[f].clinical.as_ref().expect("held").events[k];
for (id, held) in holders.iter().filter(|(_, h)| h.len() > 1) {
let chosen = *held
.iter()
.min_by(|a, b| fragments[a.0].source.content_id.cmp(&fragments[b.0].source.content_id))
.expect("held");
let position = at_position[id];
merged.events[position] = event(chosen).clone();
merged.event_fragments[position] = chosen.0;
}
let owned_contents = |f: usize, event_id: &str| -> std::result::Result<BTreeSet<String>, String> {
let c = fragments[f].clinical.as_ref().expect("held");
let mut out = BTreeSet::new();
for l in c.links.iter().filter(|l| {
l.relation == "describes"
&& l.source_type == "event"
&& l.source_id == event_id
&& l.target_type == "document"
}) {
let d = c
.document(&l.target_id)
.map_err(|e| format!("document {} cannot be read: {e}", repr_str(&l.target_id)))?;
out.insert(fingerprint(&json!({
"media_type": d.media_type,
"text": d.text,
"language": d.language,
"source_type": d.source_type,
})));
}
Ok(out)
};
let mut duplicated: Vec<(&str, &Vec<(usize, usize)>)> =
holders.iter().filter(|(_, h)| h.len() > 1).map(|(id, h)| (*id, h)).collect();
duplicated.sort_unstable();
let mut recorded: BTreeSet<&str> = BTreeSet::new();
for r in &subject.reconciled {
if !recorded.insert(r.event_id.as_str()) {
findings.push(Finding::new(
"T305",
&subject.subject_id,
format!("event {} is reconciled twice", repr_str(&r.event_id)),
));
} else if !duplicated.iter().any(|(id, _)| *id == r.event_id) {
findings.push(Finding::new(
"T305",
&subject.subject_id,
format!("event {} is reconciled, but no two of the subject's fragments hold it", repr_str(&r.event_id)),
));
}
}
for (event_id, held) in duplicated {
let digest = event_digest(event(held[0]));
for (f, k) in &held[1..] {
let other = event_digest(event((*f, *k)));
if other != digest {
findings.push(Finding::new(
"T305",
at(&fragments[*f]),
format!("event {} differs between fragments ({digest} vs {other})", repr_str(event_id)),
));
}
}
if event(held[0]).kind == "document" {
match owned_contents(held[0].0, event_id) {
Err(why) => findings.push(Finding::new("T306", at(&fragments[held[0].0]), why)),
Ok(first) => {
for (f, _) in &held[1..] {
match owned_contents(*f, event_id) {
Err(why) => findings.push(Finding::new("T306", at(&fragments[*f]), why)),
Ok(other) if other != first => findings.push(Finding::new(
"T305",
at(&fragments[*f]),
format!(
"document event {} owns another text here than in {}: one event version owns \
one document, so the fragments contradict each other",
repr_str(event_id),
at(&fragments[held[0].0])
),
)),
Ok(_) => {}
}
}
}
}
}
let holders: BTreeSet<&str> = held.iter().map(|(f, _)| fragments[*f].source.source_id.as_str()).collect();
match subject.reconciled.iter().find(|r| r.event_id == event_id) {
Some(r) if r.digest == digest => {
let declared: BTreeSet<&str> = r.sources.iter().map(String::as_str).collect();
if declared != holders {
findings.push(Finding::new(
"T305",
&subject.subject_id,
format!(
"event {} is reconciled as held by {}, but is held by {}",
repr_str(event_id),
declared.iter().copied().collect::<Vec<_>>().join(", "),
holders.iter().copied().collect::<Vec<_>>().join(", ")
),
));
}
}
Some(r) => findings.push(Finding::new(
"T305",
&subject.subject_id,
format!("event {} is reconciled at {} but its fragments hold {digest}", repr_str(event_id), r.digest),
)),
None => findings.push(Finding::new(
"T305",
&subject.subject_id,
format!(
"event {} is in fragments {} but the manifest records no reconciliation for it",
repr_str(event_id),
held.iter().map(|(f, _)| fragments[*f].source.source_id.as_str()).collect::<Vec<_>>().join(", ")
),
)),
}
}
let mut documents: BTreeMap<&str, Vec<usize>> = BTreeMap::new();
for (i, f) in fragments.iter().enumerate() {
if let Some(c) = &f.clinical {
for d in c.documents() {
documents.entry(d.document_id.as_str()).or_default().push(i);
}
}
}
for (document_id, held) in documents.iter().filter(|(_, h)| h.len() > 1) {
let mut digests = Vec::with_capacity(held.len());
for f in held {
let c = fragments[*f].clinical.as_ref().expect("held");
match c.document(document_id) {
Ok(d) => digests.push((*f, fingerprint(&d.to_json()))),
Err(e) => findings.push(Finding::new(
"T306",
at(&fragments[*f]),
format!("document {} cannot be read: {e}", repr_str(document_id)),
)),
}
}
if let Some((_, first)) = digests.first() {
for (f, digest) in &digests[1..] {
if digest != first {
findings.push(Finding::new(
"T305",
at(&fragments[*f]),
format!("document {} differs between fragments", repr_str(document_id)),
));
}
}
}
}
let by_id: HashMap<&str, usize> = merged.events.iter().enumerate().map(|(i, e)| (e.event_id.as_str(), i)).collect();
let mut owned = Vec::new();
for (f, l) in &merged.links {
if l.relation == "describes" && l.source_type == "event" && l.target_type == "document" {
if let Some(&e) = by_id.get(l.source_id.as_str()) {
if merged.events[e].kind == "document" && merged.event_fragments[e] == *f {
owned.push((e, *f, l.target_id.clone()));
}
}
}
}
owned.sort();
owned.dedup();
merged.documents = owned;
Ok(findings)
}
pub fn reconcile(subject: &Subject, base: Option<&Path>) -> Result<Vec<super::task::Reconciled>> {
reconcile_in(subject, &SourceAccess::new(base))
}
pub fn reconcile_in(subject: &Subject, access: &SourceAccess) -> Result<Vec<super::task::Reconciled>> {
let mut samples = Vec::new();
for source in &subject.sources {
let sample = source.open_in(access)?;
if let Some(finding) = source.check(&sample, false)?.into_iter().next() {
return Err(Error::coded("T302", finding.message));
}
samples.push((source.source_id.clone(), sample));
}
let mut clinicals = Vec::new();
for (id, sample) in &samples {
if let Some(c) = sample.clinical()? {
clinicals.push((id.clone(), c.clone()));
}
}
let mut holders: BTreeMap<&str, Vec<(usize, usize)>> = BTreeMap::new();
for (i, (_, c)) in clinicals.iter().enumerate() {
for (k, e) in c.events.iter().enumerate() {
holders.entry(e.event_id.as_str()).or_default().push((i, k));
}
}
Ok(holders
.into_iter()
.filter(|(_, h)| h.len() > 1)
.map(|(event_id, held)| {
let (f, k) = held[0];
super::task::Reconciled {
event_id: event_id.to_string(),
digest: event_digest(&clinicals[f].1.events[k]),
sources: held.iter().map(|(f, _)| clinicals[*f].0.clone()).collect(),
}
})
.collect())
}
#[derive(Debug, Clone, PartialEq)]
pub struct SlotFill {
pub slot: String,
pub fragment: Option<usize>,
pub image_id: Option<String>,
pub grid_id: Option<String>,
pub event_id: Option<String>,
pub center: Option<Vec<i64>>,
pub roi: String,
pub annotations: Vec<String>,
pub label_annotations: Vec<String>,
}
impl SlotFill {
fn empty(slot: &Slot) -> SlotFill {
SlotFill {
slot: slot.name.clone(),
fragment: None,
image_id: None,
grid_id: None,
event_id: None,
center: None,
roi: slot.roi.clone(),
annotations: Vec::new(),
label_annotations: Vec::new(),
}
}
pub fn available(&self) -> bool {
self.image_id.is_some()
}
pub fn to_json(&self) -> Value {
json!({
"slot": self.slot,
"available": self.available(),
"fragment": self.fragment,
"image_id": self.image_id,
"grid_id": self.grid_id,
"event_id": self.event_id,
"center": self.center,
"roi": self.roi,
"annotations": self.annotations,
"label_annotations": self.label_annotations,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TargetLabel {
pub status: String,
pub value: Option<f64>,
pub event_id: Option<String>,
pub reason: Option<String>,
}
impl TargetLabel {
fn none() -> TargetLabel {
TargetLabel { status: NONE.into(), value: None, event_id: None, reason: None }
}
pub fn observed(&self) -> bool {
self.value.is_some()
}
pub fn to_json(&self) -> Value {
json!({
"status": self.status,
"value": self.value,
"observed": self.observed(),
"event_id": self.event_id,
"reason": self.reason,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RowView {
pub row_id: String,
pub subject_id: String,
pub partition: Option<String>,
pub cutoff_us: i64,
pub fingerprint: String,
pub status: String,
pub reasons: Vec<String>,
pub subject: Option<usize>,
pub selection: Option<Selection>,
pub slots: Vec<SlotFill>,
pub target: TargetLabel,
}
impl RowView {
pub fn eligible(&self) -> bool {
self.status == ELIGIBLE
}
pub fn to_json(&self, subject: Option<&SubjectHistory>) -> Value {
let events: &[Event] = subject.map(|s| s.events.as_slice()).unwrap_or(&[]);
self.to_json_named(&|i| names(&events[i]))
}
pub fn to_json_named<'a>(&self, name: &dyn Fn(usize) -> [&'a str; 3]) -> Value {
json!({
"row_id": self.row_id,
"subject_id": self.subject_id,
"subject": self.subject,
"partition": self.partition,
"cutoff_us": self.cutoff_us,
"fingerprint": self.fingerprint,
"status": self.status,
"reasons": self.reasons,
"selection": self.selection.as_ref().map(|s| s.to_json_named(name)),
"slots": self.slots.iter().map(SlotFill::to_json).collect::<Vec<_>>(),
"target": self.target.to_json(),
})
}
}
#[derive(Debug)]
struct Candidate {
event: usize,
fragment: usize,
image_id: String,
modality: String,
grid_id: String,
center: Vec<i64>,
voxel: Vec<(String, Vec<i64>, Vec<i64>)>,
instances: Vec<(String, Vec<i64>)>,
}
fn candidate(
sample: &Sample,
event: usize,
fragment: usize,
image_id: &str,
instances: bool,
) -> Result<Option<Candidate>> {
let image = match sample.image(image_id) {
Ok(image) => image,
Err(Error::Key(_)) => return Ok(None),
Err(e) => return Err(e),
};
let grid = image.grid()?;
let (mut voxel, mut bearing) = (Vec::new(), Vec::new());
for (ann_id, annotation) in sample.annotations()? {
if annotation.grid_id() != Some(grid.grid_id.as_str()) {
continue;
}
if annotation.is_voxel() && annotation.kind() != "mask" {
voxel.push((ann_id.clone(), annotation.class_ids().to_vec(), annotation.annotated_class_ids().to_vec()));
}
if instances && matches!(annotation.kind(), "instances" | "boxes") {
if let Some(center) = first_instance_center(annotation, grid)? {
bearing.push((ann_id.clone(), center));
}
}
}
bearing.sort();
Ok(Some(Candidate {
event,
fragment,
image_id: image_id.to_string(),
modality: image.modality()?,
grid_id: grid.grid_id.clone(),
center: grid.spatial_shape().iter().map(|n| (*n / 2) as i64).collect(),
voxel,
instances: bearing,
}))
}
fn first_instance_center(annotation: &Annotation, grid: &Grid) -> Result<Option<Vec<i64>>> {
let boxes = match annotation.boxes_in_index(grid) {
Ok(boxes) => boxes,
Err(Error::Validation { .. } | Error::Value(_)) => return Ok(None),
Err(e) => return Err(e),
};
let n = boxes.len_of(Axis(0));
let ids = annotation.instance_ids()?.unwrap_or_else(|| (0..n as u64).collect());
let Some(first) = (0..n.min(ids.len())).min_by_key(|&i| ids[i]) else { return Ok(None) };
Ok(boxes
.index_axis(Axis(0), first)
.outer_iter()
.map(|r| {
let center = ((r[0] + r[1]) / 2.0 + 0.5).floor();
center.is_finite().then_some(center as i64)
})
.collect())
}
struct Rows<'a> {
manifest: &'a TaskManifest,
history: &'a History,
prepared: Prepared<'a>,
targets: Vec<&'a Event>,
candidates: Vec<Candidate>,
}
impl<'a> Rows<'a> {
fn new(
manifest: &'a TaskManifest,
history: &'a History,
links: &[(usize, &'a Link)],
) -> Result<(Rows<'a>, Vec<Finding>)> {
let events = &history.merged.events;
let prepared = Prepared::new(events, links)?;
let targets = match &manifest.target {
None => Vec::new(),
Some(t) => prepared
.latest()
.map(|i| &events[i])
.filter(|e| e.status != "entered_in_error")
.filter(|e| {
e.code_system.as_deref() == Some(t.code_system.as_str())
&& e.code.as_deref() == Some(t.code.as_str())
})
.filter(|e| t.kind.as_deref().is_none_or(|k| k == e.kind))
.collect(),
};
let by_id: HashMap<&str, usize> = events.iter().enumerate().map(|(i, e)| (e.event_id.as_str(), i)).collect();
let instances = manifest.slots.iter().any(|s| s.roi == ELIGIBLE_INSTANCES);
let (mut candidates, mut unread) = (Vec::new(), Vec::new());
for (fragment, l) in links {
if l.relation != "describes" || l.source_type != "event" || l.target_type != "image" {
continue;
}
let Some(&event) = by_id.get(l.source_id.as_str()) else { continue };
if events[event].kind != "imaging" {
continue; }
let source = &history.fragments[*fragment].source;
match candidate(&history.fragments[*fragment].sample, event, *fragment, &l.target_id, instances) {
Ok(Some(c)) => candidates.push(c),
Ok(None) => {}
Err(e) => unread.push(Finding::new(
"T306",
if source.source_id.is_empty() { source.locator() } else { source.source_id.clone() },
format!(
"{}: image {} or an annotation on its grid cannot be read: {e}",
source.locator(),
repr_str(&l.target_id)
),
)),
}
}
candidates.sort_by(|a, b| (a.fragment, &a.image_id).cmp(&(b.fragment, &b.image_id)));
Ok((Rows { manifest, history, prepared, targets, candidates }, unread))
}
fn fill(&self, slot: &Slot, selection: &Selection, order: &[Option<Option<Bounds>>]) -> SlotFill {
let mut fill = SlotFill::empty(slot);
let events = &self.history.merged.events;
let fragments = &self.history.fragments;
let key = |c: &Candidate| {
let when = order[c.event].flatten();
(when.map_or(i64::MIN, |b| b.hi), when.map_or(i64::MIN, |b| b.lo), events[c.event].event_id.as_str())
};
let earlier = |a: &Candidate, b: &Candidate| {
let pin = |c: &Candidate| &fragments[c.fragment].source.content_id;
pin(a).cmp(pin(b)).then_with(|| a.image_id.cmp(&b.image_id))
};
let mut best: Option<&Candidate> = None;
for c in self.candidates.iter().filter(|c| order[c.event].is_some() && c.modality == slot.modality) {
if best.is_none_or(|b| key(c).cmp(&key(b)).then_with(|| earlier(b, c)).is_gt()) {
best = Some(c);
}
}
let Some(best) = best else { return fill };
fill.fragment = Some(best.fragment);
fill.grid_id = Some(best.grid_id.clone());
fill.event_id = Some(events[best.event].event_id.clone());
fill.image_id = Some(best.image_id.clone());
fill.center = Some(best.center.clone());
let eligible: BTreeSet<&str> = selection
.payloads
.iter()
.filter(|(f, k, _)| *f == best.fragment && k == "annotation")
.map(|(_, _, id)| id.as_str())
.collect();
for (ann_id, labels, examined) in &best.voxel {
if slot.classes.iter().any(|c| labels.contains(c) || examined.contains(c)) {
fill.label_annotations.push(ann_id.clone());
}
if eligible.contains(ann_id.as_str()) {
fill.annotations.push(ann_id.clone());
}
}
if slot.roi == ELIGIBLE_INSTANCES {
match best.instances.iter().find(|(id, _)| eligible.contains(id.as_str())) {
Some((_, center)) => fill.center = Some(center.clone()),
None => fill.roi = CENTER_FALLBACK.into(),
}
}
fill
}
fn label(&self, cutoff: i64) -> TargetLabel {
let Some(t) = &self.manifest.target else { return TargetLabel::none() };
let value_of = |e: &Event| e.value_text.clone().unwrap_or_default();
let (lo_edge, hi_edge) = (cutoff, cutoff.saturating_add(t.horizon_us));
let (planned, positives): (Vec<&Event>, Vec<&Event>) = self
.targets
.iter()
.copied()
.filter(|e| e.status != "cancelled" && t.positive.contains(&value_of(e)))
.partition(|e| e.status == "planned");
if t.exclude_prevalent {
if let Some(e) = positives.iter().find(|e| e.effective_start.is_some_and(|s| s.hi <= cutoff)) {
return TargetLabel {
status: PREVALENT.into(),
value: None,
event_id: Some(e.event_id.clone()),
reason: Some("the outcome had occurred by the cutoff".into()),
};
}
}
let inside = |s: Bounds| s.lo > lo_edge && s.hi <= hi_edge;
let mut definite: Vec<&Event> =
positives.iter().copied().filter(|e| e.effective_start.is_some_and(inside)).collect();
definite.sort_by_key(|e| (e.effective_start.map(|s| (s.lo, s.hi)), e.event_id.clone()));
if let Some(first) = definite.first() {
return TargetLabel {
status: POSITIVE.into(),
value: Some(1.0),
event_id: Some(first.event_id.clone()),
reason: None,
};
}
let uncertain = positives.iter().any(|e| {
e.effective_start.is_none_or(|s| (s.lo <= lo_edge && s.hi > lo_edge) || (s.lo <= hi_edge && s.hi > hi_edge))
});
if uncertain {
return TargetLabel {
status: CENSORED.into(),
value: None,
event_id: None,
reason: Some("a positive outcome's time straddles the target window".into()),
};
}
if planned.iter().any(|e| e.effective_start.is_none_or(|s| s.hi > lo_edge && s.lo <= hi_edge)) {
return TargetLabel {
status: CENSORED.into(),
value: None,
event_id: None,
reason: Some("a positive outcome in the target window is only planned".into()),
};
}
let follow_up = cutoff.saturating_add(t.min_follow_up_us);
let mut negatives: Vec<&Event> = self
.targets
.iter()
.copied()
.filter(|e| e.status != "planned" && e.status != "cancelled")
.filter(|e| t.negative.contains(&value_of(e)))
.filter(|e| e.effective_start.is_some_and(|s| inside(s) && s.lo >= follow_up))
.collect();
negatives.sort_by_key(|e| (e.effective_start.map(|s| (s.lo, s.hi)), e.event_id.clone()));
if let Some(last) = negatives.last() {
return TargetLabel {
status: NEGATIVE.into(),
value: Some(0.0),
event_id: Some(last.event_id.clone()),
reason: None,
};
}
TargetLabel {
status: CENSORED.into(),
value: None,
event_id: None,
reason: Some("no observation inside the window, or none late enough to be a negative".into()),
}
}
fn view(&self, row: &Row, mut view: RowView) -> Result<RowView> {
let selection = self.prepared.select(row.cutoff_us, &self.manifest.policy)?;
if selection.status == UNCERTIFIABLE {
view.status = UNCERTIFIABLE.into();
view.reasons.extend(selection.uncertain_records.iter().map(|r| format!("uncertain_revision:{r}")));
}
let mut order = vec![None; self.history.merged.events.len()];
for s in &selection.events {
order[s.index] = Some(s.order);
}
for slot in &self.manifest.slots {
let fill = self.fill(slot, &selection, &order);
if slot.required && !fill.available() && view.status == ELIGIBLE {
view.status = EXCLUDED.into();
view.reasons.push(format!("missing_required_slot:{}", slot.name));
}
view.slots.push(fill);
}
view.target = self.label(row.cutoff_us);
if view.status == ELIGIBLE {
let exclude = match view.target.status.as_str() {
PREVALENT => Some("prevalent_target"),
CENSORED if self.manifest.target.as_ref().is_some_and(|t| t.censoring == "exclude") => Some("censored"),
_ => None,
};
if let Some(reason) = exclude {
view.status = EXCLUDED.into();
view.reasons.push(reason.into());
}
}
view.selection = Some(selection);
Ok(view)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Preflight {
pub task_fingerprint: String,
pub manifest_fingerprint: String,
pub findings: Vec<Finding>,
pub subjects: Vec<SubjectHistory>,
pub rows: Vec<RowView>,
}
impl Preflight {
pub fn ok(&self) -> bool {
self.findings.is_empty()
}
pub fn counts(&self) -> BTreeMap<String, usize> {
let mut out = BTreeMap::new();
for r in &self.rows {
*out.entry(r.status.clone()).or_default() += 1;
}
out
}
pub fn row(&self, row_id: &str) -> Option<&RowView> {
self.rows.iter().find(|r| r.row_id == row_id)
}
pub fn subject_of(&self, row: &RowView) -> Option<&SubjectHistory> {
row.subject.and_then(|i| self.subjects.get(i))
}
pub fn events_of(&self, row: &RowView) -> Vec<&Event> {
match (self.subject_of(row), &row.selection) {
(Some(subject), Some(selection)) => selection.events.iter().map(|s| &subject.events[s.index]).collect(),
_ => Vec::new(),
}
}
pub fn to_json(&self) -> Value {
json!({
"task_fingerprint": self.task_fingerprint,
"manifest_fingerprint": self.manifest_fingerprint,
"subjects": self.subjects.iter().map(SubjectHistory::to_json).collect::<Vec<_>>(),
"rows": self.rows.iter().map(|r| r.to_json(self.subject_of(r))).collect::<Vec<_>>(),
"ok": self.ok(),
"counts": self.counts(),
"findings": self.findings.iter().map(Finding::to_json).collect::<Vec<_>>(),
})
}
}
pub fn preflight(manifest: &TaskManifest, base: Option<&Path>, deep: bool) -> Result<Preflight> {
preflight_in(manifest, &SourceAccess::new(base), deep)
}
pub fn preflight_in(manifest: &TaskManifest, access: &SourceAccess, deep: bool) -> Result<Preflight> {
let mut subjects = Vec::new();
let mut views: Vec<Option<RowView>> = vec![None; manifest.rows.len()];
let done = preflight_each_in(manifest, access, deep, &mut |history, rows| {
for (r, view) in rows {
views[r] = Some(view);
}
subjects.push(history);
Ok(())
})?;
let rows = views.into_iter().zip(done.unclaimed).map(|(view, blank)| view.or(blank).expect("every row")).collect();
Ok(Preflight {
task_fingerprint: done.task_fingerprint,
manifest_fingerprint: done.manifest_fingerprint,
findings: done.findings,
subjects,
rows,
})
}
#[derive(Debug, Clone, PartialEq)]
pub struct PreflightDone {
pub task_fingerprint: String,
pub manifest_fingerprint: String,
pub findings: Vec<Finding>,
pub unclaimed: Vec<Option<RowView>>,
}
pub fn preflight_each(
manifest: &TaskManifest,
base: Option<&Path>,
deep: bool,
sink: &mut dyn FnMut(SubjectHistory, Vec<(usize, RowView)>) -> Result<()>,
) -> Result<PreflightDone> {
preflight_each_in(manifest, &SourceAccess::new(base), deep, sink)
}
pub fn preflight_each_in(
manifest: &TaskManifest,
access: &SourceAccess,
deep: bool,
sink: &mut dyn FnMut(SubjectHistory, Vec<(usize, RowView)>) -> Result<()>,
) -> Result<PreflightDone> {
let mut findings = manifest.validate();
let task_fingerprint = manifest.task_fingerprint();
let mut rows_of: HashMap<&str, Vec<usize>> = HashMap::new();
for (i, row) in manifest.rows.iter().enumerate() {
rows_of.entry(row.subject_id.as_str()).or_default().push(i);
}
let mut claimed = vec![false; manifest.rows.len()];
if findings.is_empty() {
for (index, subject) in manifest.subjects.iter().enumerate() {
let wanted = rows_of.get(subject.subject_id.as_str()).map(Vec::as_slice).unwrap_or(&[]);
let (history, found, rows) =
subject_part(manifest, &task_fingerprint, subject, index, wanted, access, deep)?;
findings.extend(found);
for (r, _) in &rows {
claimed[*r] = true;
}
sink(history, rows)?;
}
}
let unclaimed = manifest
.rows
.iter()
.zip(claimed)
.map(|(row, claimed)| {
(!claimed).then(|| {
let mut view = blank(manifest, &task_fingerprint, row, manifest.subject(&row.subject_id), None);
view.status = ERROR.into();
view.reasons = vec!["manifest_invalid: the task manifest has findings; see them first".into()];
view
})
})
.collect();
Ok(PreflightDone { task_fingerprint, manifest_fingerprint: manifest.manifest_fingerprint(), findings, unclaimed })
}
pub fn write_preflight(manifest: &TaskManifest, base: Option<&Path>, deep: bool, out: &mut dyn Write) -> Result<bool> {
write_preflight_in(manifest, &SourceAccess::new(base), deep, out)
}
pub fn write_preflight_in(
manifest: &TaskManifest,
access: &SourceAccess,
deep: bool,
out: &mut dyn Write,
) -> Result<bool> {
write_preflight_with(manifest, access, deep, None, out)
}
pub fn write_preflight_noted(
manifest: &TaskManifest,
access: &SourceAccess,
deep: bool,
notes: &[Note],
out: &mut dyn Write,
) -> Result<bool> {
write_preflight_with(manifest, access, deep, Some(notes), out)
}
fn write_preflight_with(
manifest: &TaskManifest,
access: &SourceAccess,
deep: bool,
notes: Option<&[Note]>,
out: &mut dyn Write,
) -> Result<bool> {
let fingerprints = [manifest.task_fingerprint(), manifest.manifest_fingerprint()];
write!(out, "{{\n \"task_fingerprint\": {}", pretty_at(&json!(fingerprints[0]), 1))?;
write!(out, ",\n \"manifest_fingerprint\": {},\n \"subjects\": ", pretty_at(&json!(fingerprints[1]), 1))?;
let mut items = Items::default();
let mut names: Vec<Names> = Vec::new();
let mut views: Vec<Option<RowView>> = vec![None; manifest.rows.len()];
let done = preflight_each_in(manifest, access, deep, &mut |history, rows| {
items.next(out, &history.to_json())?;
names.push(Names::of(&history.events));
for (r, view) in rows {
views[r] = Some(view);
}
Ok(())
})?;
items.close(out)?;
out.write_all(b",\n \"rows\": ")?;
let mut items = Items::default();
let mut counts: BTreeMap<String, usize> = BTreeMap::new();
for (view, blank) in views.into_iter().zip(done.unclaimed) {
let view = view.or(blank).expect("every row");
*counts.entry(view.status.clone()).or_default() += 1;
let subject = view.subject.and_then(|i| names.get(i));
items.next(out, &view.to_json_named(&|i| subject.expect("a selection has its subject").get(i)))?;
}
items.close(out)?;
let ok = done.findings.is_empty();
let findings: Vec<Value> = done.findings.iter().map(Finding::to_json).collect();
write!(out, ",\n \"ok\": {ok},\n \"counts\": {}", pretty_at(&json!(counts), 1))?;
write!(out, ",\n \"findings\": {}", pretty_at(&json!(findings), 1))?;
if let Some(notes) = notes {
let notes: Vec<Value> = notes.iter().map(Note::to_json).collect();
write!(out, ",\n \"notes\": {}", pretty_at(&json!(notes), 1))?;
}
out.write_all(b"\n}")?;
Ok(ok)
}
#[derive(Default)]
struct Items {
written: bool,
}
impl Items {
fn next(&mut self, out: &mut dyn Write, item: &Value) -> Result<()> {
out.write_all(if self.written { b",\n " } else { b"[\n " })?;
out.write_all(pretty_at(item, 2).as_bytes())?;
self.written = true;
Ok(())
}
fn close(self, out: &mut dyn Write) -> Result<()> {
out.write_all(if self.written { b"\n ]" } else { b"[]" })?;
Ok(())
}
}
#[derive(Debug, Default, Clone, PartialEq)]
struct Names {
text: String,
ends: Vec<usize>,
}
impl Names {
fn of(events: &[Event]) -> Names {
let mut out = Names { text: String::new(), ends: Vec::with_capacity(3 * events.len()) };
for event in events {
for name in names(event) {
out.text.push_str(name);
out.ends.push(out.text.len());
}
}
out
}
fn get(&self, index: usize) -> [&str; 3] {
let name = |k: usize| &self.text[if k == 0 { 0 } else { self.ends[k - 1] }..self.ends[k]];
[name(3 * index), name(3 * index + 1), name(3 * index + 2)]
}
}
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Admitted {
rows: HashMap<String, usize>,
views: Vec<Admits>,
names: Vec<Names>,
pins: Vec<BTreeSet<String>>,
}
type Admits = (i64, Option<usize>, Vec<usize>, String, bool);
impl Admitted {
pub fn of(pre: &Preflight) -> Admitted {
let mut out = Admitted {
names: pre.subjects.iter().map(|s| Names::of(&s.events)).collect(),
pins: pre.subjects.iter().map(pins_of).collect(),
..Default::default()
};
for view in &pre.rows {
out.insert(&view.row_id, Admitted::admits(view));
}
out
}
pub fn preflight(manifest: &TaskManifest, base: Option<&Path>, deep: bool) -> Result<Admitted> {
Admitted::preflight_in(manifest, &SourceAccess::new(base), deep)
}
pub fn preflight_in(manifest: &TaskManifest, access: &SourceAccess, deep: bool) -> Result<Admitted> {
let mut names = Vec::new();
let mut pins = Vec::new();
let mut found: Vec<Option<Admits>> = vec![None; manifest.rows.len()];
let done = preflight_each_in(manifest, access, deep, &mut |history, rows| {
names.push(Names::of(&history.events));
pins.push(pins_of(&history));
for (r, view) in rows {
found[r] = Some(Admitted::admits(&view));
}
Ok(())
})?;
let mut out = Admitted { names, pins, ..Default::default() };
for ((row, found), blank) in manifest.rows.iter().zip(found).zip(done.unclaimed) {
out.insert(&row.row_id, found.or_else(|| blank.as_ref().map(Admitted::admits)).expect("every row"));
}
Ok(out)
}
fn admits(view: &RowView) -> Admits {
let positions = view.selection.as_ref().map_or_else(Vec::new, |s| s.events.iter().map(|e| e.index).collect());
(view.cutoff_us, view.subject, positions, view.fingerprint.clone(), view.status == ERROR)
}
fn insert(&mut self, row_id: &str, admits: Admits) {
self.rows.entry(row_id.to_string()).or_insert(self.views.len());
self.views.push(admits);
}
pub fn row_pins(&self, row_id: &str) -> Option<&BTreeSet<String>> {
static NONE: std::sync::OnceLock<BTreeSet<String>> = std::sync::OnceLock::new();
let (_, subject, _, _, _) = &self.views[*self.rows.get(row_id)?];
Some(subject.and_then(|i| self.pins.get(i)).unwrap_or_else(|| NONE.get_or_init(BTreeSet::new)))
}
pub fn row_fingerprint(&self, row_id: &str) -> Option<&str> {
Some(self.views[*self.rows.get(row_id)?].3.as_str())
}
pub(crate) fn in_error(&self, row_id: &str) -> Option<bool> {
Some(self.views[*self.rows.get(row_id)?].4)
}
pub fn row(&self, row_id: &str) -> Option<(i64, Vec<&str>)> {
let (cutoff, subject, positions, _, _) = &self.views[*self.rows.get(row_id)?];
let ids = subject
.and_then(|i| self.names.get(i))
.map_or_else(Vec::new, |names| positions.iter().map(|&i| names.get(i)[0]).collect());
Some((*cutoff, ids))
}
}
fn pins_of(history: &SubjectHistory) -> BTreeSet<String> {
history.sources.iter().map(|s| s.content_id.clone()).collect()
}
type SubjectPart = (SubjectHistory, Vec<Finding>, Vec<(usize, RowView)>);
fn blank(
manifest: &TaskManifest,
task_fingerprint: &str,
row: &Row,
subject: Option<&Subject>,
index: Option<usize>,
) -> RowView {
RowView {
row_id: row.row_id.clone(),
subject_id: row.subject_id.clone(),
partition: subject.and_then(|s| s.partition.clone()),
cutoff_us: row.cutoff_us,
fingerprint: manifest.row_fingerprint_with(task_fingerprint, subject, row),
status: ELIGIBLE.into(),
reasons: Vec::new(),
subject: index,
selection: None,
slots: Vec::new(),
target: TargetLabel::none(),
}
}
fn subject_part(
manifest: &TaskManifest,
task_fingerprint: &str,
subject: &Subject,
index: usize,
wanted: &[usize],
access: &SourceAccess,
deep: bool,
) -> Result<SubjectPart> {
let history = load_subject_in(subject, access, deep)?;
let view = |r: usize| blank(manifest, task_fingerprint, &manifest.rows[r], Some(subject), Some(index));
let mut rows = Vec::with_capacity(wanted.len());
let mut findings = history.findings.clone();
if !history.findings.is_empty() {
let reasons: Vec<String> = history.findings.iter().map(Finding::line).collect();
for &r in wanted {
let mut v = view(r);
v.status = ERROR.into();
v.reasons = reasons.clone();
rows.push((r, v));
}
} else if !history.fragments.iter().any(|f| f.clinical.is_some()) {
for &r in wanted {
let mut v = view(r);
v.status = EXCLUDED.into();
v.reasons.push(
"no_clinical_source: no fragment declares the clinical profile, so nothing is attributable".into(),
);
rows.push((r, v));
}
} else if !wanted.is_empty() {
let links = history.merged.link_refs();
match Rows::new(manifest, &history, &links) {
Ok((shared, unread)) if unread.is_empty() => {
for &r in wanted {
rows.push((r, shared.view(&manifest.rows[r], view(r))?));
}
}
Ok((_, unread)) => {
let reasons: Vec<String> = unread.iter().map(Finding::line).collect();
for &r in wanted {
let mut v = view(r);
v.status = ERROR.into();
v.reasons = reasons.clone();
rows.push((r, v));
}
findings.extend(unread);
}
Err(e) if e.code() == Some("E816") => {
let finding = Finding::new(
"T305",
&subject.subject_id,
format!("its fragments' revision chains contradict each other once merged: {}", e.message()),
);
for &r in wanted {
let mut v = view(r);
v.status = ERROR.into();
v.reasons = vec![finding.line()];
rows.push((r, v));
}
findings.push(finding);
}
Err(e) => return Err(e),
}
}
Ok((history.merged, findings, rows))
}