Add design result analysis shortcut

This commit is contained in:
Lihatoo 2026-05-28 01:15:39 +08:00
parent e1b3156754
commit 11dc85d7ad

View file

@ -2213,6 +2213,10 @@ A+B</textarea>
design_target_target: "目标浓度",
design_tube_defect: "Tube 缺陷",
design_complex_defect: "Complex 缺陷",
design_analyze_direct: "直接分析设计结果",
design_load_analysis: "载入为 Analysis 输入",
design_analysis_loaded: "已载入设计序列,可修改链浓度后分析",
design_analysis_unavailable: "没有可分析的设计结果",
details_json: "展开原始 JSON",
toggle_structure_data: "结构数据",
toggle_pairs_panel: "配对热图",
@ -2517,6 +2521,10 @@ A+B</textarea>
design_target_target: "Target Concentration",
design_tube_defect: "Tube Defect",
design_complex_defect: "Complex Defect",
design_analyze_direct: "Analyze Designed Result",
design_load_analysis: "Load as Analysis Input",
design_analysis_loaded: "Designed sequences loaded; adjust strand concentrations before analysis",
design_analysis_unavailable: "No designed result is available for analysis",
details_json: "Show raw JSON",
toggle_structure_data: "Structure Data",
toggle_pairs_panel: "Pair Heatmap",
@ -3747,6 +3755,81 @@ A+B</textarea>
};
}
function buildAnalysisOptionsFromControls() {
return {
num_sample: Number(document.getElementById("numSample").value),
energy_gap: Number(document.getElementById("energyGap").value),
sparsity_fraction: Number(document.getElementById("sparsityFraction").value),
sparsity_threshold: Number(document.getElementById("sparsityThreshold").value),
single_mfe: false,
result_limit: Number(document.getElementById("resultLimit").value),
pairs_preview_size: Number(document.getElementById("pairsPreviewSize").value),
};
}
function buildDesignedAnalysisPayload(result) {
if (!result || workflowKind(result) !== "design" || !Array.isArray(result.strands) || !result.strands.length) {
throw new Error(t("design_analysis_unavailable"));
}
const inputStrandsByName = new Map((lastPayload?.strands || []).map((strand) => [strand.name, strand]));
const strands = result.strands.map((strand) => {
const input = inputStrandsByName.get(strand.name) || {};
return {
name: strand.name,
sequence: strand.sequence,
concentration: Number(input.concentration ?? 1.0),
unit: input.unit || "uM",
};
});
const targetLines = (result.design?.targets || result.complexes || [])
.map((target) => Array.isArray(target.strand_names) ? target.strand_names.join("+") : "")
.filter(Boolean);
return {
workflow: "analysis",
mode: result.mode || "tube",
model: { ...result.model },
compute: ["pfunc", "mfe", "pairs"],
options: buildAnalysisOptionsFromControls(),
strands,
tube: {
name: result.tube?.name || result.design?.target_tubes?.[0]?.name || "designed_tube",
max_size: Number(result.tube?.max_size ?? result.design?.target_tubes?.[0]?.max_size ?? 2),
},
complexes_text: targetLines.join("\n"),
design_domains: [],
design_complexes: [],
design_tubes: [],
design: {},
hard_constraints: [],
soft_constraints: [],
defect_weights: [],
};
}
function loadDesignedAnalysisInput(result) {
const payload = buildDesignedAnalysisPayload(result);
workflowSelect.value = "analysis";
modeSelect.value = payload.mode;
document.getElementById("material").value = payload.model.material;
document.getElementById("ensemble").value = payload.model.ensemble;
document.getElementById("celsius").value = payload.model.celsius;
document.getElementById("sodium").value = payload.model.sodium;
document.getElementById("magnesium").value = payload.model.magnesium;
document.getElementById("tubeName").value = payload.tube.name;
document.getElementById("maxSize").value = payload.tube.max_size;
document.querySelectorAll(".check input").forEach((input) => {
input.checked = payload.compute.includes(input.value);
});
strandList.innerHTML = "";
payload.strands.forEach((strand) => addStrandRow(strand));
syncComplexRowsFromText(payload.complexes_text);
syncWorkflow();
syncMode();
document.querySelector(".main-grid")?.scrollIntoView({ behavior: "smooth", block: "start" });
setStatus(formatStatus(t("design_analysis_loaded")));
return payload;
}
function renderModelCard(result) {
return `
<div class="result-card">
@ -3864,6 +3947,10 @@ A+B</textarea>
<div><strong>${t("design_seed")}</strong><span class="metric-value">${stats.seed ?? "-"}</span></div>
<div><strong>${t("summary_elapsed")}</strong><span class="metric-value">${stats.design_time ? `${Number(stats.design_time).toFixed(3)} s` : "-"}</span></div>
</div>
<div class="compact-actions" style="margin-top: 12px;">
<button class="secondary" id="loadDesignedAnalysisBtn" type="button">${t("design_load_analysis")}</button>
<button id="runDesignedAnalysisBtn" type="button">${t("design_analyze_direct")}</button>
</div>
</div>
`;
}
@ -5191,6 +5278,14 @@ A+B</textarea>
document.getElementById("exportResultBtn")?.addEventListener("click", () => {
if (lastResult) downloadText("nupack-design-result.json", JSON.stringify(lastResult, null, 2));
});
document.getElementById("loadDesignedAnalysisBtn")?.addEventListener("click", () => {
try {
loadDesignedAnalysisInput(result);
} catch (error) {
setStatus(`${t("status_error")}: ${error.message}`, true);
}
});
document.getElementById("runDesignedAnalysisBtn")?.addEventListener("click", runDesignedAnalysis);
return;
}
result.complexes.forEach((complex, index) => {
@ -5302,6 +5397,55 @@ A+B</textarea>
}
}
async function runDesignedAnalysis() {
if (activeSubmission) {
setStatus(formatStatus(t("submission_locked")));
return;
}
let payload;
try {
payload = buildDesignedAnalysisPayload(lastResult);
const validationError = validatePayload(payload);
if (validationError) throw new Error(validationError);
} catch (error) {
setStatus(`${t("status_error")}: ${error.message}`, true);
return;
}
setSubmissionLocked(true);
currentJobId = null;
setStatus(formatStatus(t("status_running")));
results.innerHTML = "";
try {
refreshHealth();
const { response, data } = await fetchJsonOrThrow("/api/jobs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok || data.status !== "accepted") {
throw new Error(data?.error || `Analysis failed with HTTP ${response.status}`);
}
currentJobId = data.job_id;
setStatus(formatStatus(t("status_waiting_job")));
const result = await waitForJob(data.job_id);
result.job_id = data.job_id;
lastPayload = payload;
lastResult = result;
renderResult(result);
saveHistory(payload, result);
renderHistory();
setStatus(formatStatus(t("status_done")));
} catch (error) {
results.innerHTML = `<div class="result-card"><h3>${t("request_failed")}</h3><pre>${String(error)}</pre></div>`;
setStatus(t("status_error"), true);
refreshHealth();
} finally {
setSubmissionLocked(false);
}
}
async function runAnalysis() {
if (activeSubmission) {
setStatus(formatStatus(t("submission_locked")));