use std::cmp::max;
use std::collections::{HashMap, HashSet};
use std::fmt::Display;
use std::ops::Deref;
use std::pin::Pin;
use std::sync::Arc;
use anyhow::Result;
use async_stream::try_stream;
use dihardts_cstools::bloom_filter::BloomFilter;
use dihardts_omicstools::chemistry::amino_acid::{AminoAcid, CANONICAL_AMINO_ACIDS};
use dihardts_omicstools::proteomics::post_translational_modifications::PostTranslationalModification as PTM;
use futures::{pin_mut, Stream, StreamExt};
use itertools::Itertools;
use scylla::value::CqlValue;
use tokio::sync::mpsc::{unbounded_channel as channel, UnboundedSender as Sender};
use tracing::error;
use crate::chemistry::amino_acid::INTERNAL_GLYCINE;
use crate::entities::configuration::Configuration;
use crate::entities::peptide::MatchingPeptide;
use crate::functions::post_translational_modification::PTMCollection;
use crate::mass::convert::{to_float as mass_to_float, to_int as mass_to_int};
use crate::tools::peptide_partitioner::get_mass_partition;
use crate::{database::scylla::peptide_table::PeptideTable, entities::peptide::Peptide};
use super::client::Client;
pub trait FilterFunction: Send + Sync + Display {
fn is_match(&mut self, peptide: &Peptide) -> Result<bool>;
}
struct IsSwissProtFilterFunction;
impl FilterFunction for IsSwissProtFilterFunction {
fn is_match(&mut self, peptide: &Peptide) -> Result<bool> {
Ok(peptide.get_is_swiss_prot())
}
}
impl Display for IsSwissProtFilterFunction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "is SwissProt")
}
}
struct IsTrEMBLFilterFunction;
impl FilterFunction for IsTrEMBLFilterFunction {
fn is_match(&mut self, peptide: &Peptide) -> Result<bool> {
Ok(peptide.get_is_trembl())
}
}
impl Display for IsTrEMBLFilterFunction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "is TrEMBL")
}
}
pub struct ThreadSafeDistinctFilterFunction {
bloom_filter: BloomFilter,
}
impl FilterFunction for ThreadSafeDistinctFilterFunction {
fn is_match(&mut self, peptide: &Peptide) -> Result<bool> {
if self.bloom_filter.contains(peptide.get_sequence())? {
return Ok(false);
}
self.bloom_filter.add(peptide.get_sequence())?;
Ok(true)
}
}
impl Display for ThreadSafeDistinctFilterFunction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "distinct")
}
}
struct TaxonomyFilterFunction {
taxonomy_ids: Arc<Vec<i64>>,
}
impl FilterFunction for TaxonomyFilterFunction {
fn is_match(&mut self, peptide: &Peptide) -> Result<bool> {
for taxonomy_id in self.taxonomy_ids.iter() {
if peptide.get_taxonomy_ids().contains(taxonomy_id) {
return Ok(true);
}
}
Ok(false)
}
}
impl Display for TaxonomyFilterFunction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "taxonomy in [{}]", self.taxonomy_ids.iter().join(", "))
}
}
struct ProteomeFilterFunction {
proteome_ids: Arc<Vec<String>>,
}
impl FilterFunction for ProteomeFilterFunction {
fn is_match(&mut self, peptide: &Peptide) -> Result<bool> {
for proteome_id in self.proteome_ids.iter() {
if peptide.get_proteome_ids().contains(proteome_id) {
return Ok(true);
}
}
Ok(false)
}
}
impl Display for ProteomeFilterFunction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "proteome in [{}]", self.proteome_ids.iter().join(", "))
}
}
struct StartsWithFilterFunction {
amino_acid: char,
}
impl FilterFunction for StartsWithFilterFunction {
fn is_match(&mut self, peptide: &Peptide) -> Result<bool> {
Ok(peptide.get_sequence().starts_with(self.amino_acid))
}
}
impl Display for StartsWithFilterFunction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "starts with '{}'", self.amino_acid)
}
}
struct EndsWithFilterFunction {
amino_acid: char,
}
impl FilterFunction for EndsWithFilterFunction {
fn is_match(&mut self, peptide: &Peptide) -> Result<bool> {
Ok(peptide.get_sequence().ends_with(self.amino_acid))
}
}
impl Display for EndsWithFilterFunction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ends with '{}'", self.amino_acid)
}
}
struct EqualsNumberOfOccurrencesFilterFunction {
amino_acid: char,
amount: i16,
}
impl FilterFunction for EqualsNumberOfOccurrencesFilterFunction {
fn is_match(&mut self, peptide: &Peptide) -> Result<bool> {
let count = peptide.get_aa_count(self.amino_acid);
Ok(count == self.amount)
}
}
impl Display for EqualsNumberOfOccurrencesFilterFunction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "occurences of '{}' == {}", self.amino_acid, self.amount,)
}
}
struct GreaterOrEqualsNumberOfOccurrencesFilterFunction {
amino_acid: char,
amount: i16,
}
impl FilterFunction for GreaterOrEqualsNumberOfOccurrencesFilterFunction {
fn is_match(&mut self, peptide: &Peptide) -> Result<bool> {
let count = peptide.get_aa_count(self.amino_acid);
Ok(count >= self.amount)
}
}
impl Display for GreaterOrEqualsNumberOfOccurrencesFilterFunction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "occurences of '{}' >= {}", self.amino_acid, self.amount,)
}
}
struct NoOccurrencesFilterFunction {
amino_acid: char,
}
impl FilterFunction for NoOccurrencesFilterFunction {
fn is_match(&mut self, peptide: &Peptide) -> Result<bool> {
let count = peptide.get_aa_count(self.amino_acid);
Ok(count == 0)
}
}
impl Display for NoOccurrencesFilterFunction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "occurences of '{}' == 0", self.amino_acid,)
}
}
pub type FalliblePeptideStream = Pin<Box<dyn Stream<Item = Result<MatchingPeptide>> + Send>>;
pub type FinalizedPeptideConditionMap = HashMap<usize, Vec<(i64, i64, FinalizedPeptideCondition)>>;
#[allow(clippy::too_many_arguments)]
pub trait Search {
fn search(
client: Arc<Client>,
configuration: Arc<Configuration>,
mass: i64,
lower_mass_tolerance_ppm: i64,
upper_mass_tolerance_ppm: i64,
max_variable_modifications: usize,
distinct: bool,
taxonomy_ids: Option<Vec<i64>>,
proteome_ids: Option<Vec<String>>,
is_reviewed: Option<bool>,
ptm_collection: &PTMCollection,
resolve_modifications: bool,
num_threads: Option<usize>,
) -> impl std::future::Future<Output = Result<FalliblePeptideStream>> + Send;
fn create_filter_pipeline(
distinct: bool,
taxonomy_ids: Option<Arc<Vec<i64>>>,
proteome_ids: Option<Arc<Vec<String>>>,
is_reviewed: Option<bool>,
) -> Result<Vec<Box<dyn FilterFunction>>> {
let mut filter_pipeline: Vec<Box<dyn FilterFunction>> = Vec::new();
if distinct {
filter_pipeline.push(Box::new(ThreadSafeDistinctFilterFunction {
bloom_filter: BloomFilter::new_by_size_and_fp_prob(80_000_000, 0.001)?,
}));
}
if let Some(taxonomy_ids) = taxonomy_ids {
filter_pipeline.push(Box::new(TaxonomyFilterFunction { taxonomy_ids }));
}
if let Some(proteome_ids) = proteome_ids {
filter_pipeline.push(Box::new(ProteomeFilterFunction { proteome_ids }));
}
if let Some(is_reviewed) = is_reviewed {
if is_reviewed {
filter_pipeline.push(Box::new(IsSwissProtFilterFunction {}));
} else {
filter_pipeline.push(Box::new(IsTrEMBLFilterFunction {}));
}
}
Ok(filter_pipeline)
}
fn search_with_ptm_conditions(
task_id: usize,
client: Arc<Client>,
partition: usize,
mut conditions: Vec<(i64, i64, FinalizedPeptideCondition)>,
mut filter_pipeline: Vec<Box<dyn FilterFunction>>,
resolve_modifications: bool,
peptide_sender: Sender<Result<MatchingPeptide>>,
) -> impl std::future::Future<Output = Result<()>> + Send {
async move {
let partition = CqlValue::BigInt(partition as i64);
for (lower_mass_limit, upper_mass_limit, ptm_condition) in conditions.iter_mut() {
let lower_mass_limit = CqlValue::BigInt(*lower_mass_limit);
let upper_mass_limit = CqlValue::BigInt(*upper_mass_limit);
let query_params = vec![&partition, &lower_mass_limit, &upper_mass_limit];
let peptide_stream = match PeptideTable::select(
client.as_ref(),
"WHERE partition = ? AND mass >= ? AND mass <= ?",
&query_params,
)
.await
{
Ok(stream) => stream,
Err(err) => {
error!("task {}: error creating peptide stream: {}", task_id, err);
return Err(err);
}
};
pin_mut!(peptide_stream);
'peptide_loop: while let Some(peptide) = peptide_stream.next().await {
let peptide = match peptide {
Ok(peptide) => peptide,
Err(err) => {
error!("task {}: error receiving peptide: {}", task_id, err);
return Err(err);
}
};
if !ptm_condition.check_peptide(&peptide) {
continue;
}
for filter in filter_pipeline.iter_mut() {
if !filter.is_match(&peptide)? {
continue 'peptide_loop;
}
}
let additional_sequences = if resolve_modifications {
ptm_condition.modify_sequence(peptide.get_sequence())
} else {
Vec::new()
};
match peptide_sender
.send(Ok(MatchingPeptide::new(peptide, additional_sequences)))
{
Ok(_) => {}
Err(err) => {
error!("task {}: error sending peptide: {}", task_id, err);
return Err(err.into());
}
};
}
}
drop(peptide_sender);
Ok(())
}
}
fn search_without_ptm_condition(
client: Arc<Client>,
configuration: Arc<Configuration>,
mass: i64,
lower_mass_tolerance_ppm: i64,
upper_mass_tolerance_ppm: i64,
mut filter_pipeline: Vec<Box<dyn FilterFunction>>,
peptide_sender: Sender<Result<MatchingPeptide>>,
) -> impl std::future::Future<Output = Result<()>> + Send {
async move {
let lower_mass_limit = mass - (mass / 1_000_000 * lower_mass_tolerance_ppm);
let upper_mass_limit = mass + (mass / 1_000_000 * upper_mass_tolerance_ppm);
let peptide_stream = PeptideTable::select_by_mass_range(
client.as_ref(),
lower_mass_limit,
upper_mass_limit,
configuration.get_partition_limits(),
)
.await?;
pin_mut!(peptide_stream);
'peptide_loop: while let Some(peptide) = peptide_stream.next().await {
let peptide = peptide?;
for filter in filter_pipeline.iter_mut() {
if !filter.is_match(&peptide)? {
continue 'peptide_loop;
}
}
peptide_sender.send(Ok(MatchingPeptide::new(peptide, Vec::new())))?;
}
Ok(())
}
}
fn split_and_sort_peptide_conditions(
peptide_conditions: Vec<PeptideCondition>,
partition_limits: &[i64],
lower_mass_tolerance_ppm: i64,
upper_mass_tolerance_ppm: i64,
) -> Result<FinalizedPeptideConditionMap> {
let mut sorted_peptide_conditions: FinalizedPeptideConditionMap = HashMap::new();
for peptide_condition in peptide_conditions {
let lower_mass_limit = peptide_condition.query_mass
- (peptide_condition.query_mass / 1_000_000 * lower_mass_tolerance_ppm);
let upper_mass_limit = peptide_condition.query_mass
+ (peptide_condition.query_mass / 1_000_000 * upper_mass_tolerance_ppm);
let lower_partition_index = get_mass_partition(partition_limits, lower_mass_limit)?;
let upper_partition_index = get_mass_partition(partition_limits, upper_mass_limit)?;
if lower_partition_index == upper_partition_index {
sorted_peptide_conditions
.entry(lower_partition_index)
.or_default()
.push((lower_mass_limit, upper_mass_limit, peptide_condition.into()));
} else {
#[allow(clippy::needless_range_loop)]
for partition in lower_partition_index..=upper_partition_index {
sorted_peptide_conditions
.entry(partition)
.or_default()
.push((
lower_mass_limit,
upper_mass_limit,
peptide_condition.clone().into(),
));
}
}
}
Ok(sorted_peptide_conditions)
}
}
pub struct MultiTaskSearch;
impl Search for MultiTaskSearch {
async fn search(
client: Arc<Client>,
configuration: Arc<Configuration>,
mass: i64,
lower_mass_tolerance_ppm: i64,
upper_mass_tolerance_ppm: i64,
max_variable_modifications: usize,
distinct: bool,
taxonomy_ids: Option<Vec<i64>>,
proteome_ids: Option<Vec<String>>,
is_reviewed: Option<bool>,
ptm_collection: &PTMCollection<'_>,
resolve_modifications: bool,
_num_threads: Option<usize>,
) -> Result<FalliblePeptideStream> {
let taxonomy_ids = taxonomy_ids.map(Arc::new);
let proteome_ids = proteome_ids.map(Arc::new);
let min_mass = match configuration.get_min_peptide_length() {
Some(min_length) => INTERNAL_GLYCINE.get_mono_mass_int() * min_length as i64,
None => 0,
};
let largest_negative_static_ptm = ptm_collection
.get_static_ptms()
.iter()
.filter(|ptm| ptm.get_mass_delta().is_sign_negative())
.fold(0_i64, |acc, ptm| {
acc.min(mass_to_int(*ptm.get_mass_delta()))
})
.abs();
let largest_negative_variable_ptm = ptm_collection
.get_variable_ptms()
.iter()
.filter(|ptm| ptm.get_mass_delta().is_sign_negative())
.fold(0_i64, |acc, ptm| {
acc.min(mass_to_int(*ptm.get_mass_delta()))
})
.abs();
let amino_acid_average = mass_to_int(
CANONICAL_AMINO_ACIDS
.iter()
.map(|aa| aa.get_mono_mass())
.sum::<f64>()
/ CANONICAL_AMINO_ACIDS.len() as f64,
);
let possible_peptide_length = ((mass / amino_acid_average) as f64 * 1.3) as i64;
let max_mass = mass
+ (largest_negative_static_ptm * possible_peptide_length)
+ (largest_negative_variable_ptm * possible_peptide_length);
let sorted_ptm_conditions = Self::split_and_sort_peptide_conditions(
PeptideCondition::from_ptm_collection(
ptm_collection,
mass,
min_mass,
max_mass,
max_variable_modifications,
),
configuration.get_partition_limits(),
lower_mass_tolerance_ppm,
upper_mass_tolerance_ppm,
)?;
let (peptide_sender, mut peptide_receiver) = channel::<Result<MatchingPeptide>>();
Ok(Box::pin(try_stream! {
let mut tasks: Vec<tokio::task::JoinHandle<Result<()>>> = Vec::with_capacity(max(sorted_ptm_conditions.len(), 1));
if !sorted_ptm_conditions.is_empty() {
for (task_id, (partition, conditions)) in sorted_ptm_conditions.into_iter().enumerate() {
let filter_pipeline = Self::create_filter_pipeline(
distinct,
taxonomy_ids.clone(),
proteome_ids.clone(),
is_reviewed,
)?;
tasks.push(tokio::task::spawn(
Self::search_with_ptm_conditions(
task_id,
client.clone(),
partition,
conditions,
filter_pipeline,
resolve_modifications,
peptide_sender.clone(),
)
));
}
} else {
tasks.push(tokio::task::spawn(
Self::search_without_ptm_condition(
client.clone(),
configuration.clone(),
mass,
lower_mass_tolerance_ppm,
upper_mass_tolerance_ppm,
Self::create_filter_pipeline(
distinct,
taxonomy_ids,
proteome_ids,
is_reviewed,
)?,
peptide_sender.clone(),
)
));
}
drop(peptide_sender);
while let Some(peptide) = peptide_receiver.recv().await {
yield peptide?;
}
for task in tasks {
task.await??;
}
}))
}
}
#[derive(Clone)]
pub struct PeptideCondition {
query_mass: i64,
static_ptms: Vec<PTM>,
variable_ptms: Vec<PTM>,
n_terminal_ptm: Option<PTM>,
c_terminal_ptm: Option<PTM>,
n_bond_ptm: Option<PTM>,
c_bond_ptm: Option<PTM>,
excluded_amino_acids: HashSet<char>,
}
impl PeptideCondition {
pub fn new(targeted_mass: i64) -> Self {
Self {
query_mass: targeted_mass,
static_ptms: Vec::new(),
variable_ptms: Vec::new(),
n_terminal_ptm: None,
c_terminal_ptm: None,
n_bond_ptm: None,
c_bond_ptm: None,
excluded_amino_acids: HashSet::new(),
}
}
pub fn add_static_ptm(&mut self, ptm: &PTM) -> bool {
let mass_delta_int = mass_to_int(*ptm.get_mass_delta());
if mass_delta_int > self.query_mass {
return false;
}
self.static_ptms.push(ptm.clone());
self.query_mass -= mass_delta_int;
true
}
pub fn add_variable_ptm(&mut self, ptm: &PTM) -> bool {
let mass_delta_int = mass_to_int(*ptm.get_mass_delta());
if mass_delta_int > self.query_mass {
return false;
}
self.variable_ptms.push(ptm.clone());
self.query_mass -= mass_delta_int;
true
}
pub fn set_n_terminal_ptm(&mut self, ptm: &PTM) -> bool {
let mass_delta_int = mass_to_int(*ptm.get_mass_delta());
if self.n_terminal_ptm.is_some() || mass_delta_int > self.query_mass {
return false;
}
self.n_terminal_ptm = Some(ptm.clone());
self.query_mass -= mass_delta_int;
true
}
pub fn set_c_terminal_ptm(&mut self, ptm: &PTM) -> bool {
let mass_delta_int = mass_to_int(*ptm.get_mass_delta());
if self.c_terminal_ptm.is_some() || mass_delta_int > self.query_mass {
return false;
}
self.c_terminal_ptm = Some(ptm.clone());
self.query_mass -= mass_delta_int;
true
}
pub fn set_n_bond_ptm(&mut self, ptm: &PTM) -> bool {
let mass_delta_int = mass_to_int(*ptm.get_mass_delta());
if self.n_bond_ptm.is_some() || mass_delta_int > self.query_mass {
return false;
}
self.n_bond_ptm = Some(ptm.clone());
self.query_mass -= mass_delta_int;
true
}
pub fn set_c_bond_ptm(&mut self, ptm: &PTM) -> bool {
let mass_delta_int = mass_to_int(*ptm.get_mass_delta());
if self.c_bond_ptm.is_some() || mass_delta_int > self.query_mass {
return false;
}
self.c_bond_ptm = Some(ptm.clone());
self.query_mass -= mass_delta_int;
true
}
pub fn add_excluded_amino_acid(&mut self, amino_acid: &dyn AminoAcid) {
self.excluded_amino_acids
.insert(*amino_acid.get_one_letter_code());
}
pub fn modify_sequence(&self, sequence: &str) -> Vec<String> {
let mut variable_modifications_map: HashMap<char, Vec<&PTM>> = HashMap::new();
for ptm in self.variable_ptms.iter() {
variable_modifications_map
.entry(*ptm.get_amino_acid().get_one_letter_code())
.and_modify(|mods| mods.push(ptm))
.or_insert(vec![ptm]);
}
let mut proforma_sequences: HashSet<String> = HashSet::new();
let static_mods = self
.static_ptms
.iter()
.map(|ptm| {
format!(
"[{:+}]@{}",
ptm.get_mass_delta(),
ptm.get_amino_acid().get_one_letter_code(),
)
})
.collect::<HashSet<_>>()
.into_iter()
.join(",");
let mut modded_peptide = String::with_capacity(sequence.len());
if !static_mods.is_empty() {
modded_peptide = format!("<{static_mods}>",);
}
if let Some(n_bond_ptm) = &self.n_bond_ptm {
modded_peptide.push_str(&format!("[{}]-", n_bond_ptm.get_mass_delta()));
}
self.inner_modify_sequence(
sequence,
modded_peptide.clone(),
&variable_modifications_map,
0,
0,
&mut proforma_sequences,
);
proforma_sequences.into_iter().collect::<Vec<_>>()
}
#[allow(clippy::too_many_arguments)]
fn inner_modify_sequence(
&self,
peptide: &str,
mut modified_peptide: String,
variable_modifications_map: &HashMap<char, Vec<&PTM>>,
position: usize,
applied_vmods: usize,
proforma_sequences: &mut HashSet<String>,
) {
if position >= peptide.len() {
self.end_modify_sequence(modified_peptide, applied_vmods, proforma_sequences);
return;
}
modified_peptide.push(peptide.chars().nth(position).unwrap());
if position == 0 && self.n_terminal_ptm.is_some() {
modified_peptide.push_str(&format!(
"[{:+}]",
self.n_terminal_ptm.as_ref().unwrap().get_mass_delta()
));
self.inner_modify_sequence(
peptide,
modified_peptide,
variable_modifications_map,
position + 1,
applied_vmods,
proforma_sequences,
);
} else if position == peptide.len() - 1 && self.c_terminal_ptm.is_some() {
modified_peptide.push_str(&format!(
"[{:+}]",
self.c_terminal_ptm.as_ref().unwrap().get_mass_delta()
));
self.inner_modify_sequence(
peptide,
modified_peptide,
variable_modifications_map,
position + 1,
applied_vmods,
proforma_sequences,
);
} else {
self.inner_modify_sequence(
peptide,
modified_peptide.clone(),
variable_modifications_map,
position + 1,
applied_vmods,
proforma_sequences,
);
if applied_vmods < self.variable_ptms.len() {
if let Some(modifications) =
variable_modifications_map.get(&peptide.chars().nth(position).unwrap())
{
for modification in modifications.iter() {
let next_modified_peptide =
format!("{}[{:+}]", &modified_peptide, modification.get_mass_delta());
self.inner_modify_sequence(
peptide,
next_modified_peptide,
variable_modifications_map,
position + 1,
applied_vmods + 1,
proforma_sequences,
);
}
}
}
}
}
fn end_modify_sequence(
&self,
mut modified_peptide: String,
applied_vmods: usize,
proforma_sequences: &mut HashSet<String>,
) {
if let Some(c_bond_ptm) = &self.c_bond_ptm {
modified_peptide.push_str(&format!("-[{}]", c_bond_ptm.get_mass_delta(),));
}
if applied_vmods == self.variable_ptms.len() {
proforma_sequences.insert(modified_peptide);
}
}
pub fn from_ptm_collection(
ptm_collection: &PTMCollection,
targeted_mass: i64,
min_mass: i64,
max_mass: i64,
max_variable_modifications: usize,
) -> Vec<PeptideCondition> {
if ptm_collection.is_empty() {
return Vec::new();
}
let mut resulting_conditions: Vec<PeptideCondition> = Vec::new();
let mut condition = PeptideCondition::new(targeted_mass);
for static_ptm in ptm_collection.get_static_ptms() {
condition.add_excluded_amino_acid(static_ptm.get_amino_acid());
}
resulting_conditions.push(condition);
let condition = PeptideCondition::new(targeted_mass);
Self::calculate_peptide_conditions_for_static_modifications(
ptm_collection,
min_mass,
max_mass,
condition.clone(),
0,
&mut resulting_conditions,
);
let current_len = resulting_conditions.len();
for i in 0..current_len {
let condition = resulting_conditions[i].clone();
Self::calculate_peptide_conditions_for_variable_modifications(
ptm_collection,
min_mass,
max_mass,
max_variable_modifications,
condition,
0,
&mut resulting_conditions,
)
}
let current_len = resulting_conditions.len();
for i in 0..current_len {
let mut condition = resulting_conditions[i].clone();
for modification in ptm_collection.get_n_terminal_ptms() {
if condition.set_n_terminal_ptm(modification) {
resulting_conditions.push(condition.clone());
}
}
}
let current_len = resulting_conditions.len();
for i in 0..current_len {
let mut condition = resulting_conditions[i].clone();
for modification in ptm_collection.get_c_terminal_ptms() {
if condition.set_c_terminal_ptm(modification) {
resulting_conditions.push(condition.clone());
}
}
}
let current_len = resulting_conditions.len();
for i in 0..current_len {
let mut condition = resulting_conditions[i].clone();
for modification in ptm_collection.get_n_bond_ptms() {
if condition.set_n_bond_ptm(modification) {
resulting_conditions.push(condition.clone());
}
}
}
let current_len = resulting_conditions.len();
for i in 0..current_len {
let mut condition = resulting_conditions[i].clone();
for modification in ptm_collection.get_c_bond_ptms() {
if condition.set_c_bond_ptm(modification) {
resulting_conditions.push(condition.clone());
}
}
}
resulting_conditions
}
fn calculate_peptide_conditions_for_static_modifications(
ptm_collection: &PTMCollection,
min_mass: i64,
max_mass: i64,
mut condition: PeptideCondition,
modification_position: usize,
resulting_conditions: &mut Vec<PeptideCondition>,
) {
if modification_position >= ptm_collection.get_static_ptms().len() {
return;
}
Self::calculate_peptide_conditions_for_static_modifications(
ptm_collection,
min_mass,
max_mass,
condition.clone(),
modification_position + 1,
resulting_conditions,
);
while condition.add_static_ptm(ptm_collection.get_static_ptms()[modification_position]) {
if condition.query_mass < min_mass || condition.query_mass > max_mass {
break;
}
resulting_conditions.push(condition.clone());
Self::calculate_peptide_conditions_for_static_modifications(
ptm_collection,
min_mass,
max_mass,
condition.clone(),
modification_position + 1,
resulting_conditions,
);
}
}
fn calculate_peptide_conditions_for_variable_modifications(
ptm_collection: &PTMCollection,
min_mass: i64,
max_mass: i64,
max_variable_modifications: usize,
mut condition: PeptideCondition,
modification_position: usize,
resulting_conditions: &mut Vec<PeptideCondition>,
) {
if modification_position >= ptm_collection.get_variable_ptms().len() {
return;
}
Self::calculate_peptide_conditions_for_variable_modifications(
ptm_collection,
min_mass,
max_mass,
max_variable_modifications,
condition.clone(),
modification_position + 1,
resulting_conditions,
);
while condition.add_variable_ptm(ptm_collection.get_variable_ptms()[modification_position])
{
if condition.variable_ptms.len() > max_variable_modifications
|| condition.query_mass < min_mass
|| condition.query_mass > max_mass
{
break;
}
resulting_conditions.push(condition.clone());
Self::calculate_peptide_conditions_for_variable_modifications(
ptm_collection,
min_mass,
max_mass,
max_variable_modifications,
condition.clone(),
modification_position + 1,
resulting_conditions,
);
}
}
}
impl Display for PeptideCondition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let static_mods = self
.static_ptms
.iter()
.map(|ptm| {
format!(
"[{}]@{}",
ptm.get_mass_delta(),
ptm.get_amino_acid().get_one_letter_code()
)
})
.join(", ");
let variable_mods = self
.variable_ptms
.iter()
.map(|ptm| {
format!(
"v[{}]@{}",
ptm.get_mass_delta(),
ptm.get_amino_acid().get_one_letter_code()
)
})
.join(", ");
let n_bind_mod = match &self.n_bond_ptm {
Some(ptm) => format!("[{}]-", ptm.get_mass_delta()),
None => String::new(),
};
let c_bind_mod = match &self.c_bond_ptm {
Some(ptm) => format!("-[{}]", ptm.get_mass_delta()),
None => String::new(),
};
let n_terminal_mod = match &self.n_bond_ptm {
Some(ptm) => format!(
"cterm{}@{}",
ptm.get_mass_delta(),
ptm.get_amino_acid().get_one_letter_code()
),
None => String::new(),
};
let c_terminal_mod = match &self.c_bond_ptm {
Some(ptm) => format!(
"nterm{}@{}",
ptm.get_mass_delta(),
ptm.get_amino_acid().get_one_letter_code()
),
None => String::new(),
};
write!(
f,
"PeptideCondition: '<{static_mods}>{n_bind_mod}{n_terminal_mod}{variable_mods}{c_terminal_mod}{c_bind_mod}' @ {} Da",
mass_to_float(self.query_mass),
)
}
}
pub struct FinalizedPeptideCondition {
inner_peptide_condition: PeptideCondition,
filter_functions: Vec<Box<dyn FilterFunction>>,
}
impl FinalizedPeptideCondition {
fn get_filter_functions(peptide_condition: &PeptideCondition) -> Vec<Box<dyn FilterFunction>> {
let mut filter_functions: Vec<Box<dyn FilterFunction>> = Vec::with_capacity(
peptide_condition.static_ptms.len()
+ peptide_condition.variable_ptms.len()
+ peptide_condition.excluded_amino_acids.len()
+ 2, );
for excluded_aa in peptide_condition.excluded_amino_acids.iter() {
filter_functions.push(Box::new(NoOccurrencesFilterFunction {
amino_acid: *excluded_aa,
}));
}
let mut statically_modified_amino_acid_counts: HashMap<char, i16> = HashMap::new();
for ptm in peptide_condition.static_ptms.iter() {
statically_modified_amino_acid_counts
.entry(*ptm.get_amino_acid().get_one_letter_code())
.and_modify(|count| *count += 1)
.or_insert(1);
}
for (amino_acid, amount) in statically_modified_amino_acid_counts
.into_iter()
.sorted_by(|x, y| x.0.cmp(&y.0))
{
filter_functions.push(Box::new(EqualsNumberOfOccurrencesFilterFunction {
amino_acid,
amount,
}));
}
let mut variable_modified_amino_acid_counts: HashMap<char, i16> = HashMap::new();
for ptm in peptide_condition.variable_ptms.iter() {
variable_modified_amino_acid_counts
.entry(*ptm.get_amino_acid().get_one_letter_code())
.and_modify(|count| *count += 1)
.or_insert(1);
}
if let Some(ptm) = &peptide_condition.n_terminal_ptm {
variable_modified_amino_acid_counts
.entry(*ptm.get_amino_acid().get_one_letter_code())
.and_modify(|count| *count += 1)
.or_insert(1);
filter_functions.push(Box::new(StartsWithFilterFunction {
amino_acid: *ptm.get_amino_acid().get_one_letter_code(),
}));
}
if let Some(ptm) = &peptide_condition.c_terminal_ptm {
variable_modified_amino_acid_counts
.entry(*ptm.get_amino_acid().get_one_letter_code())
.and_modify(|count| *count += 1)
.or_insert(1);
filter_functions.push(Box::new(EndsWithFilterFunction {
amino_acid: *ptm.get_amino_acid().get_one_letter_code(),
}));
}
for (amino_acid, amount) in variable_modified_amino_acid_counts
.into_iter()
.sorted_by(|x, y| x.0.cmp(&y.0))
{
filter_functions.push(Box::new(GreaterOrEqualsNumberOfOccurrencesFilterFunction {
amino_acid,
amount,
}));
}
filter_functions
}
pub fn check_peptide(&mut self, peptide: &Peptide) -> bool {
for filter in self.filter_functions.iter_mut() {
if !filter.is_match(peptide).unwrap_or(false) {
return false;
}
}
true
}
}
impl Deref for FinalizedPeptideCondition {
type Target = PeptideCondition;
fn deref(&self) -> &Self::Target {
&self.inner_peptide_condition
}
}
impl From<PeptideCondition> for FinalizedPeptideCondition {
fn from(peptide_condition: PeptideCondition) -> Self {
let filter_functions = FinalizedPeptideCondition::get_filter_functions(&peptide_condition);
Self {
inner_peptide_condition: peptide_condition,
filter_functions,
}
}
}
impl Display for FinalizedPeptideCondition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let filter_descriptions = self
.filter_functions
.iter()
.map(|filter| format!("{filter}"))
.join(" && ");
write!(
f,
"FinalizedPeptideCondition: {filter_descriptions} @ {}",
mass_to_float(self.query_mass)
)
}
}
#[cfg(test)]
mod tests {
use dihardts_omicstools::{
chemistry::amino_acid::get_amino_acid_by_one_letter_code,
proteomics::post_translational_modifications::{ModificationType, Position},
};
use super::*;
#[tokio::test]
async fn test_peptide_condition_from_ptm_collection() {
let ptms = vec![
PTM::new(
"carba of C",
get_amino_acid_by_one_letter_code('C').unwrap(),
57.021464,
ModificationType::Static,
Position::Anywhere,
),
PTM::new(
"oxi of M",
get_amino_acid_by_one_letter_code('M').unwrap(),
15.99491,
ModificationType::Variable,
Position::Anywhere,
),
PTM::new(
"oxi of term M",
get_amino_acid_by_one_letter_code('M').unwrap(),
16.99491,
ModificationType::Variable,
Position::Terminus(dihardts_omicstools::proteomics::peptide::Terminus::N),
),
PTM::new(
"oxi of term K",
get_amino_acid_by_one_letter_code('K').unwrap(),
20.3,
ModificationType::Variable,
Position::Terminus(dihardts_omicstools::proteomics::peptide::Terminus::C),
),
PTM::new(
"something on N-bond",
get_amino_acid_by_one_letter_code('X').unwrap(),
10.0,
ModificationType::Variable,
Position::Terminus(dihardts_omicstools::proteomics::peptide::Terminus::N),
),
PTM::new(
"something on N-bond",
get_amino_acid_by_one_letter_code('X').unwrap(),
40.3,
ModificationType::Variable,
Position::Terminus(dihardts_omicstools::proteomics::peptide::Terminus::C),
),
];
let ptm_collection = PTMCollection::new(&ptms).unwrap();
let mass: f64 = 839.403366202;
let conditions = PeptideCondition::from_ptm_collection(
&ptm_collection,
mass_to_int(mass),
mass_to_int(
get_amino_acid_by_one_letter_code('G')
.unwrap()
.get_mono_mass()
* 6.0,
),
mass_to_int(mass),
2,
);
let stringyfied_conditions = conditions
.into_iter()
.map(|condition| format!("{}", FinalizedPeptideCondition::from(condition)))
.collect::<HashSet<_>>();
let expected_conditions =
std::fs::read_to_string("test_files/finalized_peptide_condition.txt")
.unwrap()
.split("\n")
.map(|line| line.to_string())
.collect::<HashSet<_>>();
assert_eq!(stringyfied_conditions.len(), expected_conditions.len());
for condition in stringyfied_conditions.iter() {
assert!(
expected_conditions.contains(condition),
"Condition not found: {condition}"
);
}
}
#[test]
fn test_condition_building_and_sequence_modification() {
let sequence = "MFCQLAKTCPVQLWVDMSTPPPGTRVR";
let mass = 3060.516981066636;
let peptide = Peptide::new(
0,
mass_to_int(3060.516981066636),
sequence.to_string(),
2,
Vec::new(),
false,
false,
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
)
.unwrap();
let carbamidomethylation_c = PTM::new(
"carba of C",
get_amino_acid_by_one_letter_code('C').unwrap(),
57.021464,
ModificationType::Static,
Position::Anywhere,
);
let oxidation_m = PTM::new(
"oxi of M",
get_amino_acid_by_one_letter_code('M').unwrap(),
15.99491,
ModificationType::Variable,
Position::Anywhere,
);
let something_terminal_m = PTM::new(
"oxi of term M",
get_amino_acid_by_one_letter_code('M').unwrap(),
16.99491,
ModificationType::Variable,
Position::Terminus(dihardts_omicstools::proteomics::peptide::Terminus::N),
);
let something_terminal_r = PTM::new(
"oxi of term R",
get_amino_acid_by_one_letter_code('R').unwrap(),
20.3,
ModificationType::Variable,
Position::Terminus(dihardts_omicstools::proteomics::peptide::Terminus::C),
);
let something_bond_n = PTM::new(
"something on N-bond",
get_amino_acid_by_one_letter_code('X').unwrap(),
10.0,
ModificationType::Variable,
Position::Terminus(dihardts_omicstools::proteomics::peptide::Terminus::N),
);
let something_bond_c = PTM::new(
"something on N-bond",
get_amino_acid_by_one_letter_code('X').unwrap(),
40.3,
ModificationType::Variable,
Position::Terminus(dihardts_omicstools::proteomics::peptide::Terminus::C),
);
let mut condition = PeptideCondition::new(mass_to_int(mass));
condition.add_static_ptm(&carbamidomethylation_c);
condition.add_static_ptm(&carbamidomethylation_c);
condition.add_variable_ptm(&oxidation_m);
let mut finalized_condition: FinalizedPeptideCondition = condition.clone().into();
assert!(finalized_condition.check_peptide(&peptide));
let mut modified_sequences = condition.modify_sequence(sequence);
modified_sequences.sort();
assert_eq!(
modified_sequences.as_slice(),
[
"<[+57.021464]@C>MFCQLAKTCPVQLWVDM[+15.99491]STPPPGTRVR",
"<[+57.021464]@C>M[+15.99491]FCQLAKTCPVQLWVDMSTPPPGTRVR"
]
);
condition.set_n_terminal_ptm(&something_terminal_m);
finalized_condition = condition.clone().into();
assert!(finalized_condition.check_peptide(&peptide));
let mut modified_sequences = condition.modify_sequence(sequence);
modified_sequences.sort();
assert_eq!(
modified_sequences.as_slice(),
["<[+57.021464]@C>M[+16.99491]FCQLAKTCPVQLWVDM[+15.99491]STPPPGTRVR",]
);
condition.set_c_terminal_ptm(&something_terminal_r);
finalized_condition = condition.clone().into();
assert!(finalized_condition.check_peptide(&peptide));
let mut modified_sequences = condition.modify_sequence(sequence);
modified_sequences.sort();
assert_eq!(
modified_sequences.as_slice(),
["<[+57.021464]@C>M[+16.99491]FCQLAKTCPVQLWVDM[+15.99491]STPPPGTRVR[+20.3]",]
);
condition.set_n_bond_ptm(&something_bond_n);
finalized_condition = condition.clone().into();
assert!(finalized_condition.check_peptide(&peptide));
let mut modified_sequences = condition.modify_sequence(sequence);
modified_sequences.sort();
assert_eq!(
modified_sequences.as_slice(),
["<[+57.021464]@C>[10]-M[+16.99491]FCQLAKTCPVQLWVDM[+15.99491]STPPPGTRVR[+20.3]",]
);
condition.set_c_bond_ptm(&something_bond_c);
finalized_condition = condition.clone().into();
assert!(finalized_condition.check_peptide(&peptide));
let mut modified_sequences = condition.modify_sequence(sequence);
modified_sequences.sort();
assert_eq!(
modified_sequences.as_slice(),
["<[+57.021464]@C>[10]-M[+16.99491]FCQLAKTCPVQLWVDM[+15.99491]STPPPGTRVR[+20.3]-[40.3]",]
);
}
}