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 }
  }
}