Skip to content
Open
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
62 changes: 62 additions & 0 deletions backend/api/tandem_ms_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from typing import Any, Dict
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
from backend.logic.tandem_ms import TandemMS

router = APIRouter(
prefix="/tandem_ms",
tags=["Tandem Mass Spectrometry"],
)


class TandemMSRequest(BaseModel):
fasta_content: str = Field(..., description="FASTA content as raw text")
amino_acid: str = Field(
...,
description="Amino acid protein sequence for tandem mass spectrometry analysis",
)
protease: str = Field(
"trypsin",
description="Protease used for digestion (e.g., trypsin, chymotrypsin)",
)
lower_limit: float = Field(
0.0, ge=0.0, le=100.0, description="Lower limit for mass/charge ratio"
)
upper_limit: float = Field(
3000.0, ge=0.0, le=3000.0, description="Upper limit for mass/charge ratio"
)


class TandemMSPeaksRequest(BaseModel):
amino_acid: str = Field(
..., description="Amino acid protein sequence for fragment analysis"
)


@router.post("/plot", response_model=Dict[str, Any])
async def plot_tandem_ms(body: TandemMSRequest) -> Any:
if not body.fasta_content.strip():
raise HTTPException(status_code=422, detail="No FASTA content provided.")

try:
sim = TandemMS()

return sim.runAnalysis(
sequence=body.amino_acid,
proteaseChoice=body.protease,
lowerLimit=body.lower_limit,
upperLimit=body.upper_limit,
)

except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc


@router.post("/by_peaks", response_model=Dict[str, Any])
async def by_peaks_tandem_ms(body: TandemMSPeaksRequest) -> Dict[str, Any]:
try:
sim = TandemMS()

return sim.sequencePeaks(peptide=body.amino_acid)
except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
9 changes: 6 additions & 3 deletions backend/logic/fractionation/fractionation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
from collections.abc import Sequence
from dataclasses import dataclass, field
from io import StringIO
from typing import Any
from typing import Any, TypeVar

from Bio import SeqIO
from Bio.SeqUtils.ProtParam import ProteinAnalysis

from backend.utility.protein import Protein


T = TypeVar("T")


class Fractionation:
@dataclass
class ProteinEntry:
Expand Down Expand Up @@ -91,7 +94,7 @@ def parse_fasta_text(fasta_content: str) -> list[Any]:
return list(SeqIO.parse(handle, "fasta"))

@staticmethod
def fractionate_with_overlap[T: ProteinEntry](
def fractionate_with_overlap(
items: Sequence[T],
n_fractions: int,
overlap: float,
Expand Down Expand Up @@ -122,7 +125,7 @@ def fractionate_with_overlap[T: ProteinEntry](
return fractions

@staticmethod
def process_fractions[T: ProteinEntry](fraction_lists: Sequence[Sequence[T]]):
def process_fractions(fraction_lists: Sequence[Sequence[T]]):
return [
{
"fractionIndex": index + 1,
Expand Down
75 changes: 75 additions & 0 deletions backend/logic/protease.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""
* Contains the methods needed for a Protease who cuts before or after apecific proteins without any other conditions. Otherwise the method cut needs to be overwritten and new conditions added
"""


class Protease:
def __init__(self):
self.cutAminoAcids = []
self.cutsBefore = False

def cut(self, sequence: str):
sequence = self.checkSequence(sequence)

# We are grabbing the positions/indexes where the Protease should expect to cut
cut_indexes = [
i if self.cutsBefore else i + 1 # If cuts after, index is + 1
for i, aa in enumerate(sequence)
# passing current amino acid, and next if it exists
if self.shouldCutHere(aa, sequence[i + 1] if i + 1 < len(sequence) else "")
]

# Sorted list of the cut indexes, with the first and last indexes added (for the first and last sequence)
cut_index_bounds = sorted({0, *cut_indexes, len(sequence)})

# Grabbing the sequences, zip gives the list of pairings for the sequence start and end index
sequence_indexes = [
sequence[a:b] for a, b in zip(cut_index_bounds, cut_index_bounds[1:])
]

return sequence_indexes

def shouldCutHere(self, currentAA: str, afterAA: str):
return currentAA in self.cutAminoAcids

def checkSequence(self, sequence: str):
# TODO implement proper check
"""if (sequence.contains(" ")) {
throw new ProteaseException("Sequence to be cut must not contain spaces.");
} else if (sequence.matches(".*\\d.*")) {
throw new ProteaseException("Sequence to be cut must not contain numbers.");
} else if (sequence.matches(".*[a-z].*")) {
throw new ProteaseException("Sequence to be cut must contain all upper case letters.");
}"""
return sequence


class Chymotrypsin(Protease):
def __init__(self):
super().__init__()
self.cutAminoAcids = ["Y", "W", "F"]
self.cutsBefore = False


class Thermolysin(Protease):
def __init__(self):
super().__init__()
self.cutAminoAcids = ["I", "L", "M", "V"]
self.cutsBefore = True


class Trypsin(Protease):
def __init__(self):
super().__init__()
self.cutAminoAcids = ["R", "K"]
self.cutsBefore = False

def shouldCutHere(self, currentAA: str, afterAA: str):
return currentAA in self.cutAminoAcids and afterAA != "P"


class ProteinaseK(Protease):
def __init__(self):
super().__init__()
self.cutAminoAcids = ["A", "F", "I", "L", "V", "W", "Y"]
self.cutsBefore = False
Loading
Loading