Spaces:
Running
Running
File size: 10,431 Bytes
27124e1 ec91db7 27124e1 ec91db7 27124e1 896bd6a 27124e1 896bd6a 27124e1 ec91db7 8c51b20 ec91db7 27124e1 600a160 27124e1 600a160 27124e1 8c51b20 27124e1 600a160 27124e1 600a160 27124e1 2273160 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 | // 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 }
}
}
|