Skip to content
Draft
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
43 changes: 43 additions & 0 deletions backend/api/affinity_chromatography_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from typing import Any, Dict

from fastapi import APIRouter
from pydantic import BaseModel, Field

from backend.logic.affinity_chromatography import AffinityChromatography


router = APIRouter(
prefix="/affinity_chromatography",
tags=["Affinity Chromatography"],
)

class AffinityChromatographyRequest(BaseModel):
"""
Request body schema for running Affinity Chromatography.
"""
fasta_content: str = Field(..., description="FASTA content as raw text")
prosite_strong: str = Field(..., description="PROSITE motif matching strongly bound proteins")
prosite_weak: str = Field(..., description="PROSITE motif matching weakly bound proteins")

# Fractionation and simulation controls
fraction_count: int = Field(80, ge=1, le=500)
noise: float = Field(0.10, ge=0.0, le=1.0, description="Overlap between fractions")

@router.post("/process", response_model=Dict[str, Any])
async def process_affinity(body: AffinityChromatographyRequest) -> Any:
"""
Run affinity chromatography and return the simulated result.
"""
if not body.fasta_content.strip():
return {"error": "No FASTA content provided."}

try:
return AffinityChromatography.process(
fasta_content=body.fasta_content,
fraction_count=body.fraction_count,
prosite_strong=body.prosite_strong,
prosite_weak=body.prosite_weak
)
except ValueError as exc:
return {"error": str(exc)}

76 changes: 76 additions & 0 deletions backend/logic/affinity_chromatography.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from __future__ import annotations
from backend.utility.protein import Protein

from typing import Any, Dict

from backend.logic.fractionation.fractionation import Fractionation

class AffinityChromatography:
"""
Simulates affinity chromatography.

Main idea:
- Each different ligand in affinity chromatography bonds to different "motifs" at different strengths
- Divide all proteins into either strongly binded, weakly binded, or unbinded
- Wash comes out in first 1/2 fractions
- Weakly binded comes out in next 1/4 fractions
- Strongly bound comes out in next 1/4 fractions
"""

@staticmethod
def process (fasta_content: str, prosite_strong: str, prosite_weak: str, fraction_count: int = 80, noise: float = 0.10) -> Dict[str, Any]:
"""
Main affinity chromatography simulation function.

Parameters:
- prosite_strong: the PROSITE motif that matches strongly bound proteins
- prosite_weak: the PROSITE motif that matches weakly bound proteins
- fraction_count: number of output fractions
- noise: overlap between fractions
"""

if fraction_count < 4:
raise ValueError("fraction_count must be at least 4")
if noise < 0:
raise ValueError("noise must be >= 0")

# Parse all input FASTA records
entries, skipped = Fractionation.convert_fasta_to_protein_entries(fasta_content)

# 1. "extract" (filter and remove) all strongly bound proteins
strongly_bound = [entry for entry in entries if Protein.matches_prosite(entry.sequence, prosite_strong)]
unbound = [entry for entry in entries if not Protein.matches_prosite(entry.sequence, prosite_strong)]

# 2. "extract" (filter and remove) all weakly bound proteins
weakly_bound = [entry for entry in entries if Protein.matches_prosite(entry.sequence, prosite_weak)]
unbound = [entry for entry in unbound if not Protein.matches_prosite(entry.sequence, prosite_weak)]

# now we have 3 our 3 lists! divide them cool-ly into the fractions
# this is a very convoluted way to get the following ratios: 1/2 1/4 1/4
wash_fractions = fraction_count // 2
weak_fractions = (fraction_count - wash_fractions) // 2
strong_fractions = fraction_count - (wash_fractions + weak_fractions)

fractions = list(Fractionation.fractionate_with_overlap(unbound, wash_fractions, noise))
fractions.extend(Fractionation.fractionate_with_overlap(weakly_bound, weak_fractions, noise))
fractions.extend(Fractionation.fractionate_with_overlap(strongly_bound, strong_fractions, noise))

return {
"ok": True,
"params": {
"fractions": fraction_count,
},
"counts": {
"total": len(entries),
"strongly_bound": len(strongly_bound),
"weakly_bound": len(weakly_bound),
"unbound": len(unbound),
"skipped": skipped,
},
"fraction_counts": {
"wash_fractions": wash_fractions,
"weak_fractions": weak_fractions,
"strong_fractions": strong_fractions
},
"fractions": Fractionation.process_fractions(fractions)
}
2 changes: 2 additions & 0 deletions backend/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import backend.api.ion_exchange_fractionation_routes as ion_exchange_fractionation_routes
import backend.api.size_exclusion as size_exclusion_routes
import backend.api.hydrophobic_interaction_fractionation_routes as hydrophobic_interaction_fractionation_routes
import backend.api.affinity_chromatography_routes as affinity_chromatography_routes


"""
Expand Down Expand Up @@ -67,6 +68,7 @@
app.include_router(ion_exchange_fractionation_routes.router)
app.include_router(size_exclusion_routes.router)
app.include_router(hydrophobic_interaction_fractionation_routes.router)
app.include_router(affinity_chromatography_routes.router)

"""
NOTE FOR FUTURE DEVELOPERS:
Expand Down
7 changes: 7 additions & 0 deletions frontend/jbio-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import PeptideRetention from "./pages/PeptideRetention";
import IonExchangeFractionation from "./pages/IonExchangeFractionation";
import HydrophobicInteractionFractionation from "./pages/HydrophobicInteractionFractionation";
import SizeExclusionPage from './pages/SizeExclusion';
import AffinityChromatography from './pages/AffinityChromatography';

function App() {
const pages = [
Expand Down Expand Up @@ -76,6 +77,12 @@ function App() {
},
{
id: 8,
icon: <SwapVertIcon />,
component: <AffinityChromatography />,
label: "Affinity Chromatography",
},
{
id: 9,
icon: <GitHubIcon />,
link: "https://github.com/paulcraig/ProSep",
label: "GitHub Repository",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ export type ChromatogramProps = {
fractionRows: ChromatogramFractionDto[]
data: FractionationResponse
afterWash?: ProteinDto[]
saltGradient?: Array<{ x: number; y: number }>
elutionPoints?: Array<{ x: number; y: number }>
elutionLabel?: string,
elutionAxisLabel?: string
}

export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel = 'Wash', afterWashLabel = 'After Wash', fractionRows, data, afterWash, saltGradient }: ChromatogramProps) {
export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel = 'Wash', afterWashLabel = 'After Wash', fractionRows, data, afterWash, elutionPoints, elutionLabel = "Salt Gradient", elutionAxisLabel = "Salt Concentration"}: ChromatogramProps) {
const lineChartRef = useRef<ChartJS<'line'> | null>(null)
const scatterChartRef = useRef<ChartJS<'scatter'> | null>(null)
const chartTheme = useChartTheme()
Expand Down Expand Up @@ -47,6 +49,7 @@ export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel =
const maxY = Math.max(...points.map(p => p.y), 1)
for (const point of points) {
point.y = Math.round((point.y / maxY) * 100) / 100
if(point.y === 0) point.y = 0.001
}
return points
}, [data, fractionRows, showWash, showAfterWash, afterWash, lastFractionX])
Expand Down Expand Up @@ -90,10 +93,10 @@ export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel =
},
]

if (saltGradient && saltGradient.length > 0) {
if (elutionPoints && elutionPoints.length > 0) {
datasets.push({
label: 'Salt Gradient',
data: saltGradient,
label: elutionLabel,
data: elutionPoints,
borderColor: 'rgba(255, 107, 107, 1)',
backgroundColor: 'rgba(255, 107, 107, 1)',
pointRadius: 0,
Expand All @@ -105,13 +108,13 @@ export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel =
yAxisID: 'y1',
})

const saltGradientScale = {
const elutionScale = {
y1: {
type: 'linear',
position: 'right',
title: {
display: true,
text: 'Salt Concentration',
text: elutionAxisLabel,
color: chartTheme.text,
},
ticks: {
Expand All @@ -124,11 +127,11 @@ export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel =
},
} as const

return { datasets, saltGradientScale }
return { datasets, elutionScale }
}

return { datasets }
}, [proteinSeriesPoints, saltGradient])
}, [proteinSeriesPoints, elutionPoints, elutionLabel, elutionAxisLabel, chartTheme.text, chartTheme.grid])

const activeChart = showLineGraph ? lineChartRef.current : scatterChartRef.current
const downloadChart = useDownloadChart()
Expand Down Expand Up @@ -162,6 +165,7 @@ export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel =
text: 'Fractions',
color: chartTheme.text,
},
// TODO: this always shows zeroth (wash) fraction, either change how wash works or conditionally remove
beginAtZero: true,
ticks: {
callback: fractionTickLabel,
Expand All @@ -186,7 +190,7 @@ export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel =
color: chartTheme.grid,
},
},
...(lineData.saltGradientScale ?? {})
...(lineData.elutionScale ?? {})
},
}}
/>
Expand All @@ -213,6 +217,7 @@ export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel =
text: 'Fractions',
color: chartTheme.text,
},
// TODO: this always shows zeroth (wash) fraction, either change how wash works or conditionally remove
beginAtZero: true,
ticks: {
callback: fractionTickLabel,
Expand All @@ -237,7 +242,7 @@ export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel =
color: chartTheme.grid,
},
},
...(lineData.saltGradientScale ?? {})
...(lineData.elutionScale ?? {})
},
}}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import { Search } from '@mui/icons-material'
import { SortDirection } from './Common'

type ProteinTableProteinDto = Pick<ProteinDto, "id" | "name" | "description" | "sequence" | "molecularWeight" | "color">
export type ProteinTableProteinDto = Pick<ProteinDto, "id" | "name" | "description" | "sequence" | "molecularWeight" | "color">

export type ProteinTableMetricDto<P> = {
label: string
Expand Down Expand Up @@ -94,7 +94,7 @@
})

return rows
}, [filteredRetainedRows, proteinSort, metrics])

Check warning on line 97 in frontend/jbio-app/src/components/chromatography/tables/ProteinTable.tsx

View workflow job for this annotation

GitHub Actions / ESLint + Prettier

React Hook useMemo has a missing dependency: 'getSortValue'. Either include it or remove the dependency array

const pagedProteins = useMemo(() => {
const start = proteinPage * proteinRowsPerPage
Expand Down
Loading
Loading