doatlas-2 / artifacts /api-server /src /lib /result-packager.ts
Iostream-Li's picture
Add files using upload-large-folder tool
5871090 verified
/**
* result-packager — assemble result.zip for benchmark deliverables
* (Task #176, Wave A).
*
* Produces a zip with:
* - result.csv wide table (task_id, ligand_id, score) for
* DUD-E / LIT-PCBA virtual-screening benchmarks.
* - result.log human-readable log referencing the Blueprint id +
* per-task EF1% (or proxy) numbers.
* - manifest.json machine summary (network name, variant id,
* blueprint id, totals, evaluator hint).
*/
import { createWriteStream } from "node:fs";
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import archiver from "archiver";
export interface ResultRow {
taskId: string;
ligandId: string;
score: number;
}
export interface TaskSummary {
taskId: string;
numLigands: number;
topScore: number;
evaluatorMetric?: number | null;
notes?: string;
}
export interface PackageResultInput {
outDir: string;
/** Filename of the produced zip (without directory). */
zipName: string;
blueprintId: string;
networkName: string;
variantId: string;
rows: ResultRow[];
taskSummaries: TaskSummary[];
/** Aggregate proxy / true EF1% across all tasks if available. */
aggregateMetric?: number | null;
/** Free-form notes appended to result.log. */
extraLogLines?: string[];
}
function csvEscape(v: string | number): string {
const s = String(v);
if (/[",\n]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
return s;
}
export async function packageResult(
input: PackageResultInput,
): Promise<{ zipPath: string; csvBytes: number; logBytes: number }> {
await mkdir(input.outDir, { recursive: true });
const stage = path.join(input.outDir, `_stage_${input.blueprintId}`);
await mkdir(stage, { recursive: true });
// result.csv
const csvLines: string[] = ["task_id,ligand_id,score"];
for (const r of input.rows) {
csvLines.push(
[csvEscape(r.taskId), csvEscape(r.ligandId), r.score.toFixed(6)].join(","),
);
}
const csvBody = csvLines.join("\n") + "\n";
await writeFile(path.join(stage, "result.csv"), csvBody, "utf8");
// result.log
const logLines: string[] = [];
logLines.push(`# DoAtlas benchmark result`);
logLines.push(`blueprint_id: ${input.blueprintId}`);
logLines.push(`network: ${input.networkName}`);
logLines.push(`variant_id: ${input.variantId}`);
logLines.push(`generated_at: ${new Date().toISOString()}`);
logLines.push(`total_tasks: ${input.taskSummaries.length}`);
logLines.push(`total_rows: ${input.rows.length}`);
if (input.aggregateMetric != null) {
logLines.push(`aggregate_metric: ${input.aggregateMetric.toFixed(6)}`);
}
logLines.push(``);
logLines.push(`## per-task summary`);
for (const s of input.taskSummaries) {
logLines.push(
`- ${s.taskId} ligands=${s.numLigands} top=${s.topScore.toFixed(6)}` +
(s.evaluatorMetric != null
? ` metric=${s.evaluatorMetric.toFixed(6)}`
: "") +
(s.notes ? ` notes=${s.notes}` : ""),
);
}
if (input.extraLogLines && input.extraLogLines.length > 0) {
logLines.push(``);
logLines.push(`## notes`);
for (const ln of input.extraLogLines) logLines.push(ln);
}
const logBody = logLines.join("\n") + "\n";
await writeFile(path.join(stage, "result.log"), logBody, "utf8");
// manifest.json
const manifest = {
blueprintId: input.blueprintId,
networkName: input.networkName,
variantId: input.variantId,
generatedAt: new Date().toISOString(),
totals: {
tasks: input.taskSummaries.length,
rows: input.rows.length,
},
aggregateMetric: input.aggregateMetric ?? null,
perTask: input.taskSummaries,
};
await writeFile(
path.join(stage, "manifest.json"),
JSON.stringify(manifest, null, 2),
"utf8",
);
// zip
const zipPath = path.join(input.outDir, input.zipName);
await new Promise<void>((resolve, reject) => {
const out = createWriteStream(zipPath);
const arch = archiver("zip", { zlib: { level: 9 } });
out.on("close", () => resolve());
out.on("error", reject);
arch.on("error", reject);
arch.pipe(out);
arch.file(path.join(stage, "result.csv"), { name: "result.csv" });
arch.file(path.join(stage, "result.log"), { name: "result.log" });
arch.file(path.join(stage, "manifest.json"), { name: "manifest.json" });
void arch.finalize();
});
return {
zipPath,
csvBytes: Buffer.byteLength(csvBody, "utf8"),
logBytes: Buffer.byteLength(logBody, "utf8"),
};
}