hydropd / website /src /pages /DetectabilityTraining.tsx
rikardsaqe's picture
Pfly via feature set, condition Other, Data dropdowns
367367f verified
Raw
History Blame Contribute Delete
10.8 kB
import { useState } from 'react'
import Card from '../components/Card'
import Button from '../components/Button'
import Field from '../components/Field'
import SequenceInput from '../components/SequenceInput'
import TaxIdInput from '../components/TaxIdInput'
import {
CLASSIFIER_OPTIONS,
DEFAULT_TRAINING_CONFIG,
FEATURE_SET_OPTIONS,
SPLIT_OPTIONS,
SAMPLE_PEPTIDES,
type TrainingConfig,
type TrainingResult,
} from '../data/mockData'
import { trainDetectability } from '../lib/api'
export default function DetectabilityTraining() {
const [input, setInput] = useState('')
const [taxId, setTaxId] = useState('')
const [useDefaults, setUseDefaults] = useState(true)
const [cfg, setCfg] = useState<TrainingConfig>(DEFAULT_TRAINING_CONFIG)
const [training, setTraining] = useState(false)
const [result, setResult] = useState<TrainingResult | null>(null)
const [live, setLive] = useState(false)
const [downloadUrl, setDownloadUrl] = useState<string | null>(null)
const [dataUrl, setDataUrl] = useState<string | null>(null)
const [errorMsg, setErrorMsg] = useState<string | null>(null)
const canRun = input.trim().length > 0 && taxId.length > 0 && !training
function set<K extends keyof TrainingConfig>(k: K, v: TrainingConfig[K]) {
setUseDefaults(false)
setCfg((c) => ({ ...c, [k]: v }))
}
// Pfly is a coupled choice: picking it in either the classifier or the feature-set
// dropdown keeps the other in sync (and de-selecting it restores the default).
function setClassifier(v: string) {
setUseDefaults(false)
setCfg((c) => ({
...c,
classifier: v,
featureSet:
v === 'pfly'
? 'pfly'
: c.featureSet === 'pfly'
? DEFAULT_TRAINING_CONFIG.featureSet
: c.featureSet,
}))
}
function setFeatureSet(v: string) {
setUseDefaults(false)
setCfg((c) => ({
...c,
featureSet: v,
classifier:
v === 'pfly'
? 'pfly'
: c.classifier === 'pfly'
? DEFAULT_TRAINING_CONFIG.classifier
: c.classifier,
}))
}
function toggleDefaults(on: boolean) {
setUseDefaults(on)
if (on) setCfg(DEFAULT_TRAINING_CONFIG)
}
async function train() {
setTraining(true)
setResult(null)
setErrorMsg(null)
setDownloadUrl(null)
setDataUrl(null)
try {
const out = await trainDetectability(input, taxId, cfg)
setResult(out.result)
setLive(out.live)
setDownloadUrl(out.downloadUrl)
setDataUrl(out.dataUrl)
setErrorMsg(out.error ?? null)
} finally {
setTraining(false)
}
}
return (
<main className="page">
<div className="page-header">
<h1>Train a Detectability Model</h1>
<p className="lead">
Give detected peptides and a species tax ID, pick your specs or use the
defaults, and train a model. Then download the model and processed data.
</p>
</div>
<div className="grid-2">
<Card title="Training data">
<SequenceInput
label="Detected peptide sequences (positives)"
value={input}
onChange={setInput}
onLoadExample={() => setInput(SAMPLE_PEPTIDES)}
/>
<TaxIdInput value={taxId} onChange={setTaxId} />
</Card>
<Card
title="Model specifications"
actions={
<label
className="option"
style={{ padding: '6px 10px', marginBottom: 0 }}
>
<input
type="checkbox"
checked={useDefaults}
onChange={(e) => toggleDefaults(e.target.checked)}
/>
<span className="option-title" style={{ fontSize: 'var(--text-sm)' }}>
Use defaults
</span>
</label>
}
>
<Field label="Classifier">
<select
value={cfg.classifier}
onChange={(e) => setClassifier(e.target.value)}
>
{CLASSIFIER_OPTIONS.map((o) => (
<option key={o.id} value={o.id}>
{o.label} · {o.desc}
</option>
))}
</select>
</Field>
<Field
label="Feature set"
hint={
cfg.featureSet === 'pfly'
? 'Pfly reads the sequence directly, no separate classifier'
: undefined
}
>
<select
value={cfg.featureSet}
onChange={(e) => setFeatureSet(e.target.value)}
>
{FEATURE_SET_OPTIONS.map((o) => (
<option key={o.id} value={o.id}>
{o.label} · {o.desc}
</option>
))}
</select>
</Field>
<Field label="Split (cross-validation grouping)">
<select
value={cfg.split}
onChange={(e) => set('split', e.target.value)}
>
{SPLIT_OPTIONS.map((o) => (
<option key={o.id} value={o.id}>
{o.label} · {o.desc}
</option>
))}
</select>
</Field>
<div className="grid-2" style={{ gap: 14 }}>
<Field label="CV folds">
<input
type="number"
min={2}
max={10}
value={cfg.folds}
onChange={(e) => set('folds', Number(e.target.value))}
/>
</Field>
<Field label="Negative balance">
<select
value={cfg.balance}
onChange={(e) => set('balance', e.target.value)}
>
{['1:1', '1:3', '1:5'].map((b) => (
<option key={b} value={b}>
{b}
</option>
))}
</select>
</Field>
</div>
<div style={{ marginTop: 8 }}>
<Button onClick={train} disabled={!canRun}>
{training ? 'Training…' : 'Train model'}
</Button>
{!canRun && !training && (
<p
className="muted"
style={{ fontSize: 'var(--text-sm)', marginTop: 8, marginBottom: 0 }}
>
Add positive peptides and a species tax ID to train.
</p>
)}
</div>
</Card>
</div>
<Card title="Training result">
{training ? (
<div className="empty-state">
Training model… computing features, sampling k-mer negatives, and
running cross-validation.
</div>
) : result == null ? (
<div className="empty-state">
Train a model to see cross-validation metrics and per-fold results.
</div>
) : (
<>
<p style={{ marginTop: 0, marginBottom: 12 }}>
<span className={'badge ' + (live ? 'badge-good' : 'badge-muted')}>
{live ? 'Live training' : 'Offline preview'}
</span>{' '}
<span className="muted" style={{ fontSize: 'var(--text-sm)' }}>
{live
? 'Trained on the server: assign proteins, sample negatives, protein-grouped CV.'
: 'Illustrative numbers; the training backend is not reachable.'}
</span>
</p>
{errorMsg && (
<p className="error-banner" style={{ marginTop: 0, marginBottom: 12 }}>
Backend could not train on this input: {errorMsg}
</p>
)}
<div className="stat-row">
<div className="stat">
<div className="stat-value">{result.rocAuc.toFixed(3)}</div>
<div className="stat-label">
Mean ROC-AUC ({cfg.folds}-fold CV)
</div>
</div>
<div className="stat">
<div className="stat-value">{result.prAuc.toFixed(3)}</div>
<div className="stat-label">Mean PR-AUC</div>
</div>
<div className="stat">
<div className="stat-value">{result.nPositives}</div>
<div className="stat-label">Positives</div>
</div>
<div className="stat">
<div className="stat-value">{result.nNegatives}</div>
<div className="stat-label">Negatives (sampled)</div>
</div>
</div>
<div className="table-wrap">
<table className="table">
<thead>
<tr>
<th>Fold</th>
<th className="num">ROC-AUC</th>
<th className="num">PR-AUC</th>
<th className="num"># train</th>
<th className="num"># test</th>
</tr>
</thead>
<tbody>
{result.folds.map((f) => (
<tr key={f.fold}>
<td>Fold {f.fold}</td>
<td className="num">{f.rocAuc.toFixed(3)}</td>
<td className="num">{f.prAuc.toFixed(3)}</td>
<td className="num">{f.nTrain}</td>
<td className="num">{f.nTest}</td>
</tr>
))}
</tbody>
</table>
</div>
<div style={{ marginTop: 16, display: 'flex', gap: 10, flexWrap: 'wrap' }}>
{downloadUrl ? (
<a href={downloadUrl} download>
<Button variant="secondary">Download trained model (.joblib)</Button>
</a>
) : (
<Button variant="secondary" disabled>
Download trained model (train on the backend to enable)
</Button>
)}
{dataUrl && (
<a href={dataUrl} download>
<Button variant="secondary">Download processed data (.csv)</Button>
</a>
)}
</div>
{dataUrl && (
<p className="muted" style={{ fontSize: 'var(--text-xs)', marginTop: 8, marginBottom: 0 }}>
The processed dataset has one row per peptide (your detected peptides + sampled
negatives) with its assigned protein, bioactivity / cytotoxicity / allergen screening,
and the full feature matrix (physicochemical + PepBERT + ESM2 embeddings).
</p>
)}
</>
)}
</Card>
</main>
)
}