hydropd / website /src /lib /api.ts
rikardsaqe's picture
Upload folder using huggingface_hub
2273160 verified
Raw
History Blame Contribute Delete
10.4 kB
// Backend client for Milestone 2 real inference. Falls back to the labeled
// client-side mock (mockData.runDetectability) when the backend is unreachable,
// so the static site still works offline / in a pure-demo deployment.
import { parseSequences } from './parseSequences'
import {
runDetectability,
runTraining,
runBioactivityPeptides,
runAllergenOrigin,
type DetectabilityRow,
type BioactivityRow,
type AllergenRow,
type TrainingConfig,
type TrainingResult,
} from '../data/mockData'
import type { RealModel } from '../data/models'
const API_BASE = import.meta.env.VITE_API_BASE ?? '/api'
export interface PredictOutcome {
rows: DetectabilityRow[]
live: boolean // true = real backend inference; false = offline mock fallback
calibrated: boolean // backend-reported; only meaningful when live
}
interface PredictResponse {
model: string
calibrated: boolean
results: {
peptide: string
length: number
score: number
prediction: string
inTraining?: 'positive' | 'negative' | null
}[]
flags: string[]
}
/**
* Score peptides with a model. Tries the backend first; on any network/HTTP
* error returns the deterministic mock so the UI degrades gracefully.
*/
export async function predictDetectability(
input: string,
model: RealModel,
): Promise<PredictOutcome> {
const peptides = parseSequences(input).map((p) => p.seq)
const label = `${model.code} · ${model.species}`
try {
const res = await fetch(`${API_BASE}/predict`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ peptides, modelCode: model.code }),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data: PredictResponse = await res.json()
const rows: DetectabilityRow[] = data.results.map((r) => ({
peptide: r.peptide,
length: r.length,
score: r.score,
prediction: r.prediction === 'Detectable' ? 'Detectable' : 'Not detectable',
model: label,
inTraining: r.inTraining ?? null,
}))
return { rows, live: true, calibrated: data.calibrated }
} catch {
// Backend down / not deployed → labeled illustrative mock.
return { rows: runDetectability(input, model), live: false, calibrated: false }
}
}
// ---------------------------------------------------------------- bioactivity screening
export interface BioactivityOutcome {
rows: BioactivityRow[]
live: boolean
}
export interface AllergenOutcome {
rows: AllergenRow[]
live: boolean
}
interface BioRespPeptide {
peptide: string
bioactivities: string
sources: string
ige_epitope: string
}
interface BioRespProtein {
protein: string
is_recognized_allergen: string
allergen_name: string
organism: string
source: string
}
/** Screen peptides for bioactivity/cytotoxicity + IgE-epitope retention (Q2). */
export async function screenBioactivityPeptides(input: string): Promise<BioactivityOutcome> {
const sequences = parseSequences(input).map((p) => p.seq)
try {
const res = await fetch(`${API_BASE}/bioactivity`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode: 'peptides', sequences }),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data: BioRespPeptide[] = await res.json()
const rows: BioactivityRow[] = data.map((r) => ({
peptide: r.peptide,
bioactivities: r.bioactivities,
sources: r.sources,
igeEpitope: r.ige_epitope === 'yes',
}))
return { rows, live: true }
} catch {
return { rows: runBioactivityPeptides(input), live: false }
}
}
/** Screen proteins for allergen origin, recognized-allergen protein match (Q1). */
export async function screenAllergenOrigin(input: string): Promise<AllergenOutcome> {
const sequences = parseSequences(input).map((p) => p.seq)
try {
const res = await fetch(`${API_BASE}/bioactivity`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode: 'proteins', sequences }),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data: BioRespProtein[] = await res.json()
const rows: AllergenRow[] = data.map((r) => ({
protein: r.protein,
isAllergen: r.is_recognized_allergen === 'yes',
allergenName: r.allergen_name,
organism: r.organism,
source: r.source,
}))
return { rows, live: true }
} catch {
return { rows: runAllergenOrigin(input), live: false }
}
}
export interface TrainOutcome {
result: TrainingResult
live: boolean // true = real backend training; false = offline mock fallback
downloadUrl: string | null // GET to fetch the trained joblib (live only)
dataUrl: string | null // GET to fetch the processed dataset CSV (live only)
species?: string
error?: string // set when the backend rejected the request (e.g. unsupported tax id)
}
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
/**
* Train a detectability model from a peptide list. Submits an async job, polls to
* completion, and maps to TrainingResult. On any network/HTTP error falls back to the
* deterministic client-side mock so the static demo still works offline.
*/
export async function trainDetectability(
input: string,
taxId: string,
cfg: TrainingConfig,
): Promise<TrainOutcome> {
const peptides = parseSequences(input).map((p) => p.seq)
try {
const res = await fetch(`${API_BASE}/train`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ peptides, taxId, config: cfg }),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const { jobId } = (await res.json()) as { jobId: string }
// poll (cap ~2 min at 1.5 s intervals)
for (let i = 0; i < 80; i++) {
await sleep(1500)
const s = await fetch(`${API_BASE}/train/${jobId}`)
if (!s.ok) throw new Error(`HTTP ${s.status}`)
const job = await s.json()
if (job.status === 'done') {
return {
result: job.result as TrainingResult,
live: true,
downloadUrl: `${API_BASE}/train/${jobId}/model`,
dataUrl: `${API_BASE}/train/${jobId}/data`,
species: job.result.species,
}
}
if (job.status === 'error') {
// A rejected request (bad tax id, unmapped peptides) is a real answer, not a
// reason to silently show fake numbers, surface it with the mock as preview.
return {
result: runTraining(input, taxId + JSON.stringify(cfg)),
live: false,
downloadUrl: null,
dataUrl: null,
error: job.error as string,
}
}
}
throw new Error('training timed out')
} catch {
// Backend down / not deployed → labeled illustrative mock.
return {
result: runTraining(input, taxId + JSON.stringify(cfg)),
live: false,
downloadUrl: null,
dataUrl: null,
}
}
}
// ---------------------------------------------------------------- protein digestibility
export interface Enzyme {
key: string
label: string
}
// Fallback enzyme list so the page still renders if the backend is unreachable (the real
// list comes from GET /api/enzymes, sourced from hydropd.api.available_enzymes()).
const FALLBACK_ENZYMES: Enzyme[] = [
{ key: 'trypsin', label: 'Trypsin' },
{ key: 'chymotrypsin', label: 'Chymotrypsin' },
{ key: 'pepsin_ph1.3', label: 'Pepsin pH1.3' },
{ key: 'pepsin_ph>2', label: 'Pepsin pH >2' },
{ key: 'subtilisin', label: 'Subtilisin' },
{ key: 'papain', label: 'Papain' },
{ key: 'thermolysin', label: 'Thermolysin' },
{ key: 'proteinase_k', label: 'Proteinase K' },
]
export async function fetchEnzymes(): Promise<Enzyme[]> {
try {
const res = await fetch(`${API_BASE}/enzymes`)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return (await res.json()) as Enzyme[]
} catch {
return FALLBACK_ENZYMES
}
}
export interface DigestSummaryRow {
enzyme: string
enzyme_key: string
candidate_set: number
functional_set: number
detected_functional_set?: number
combined_all_models?: number
}
export interface DigestTables {
candidate: Record<string, string | number>[]
candidate_total: number
functional: Record<string, string | number>[]
functional_total: number
detected: Record<string, string | number>[]
detected_total: number
det_cols: string[]
}
export interface DigestResult {
summary: DigestSummaryRow[]
summaryColumns: string[]
models: string[]
calibrated: Record<string, boolean>
multi: boolean
threshold: number
display: Record<string, DigestTables>
nProteins: number
zipName: string
}
export interface DigestOutcome {
result: DigestResult | null
live: boolean
zipUrl: string | null
error?: string
}
/**
* Run a protein-digestibility screen. Submits an async job, polls to completion (heavy: digestion
* + functionality lookup + per-model detectability), and returns the ranked summary + a ZIP URL.
* On any network/HTTP error returns a null result flagged offline so the page degrades gracefully.
*/
export async function computeDigestibility(
proteins: string,
enzymes: string[],
models: string[],
threshold: number,
): Promise<DigestOutcome> {
try {
const res = await fetch(`${API_BASE}/digestibility`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ proteins, enzymes, models, threshold }),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const { jobId } = (await res.json()) as { jobId: string }
// poll up to ~10 min at 2.5 s intervals (a full all-enzyme, multi-model run is slow)
for (let i = 0; i < 240; i++) {
await sleep(2500)
const s = await fetch(`${API_BASE}/digestibility/${jobId}`)
if (!s.ok) throw new Error(`HTTP ${s.status}`)
const job = await s.json()
if (job.status === 'done') {
return {
result: job.result as DigestResult,
live: true,
zipUrl: `${API_BASE}/digestibility/${jobId}/zip`,
}
}
if (job.status === 'error') {
return { result: null, live: false, zipUrl: null, error: job.error as string }
}
}
throw new Error('digestibility timed out')
} catch (e) {
return { result: null, live: false, zipUrl: null, error: (e as Error).message }
}
}