Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 83 additions & 2 deletions crates/app/src/ui/scientific_script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use std::path::Path;
use plotx_analysis::peaks::{DetectParams, detect_peaks, estimate_noise};
#[cfg(test)]
use plotx_io::Acquisition;
use plotx_io::{ChromatogramKind, LiquidChromatographyMethod, MassSpecRun};
use plotx_io::{AcquisitionStreamId, ChromatogramKind, LiquidChromatographyMethod, MassSpecRun};
use rhai::module_resolvers::DummyModuleResolver;
use rhai::{Array, Dynamic, Engine, EvalAltResult, Map, Position};

Expand Down Expand Up @@ -111,6 +111,8 @@ pub(crate) fn prepare_run(
ChromatogramKind::Housekeeping => "housekeeping",
ChromatogramKind::Unknown => "unknown",
},
"provenance": channel.provenance.machine_label(),
"source_stream_id": channel.source_stream.map(AcquisitionStreamId::get),
"description": channel.description,
"polarity": match channel.polarity {
plotx_io::Polarity::Positive => "positive",
Expand All @@ -130,6 +132,18 @@ pub(crate) fn prepare_run(
})
})
.collect::<Vec<_>>();
let stream_chromatograms = run
.streams
.iter()
.flat_map(|stream| {
[
ChromatogramKind::TotalIonCurrent,
ChromatogramKind::BasePeak,
]
.into_iter()
.filter_map(move |kind| prepared_stream_chromatogram(run, stream.id, kind))
})
.collect::<Vec<_>>();
let scans = run
.streams
.iter()
Expand Down Expand Up @@ -170,11 +184,52 @@ pub(crate) fn prepare_run(
"source": run.source,
"instrument": run.instrument,
"chromatograms": channels,
"stream_chromatograms": stream_chromatograms,
"scans": scans,
"lc_method": method,
})
}

fn prepared_stream_chromatogram(
run: &MassSpecRun,
stream: AcquisitionStreamId,
kind: ChromatogramKind,
) -> Option<serde_json::Value> {
let provenance = run.stream_chromatogram_provenance(stream, kind)?;
let source = run.bound_chromatogram(stream, kind);
let spectra = &run.stream(stream)?.spectra;
let (time_min, values) = if let Some(channel) = source {
(channel.time_min.clone(), channel.values.clone())
} else {
(
spectra
.iter()
.map(|spectrum| spectrum.retention_time_min)
.collect(),
spectra
.iter()
.map(|spectrum| match kind {
ChromatogramKind::TotalIonCurrent => spectrum.tic,
ChromatogramKind::BasePeak => spectrum.base_peak_intensity.unwrap_or(0.0),
_ => unreachable!("caller restricts resolved stream chromatograms"),
})
.collect(),
)
};
Some(serde_json::json!({
"stream_id": stream.get(),
"kind": match kind {
ChromatogramKind::TotalIonCurrent => "total_ion_current",
ChromatogramKind::BasePeak => "base_peak",
_ => unreachable!("caller restricts resolved stream chromatograms"),
},
"provenance": provenance.machine_label(),
"source_channel_id": source.map(|channel| channel.id.0.as_str()),
"time_min": time_min,
"values": values,
}))
}

fn summary_provenance_label(provenance: plotx_io::SpectrumSummaryProvenance) -> &'static str {
match provenance {
plotx_io::SpectrumSummaryProvenance::Source => "source",
Expand Down Expand Up @@ -325,7 +380,19 @@ mod tests {
}),
}],
}],
chromatograms: Vec::new(),
chromatograms: vec![plotx_io::ChromatogramChannel {
id: plotx_io::ChromatogramChannelId("bpc".to_owned()),
kind: ChromatogramKind::BasePeak,
provenance: plotx_io::ChromatogramProvenance::Source,
polarity: plotx_io::Polarity::Positive,
transition: None,
source_stream: Some(plotx_io::AcquisitionStreamId::new(1)),
coordinate: None,
description: "Base-peak chromatogram".to_owned(),
unit: "count".to_owned(),
time_min: vec![1.5],
values: vec![50.0],
}],
import_warnings: Vec::new(),
};

Expand All @@ -341,6 +408,20 @@ mod tests {
assert_eq!(scan["precursor"]["selected_mz"], 445.2);
assert_eq!(scan["precursor"]["isolation_window_target_mz"], 445.0);
assert_eq!(scan["precursor"]["activation_method"], "CID");
assert_eq!(
prepared["chromatograms"][0]["provenance"],
"source_chromatogram"
);
assert_eq!(prepared["chromatograms"][0]["source_stream_id"], 1);
assert_eq!(
prepared["stream_chromatograms"][0]["provenance"],
"spectrum_summary"
);
assert_eq!(
prepared["stream_chromatograms"][1]["provenance"],
"source_chromatogram"
);
assert_eq!(prepared["stream_chromatograms"][1]["values"][0], 50.0);
}

#[test]
Expand Down
58 changes: 2 additions & 56 deletions crates/app/src/ui/tools/mass_spec_browser_tests.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use super::*;
use plotx_io::{MassTransition, Polarity};
use std::path::Path;
use plotx_io::{ChromatogramProvenance, MassTransition, Polarity};

fn channel(
id: &str,
Expand All @@ -12,6 +11,7 @@ fn channel(
ChromatogramChannel {
id: ChromatogramChannelId(id.to_owned()),
kind,
provenance: ChromatogramProvenance::Source,
polarity,
transition: transition.map(
|(precursor_mz, product_mz, collision_energy, activation_method)| MassTransition {
Expand Down Expand Up @@ -172,57 +172,3 @@ fn large_channel_cache_scans_only_when_filters_change() {
assert_eq!(state.matches.len(), 1);
assert_eq!(state.matches[0].id.0, "transition=719");
}

#[test]
fn local_pxd066465_builds_720_transition_fields_when_present() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.join(".tmp/MS-data/pride-PXD066465/Drug_substance_3_scheduled_MRM.mzML");
if !path.is_file() {
return;
}
let loaded = plotx_io::mzml::load(&path).expect("PXD066465 mzML import");
let plotx_io::Acquisition::MassSpec(run) = loaded.acquisition else {
panic!("PXD066465 did not import as mass spectrometry data");
};
let dataset = MassSpecDataset::load(*run);
let index = ChannelIndex::build(&dataset);

assert_eq!(dataset.run.streams.len(), 0);
assert_eq!(dataset.run.chromatograms.len(), 722);
assert_eq!(index.entries.len(), 722);
assert_eq!(index.transition_count, 720);
assert!(
index
.entries
.iter()
.all(|entry| dataset.channel_field_id(&entry.id).is_some())
);

let first_transition = index
.entries
.iter()
.find(|entry| entry.precursor_mz.is_some() && entry.product_mz.is_some())
.expect("structured transition");
let precursor_mz = first_transition.precursor_mz.unwrap();
let product_mz = first_transition.product_mz.unwrap();
let mut state = BrowserState::new(index);
state.filters.precursor_mz = format_number(precursor_mz);
state.filters.product_mz = format_number(product_mz);
assert!(state.refresh());
assert!(!state.matches.is_empty());

let ctx = crate::typography::test_context();
let mut rendered_counts = None;
let _ = ctx.run_ui(egui::RawInput::default(), |ui| {
ui.set_width(360.0);
let selected = dataset.channel_field_id(&dataset.run.chromatograms[0].id);
assert!(channel_browser(&dataset, selected, ui).is_none());
let state_id = ui.make_persistent_id(("mass_spec_channel_browser", dataset.resource_id));
let rendered = ui
.data(|data| data.get_temp::<BrowserState>(state_id))
.expect("browser UI cache");
rendered_counts = Some((rendered.index.entries.len(), rendered.matches.len()));
});
assert_eq!(rendered_counts, Some((722, 722)));
}
5 changes: 3 additions & 2 deletions crates/core/src/project/mass_spec_convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use crate::state::{
};
use plotx_io::{
AcquisitionStream, AcquisitionStreamId, ChromatogramChannel, ChromatogramChannelId,
ChromatogramKind, MassSpecRun, MassSpectrum, Polarity, Precursor, SpectrumAcquisition,
SpectrumId, SpectrumRepresentation, SpectrumSummaryProvenance, StreamRole,
ChromatogramKind, ChromatogramProvenance, MassSpecRun, MassSpectrum, Polarity, Precursor,
SpectrumAcquisition, SpectrumId, SpectrumRepresentation, SpectrumSummaryProvenance, StreamRole,
};
use std::collections::BTreeMap;
use std::io::{Read, Write};
Expand Down Expand Up @@ -694,6 +694,7 @@ impl<'a, 'p, R: Read> Reader<'a, 'p, R> {
Ok(ChromatogramChannel {
id,
kind,
provenance: ChromatogramProvenance::Source,
polarity,
transition,
source_stream,
Expand Down
7 changes: 7 additions & 0 deletions crates/core/src/project/mass_spec_convert_project_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ fn schema_v1_project_round_trip_preserves_stream_bindings_and_extractions() {
assert_eq!(loaded.extracted_ion_chromatograms[0].mz_min, 10.0);
assert_eq!(loaded.extracted_ion_chromatograms[0].intensity, [0.0, 0.0]);
assert_eq!(loaded.field_catalog, expected_catalog);
assert!(
loaded
.run
.chromatograms
.iter()
.all(|channel| channel.provenance == ChromatogramProvenance::Source)
);
}

#[test]
Expand Down
59 changes: 59 additions & 0 deletions crates/core/src/project/mass_spec_convert_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,59 @@ fn rejects_large_structural_counts_without_reserving_the_claimed_collection() {
assert_count_rejected_without_payload(&spectra, "spectrum count");
}

#[test]
fn v1_channel_wire_layout_remains_fixed_and_defaults_provenance_to_source() {
let channel = ChromatogramChannel {
id: ChromatogramChannelId("legacy-tic".to_owned()),
kind: ChromatogramKind::TotalIonCurrent,
provenance: ChromatogramProvenance::PeakArrays,
polarity: Polarity::Unknown,
transition: None,
source_stream: None,
coordinate: None,
description: "Legacy TIC".to_owned(),
unit: "count".to_owned(),
time_min: vec![0.5],
values: vec![10.0],
};
let run = MassSpecRun {
source: String::new(),
metadata: BTreeMap::new(),
instrument: None,
streams: Vec::new(),
chromatograms: vec![channel],
import_warnings: Vec::new(),
};

let mut v1 = minimal_run_prefix();
v1.extend_from_slice(&0_u64.to_le_bytes()); // import warnings
v1.extend_from_slice(&0_u64.to_le_bytes()); // streams
v1.extend_from_slice(&1_u64.to_le_bytes()); // chromatograms
write_string(&mut v1, "legacy-tic").unwrap();
v1.push(0); // TIC
v1.push(2); // unknown polarity
v1.push(0); // no transition
v1.push(0); // no source stream
v1.push(0); // no coordinate
write_string(&mut v1, "Legacy TIC").unwrap();
write_string(&mut v1, "count").unwrap();
write_f64s(&mut v1, &[0.5]).unwrap();
write_f64s(&mut v1, &[10.0]).unwrap();
v1.extend_from_slice(&0_u64.to_le_bytes()); // no active stream
v1.extend_from_slice(&0_u64.to_le_bytes()); // extracted spectra
v1.extend_from_slice(&1_u64.to_le_bytes()); // next extraction ID
v1.extend_from_slice(&0_u64.to_le_bytes()); // extracted-ion chromatograms
v1.extend_from_slice(&1_u64.to_le_bytes()); // next chromatogram ID

assert_eq!(encode(&run).unwrap(), v1);
let decoded = decode_bytes(&v1).unwrap();
assert_eq!(decoded.chromatograms[0].polarity, Polarity::Unknown);
assert_eq!(
decoded.chromatograms[0].provenance,
ChromatogramProvenance::Source
);
}

#[test]
fn payload_round_trips_spectra_channels_precursors_and_transitions() {
let mut run = crate::state::sample_mass_spec_run();
Expand All @@ -99,6 +152,7 @@ fn payload_round_trips_spectra_channels_precursors_and_transitions() {
activation_method: Some("CID".to_owned()),
});
run.chromatograms[0].kind = ChromatogramKind::SelectedReactionMonitoring;
run.chromatograms[0].provenance = ChromatogramProvenance::SpectrumSummary;
run.chromatograms[0].polarity = Polarity::Positive;
run.chromatograms[0].transition = Some(plotx_io::MassTransition {
precursor_mz: Some(445.2),
Expand Down Expand Up @@ -146,6 +200,7 @@ fn payload_round_trips_spectra_channels_precursors_and_transitions() {
assert_eq!(precursor.activation_method.as_deref(), Some("CID"));
let channel = &decoded.chromatograms[0];
assert_eq!(channel.kind, ChromatogramKind::SelectedReactionMonitoring);
assert_eq!(channel.provenance, ChromatogramProvenance::Source);
assert_eq!(channel.polarity, Polarity::Positive);
let transition = channel.transition.as_ref().unwrap();
assert_eq!(transition.precursor_mz, Some(445.2));
Expand Down Expand Up @@ -180,4 +235,8 @@ fn chromatogram_only_payload_round_trips_with_no_active_stream() {
let decoded = decode_bytes(&encode(&run).unwrap()).unwrap();
assert!(decoded.streams.is_empty());
assert_eq!(decoded.chromatograms.len(), 1);
assert_eq!(
decoded.chromatograms[0].provenance,
ChromatogramProvenance::Source
);
}
32 changes: 12 additions & 20 deletions crates/core/src/state/dataset_trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,27 +195,19 @@ impl Dataset {
}
Self::Afm(_) => None,
Self::MassSpec(data) => {
let stream = data.run.stream(data.active_stream)?;
let chromatogram =
super::mass_spec_tic::points_for_stream_tic(&data.run, data.active_stream);
if let Some(points) = chromatogram {
let (xs, ys): (Vec<_>, Vec<_>) = points
.into_iter()
.map(|[time, value]| (time, value))
.unzip();
return Some(Trace1d {
xs,
ys,
x_reversed: false,
});
}
let chromatogram = super::mass_spec_tic::resolve_stream_chromatogram(
&data.run,
data.active_stream,
plotx_io::ChromatogramKind::TotalIonCurrent,
)?;
let (xs, ys) = chromatogram
.points
.into_iter()
.map(|[time, value]| (time, value))
.unzip();
Some(Trace1d {
xs: stream
.spectra
.iter()
.map(|scan| scan.retention_time_min)
.collect(),
ys: stream.spectra.iter().map(|scan| scan.tic).collect(),
xs,
ys,
x_reversed: false,
})
}
Expand Down
Loading
Loading