mjbuehler's picture
Upload complete research archive under data/
de1266c verified
Raw History Blame Contribute Delete
8.34 kB
#!/usr/bin/env node
/**
* HFX batch runner: executes a plan of experiments in parallel worker threads, writes per-run outputs and
* appends one JSON line per run to experiment_log.jsonl (machine-readable log of EVERY simulated design).
* Usage: node hfx_batch.js <plan.js|plan.json> [--workers N] [--force]
* A plan module exports { batch, stage, base (shared params), designs: [{designId, params, seeds:[..], reason, hypothesis, movie?, snapshotFractions?}] }
*/
const fs = require('fs'), path = require('path'), os = require('os'), crypto = require('crypto');
const { Worker, isMainThread, parentPort } = require('worker_threads');
const CORE = path.join(__dirname, '..', 'app', 'src', 'hfx_core.js');
const RUNS = path.join(__dirname, 'runs');
const LOG = path.join(__dirname, 'experiment_log.jsonl');
function runOne(job) {
const HFX = require(CORE);
const t0 = Date.now();
const params = Object.assign(HFX.defaultParams(), job.base || {}, job.params || {}, { seed: job.seed, recordStates: !!job.movie, maxStates: job.movie ? 2000 : 0, recordMemberHistory: true, memberHistoryEvery: 5 });
let out = { runId: job.runId, batch: job.batch, stage: job.stage, designId: job.designId, seed: job.seed, reason: job.reason || '', hypothesis: job.hypothesis || '', params: params, started: new Date(t0).toISOString() };
try {
const structure = HFX.generateStructure(params);
const sim = new HFX.Simulation(structure, params);
sim.run();
const metrics = HFX.computeMetrics(sim);
const hash = HFX.hashResults(sim);
const dir = path.join(RUNS, job.runId); fs.mkdirSync(dir, { recursive: true });
const bundle = HFX.resultsBundle(sim, { runId: job.runId, batch: job.batch, stage: job.stage, designId: job.designId, reason: job.reason, hypothesis: job.hypothesis, hash: hash });
fs.writeFileSync(path.join(dir, 'results.json'), JSON.stringify(bundle));
fs.writeFileSync(path.join(dir, 'steps.csv'), HFX.stepsCSV(sim));
fs.writeFileSync(path.join(dir, 'events.csv'), HFX.eventsCSV(sim));
fs.writeFileSync(path.join(dir, 'members.csv'), HFX.membersCSV(sim));
fs.writeFileSync(path.join(dir, 'member_history.csv'), HFX.memberHistoryCSV(sim));
fs.writeFileSync(path.join(dir, 'geometry.json'), JSON.stringify(HFX.geometryJSON(structure)));
// snapshots for figures: deformed state at selected displacements, reconstructed exactly from the failure history
const snaps = {}; const m = sim.model;
const fr = job.snapshotFractions || [0.25, 0.5, 0.75, 1.0];
const dEnd = sim.steps[sim.steps.length - 1].d; const dFinal = sim.finalFailureD >= 0 ? sim.finalFailureD : dEnd;
const targets = { peak: sim.dPeak, final: dFinal, end: dEnd, first: Math.max(0, sim.firstFailureD) };
fr.forEach((f, i) => { targets['f' + i] = f * dFinal; });
const failDisp = sim.failDisp, damageDisp = sim.damageDisp;
for (const key of Object.keys(targets)) {
const d = targets[key];
const st = new Int8Array(structure.elements.length);
for (let i = 0; i < st.length; i++) { if (structure.elements[i].absent) st[i] = 2; else if (failDisp[i] >= 0 && failDisp[i] <= d * (1 + 1e-12)) st[i] = 2; else if (damageDisp[i] >= 0 && damageDisp[i] <= d * (1 + 1e-12)) st[i] = 1; }
if (key === 'peak' || key === 'first') for (let i = 0; i < st.length; i++) { if (!structure.elements[i].absent && failDisp[i] >= 0 && Math.abs(failDisp[i] - d) <= d * 1e-9) st[i] = 0; }
m.state.set(st); m.rebuild(); m.solveAt(d);
snaps[key] = { d: d, F: m.F, u: Array.from(m.u, v => Number(v.toPrecision(6))), util: Array.from(m.util, v => Number(v.toPrecision(5))), N: Array.from(m.N, v => Number(v.toPrecision(5))), state: Array.from(st) };
}
fs.writeFileSync(path.join(dir, 'snapshots.json'), JSON.stringify(snaps));
if (job.movie && sim.stateRecords) {
const recs = sim.stateRecords.map(r => ({ dFrom: r.dFrom, dTo: r.dTo === Infinity ? dEnd : r.dTo, F1: r.F1, nFailed: r.nFailed, u1: Array.from(r.u1, v => Number(v.toPrecision(6))), util1: Array.from(r.util1, v => Number(v.toPrecision(5))), N1: Array.from(r.N1, v => Number(v.toPrecision(5))), state: Array.from(r.state) }));
fs.writeFileSync(path.join(dir, 'state_records.json'), JSON.stringify({ dEnd: dEnd, records: recs }));
}
const files = {}; for (const f of fs.readdirSync(dir)) files[f] = { bytes: fs.statSync(path.join(dir, f)).size, sha256: crypto.createHash('sha256').update(fs.readFileSync(path.join(dir, f))).digest('hex') };
Object.assign(out, { status: 'ok', simStatus: sim.status, converged: sim.converged, hash: hash, metrics: metrics, structureStats: structure.stats, levelStats: structure.levelStats, warnings: structure.warnings, fractureMode: metrics.fractureLabel, timing: { totalMs: Date.now() - t0, simMs: sim.elapsedMs, factorMs: sim.model.factorTimeMs }, dir: path.relative(path.join(__dirname, '..'), dir), files: files });
} catch (err) {
Object.assign(out, { status: 'error', error: String(err && err.stack || err), timing: { totalMs: Date.now() - t0 } });
}
out.finished = new Date().toISOString();
return out;
}
if (!isMainThread) {
parentPort.on('message', (job) => { const res = runOne(job); parentPort.postMessage(res); });
} else {
const args = process.argv.slice(2);
const planFile = args.find(a => !a.startsWith('--'));
if (!planFile) { console.error('usage: node hfx_batch.js <plan> [--workers N]'); process.exit(1); }
const nWorkers = args.includes('--workers') ? parseInt(args[args.indexOf('--workers') + 1], 10) : Math.max(1, Math.min(12, os.cpus().length - 2));
const plan = require(path.resolve(planFile));
const jobs = [];
for (const d of plan.designs) for (const seed of d.seeds) {
const runId = plan.batch + '_' + d.designId + '_s' + seed;
jobs.push({ runId: runId, batch: plan.batch, stage: plan.stage, designId: d.designId, seed: seed, base: plan.base, params: d.params, reason: d.reason, hypothesis: d.hypothesis, movie: !!d.movie, snapshotFractions: d.snapshotFractions });
}
const done = new Set();
if (fs.existsSync(LOG)) for (const line of fs.readFileSync(LOG, 'utf8').split('\n')) { if (!line.trim()) continue; try { const r = JSON.parse(line); if (r.status === 'ok') done.add(r.runId); } catch (e) { } }
const todo = jobs.filter(j => !done.has(j.runId) || args.includes('--force'));
if (args.includes('--force') && fs.existsSync(LOG)) { // drop superseded entries so that every runId appears once in the log
const ids = new Set(todo.map(j => j.runId));
const kept = fs.readFileSync(LOG, 'utf8').split('\n').filter(l => { if (!l.trim()) return false; try { return !ids.has(JSON.parse(l).runId); } catch (e) { return false; } });
fs.writeFileSync(LOG, kept.join('\n') + (kept.length ? '\n' : ''));
}
console.log('plan', plan.batch, 'stage', plan.stage, ':', jobs.length, 'runs,', todo.length, 'to do, workers', nWorkers);
fs.mkdirSync(RUNS, { recursive: true });
const t0 = Date.now(); let next = 0, finished = 0; const results = [];
const summaryLine = (r) => r.status === 'ok' ? `${r.runId.padEnd(40)} ${r.fractureMode.padEnd(26)} K0=${r.metrics.K0.toExponential(3)} Fpk=${r.metrics.Fpeak.toExponential(3)} Wf=${r.metrics.Wf.toExponential(3)} nF=${String(r.metrics.nFailed).padStart(4)} A=${r.metrics.abruptness.toFixed(2)} LI=${(r.metrics.localizationIndex || 0).toFixed(2)} pre=${r.metrics.prePeakFailures} conv=${r.converged} ${(r.timing.totalMs / 1000).toFixed(1)}s` : `${r.runId} ERROR ${r.error.split('\n')[0]}`;
const dispatch = (w) => { if (next >= todo.length) { w.terminate(); return; } const job = todo[next++]; w.postMessage(job); };
for (let i = 0; i < Math.min(nWorkers, todo.length); i++) {
const w = new Worker(__filename);
w.on('message', (res) => {
finished++; results.push(res);
fs.appendFileSync(LOG, JSON.stringify(res) + '\n');
console.log(`[${finished}/${todo.length}] ` + summaryLine(res));
dispatch(w);
if (finished === todo.length) console.log('batch done in', ((Date.now() - t0) / 1000).toFixed(1), 's; errors:', results.filter(r => r.status !== 'ok').length, '; not converged:', results.filter(r => r.status === 'ok' && !r.converged).length);
});
w.on('error', (e) => { console.error('worker error', e); });
dispatch(w);
}
if (todo.length === 0) console.log('nothing to do');
}