All checks were successful
Deploy to Pages / build (push) Successful in 26s
137 lines
6 KiB
JavaScript
137 lines
6 KiB
JavaScript
import { readFile, writeFile } from 'node:fs/promises';
|
|
import { createServer } from 'vite';
|
|
|
|
const server = await createServer({
|
|
server: { middlewareMode: true },
|
|
appType: 'custom',
|
|
optimizeDeps: { noDiscovery: true, include: [] },
|
|
});
|
|
try {
|
|
const [{ planFlooring }, { planSkirting }, { plannerCases }, metrics] = await Promise.all([
|
|
server.ssrLoadModule('/src/lib/flooring.ts'),
|
|
server.ssrLoadModule('/src/lib/skirting.ts'),
|
|
server.ssrLoadModule('/benchmarks/cases.ts'),
|
|
server.ssrLoadModule('/benchmarks/metrics.ts'),
|
|
]);
|
|
const cases = plannerCases();
|
|
const random = process.argv.includes('--random');
|
|
if (random) cases.forEach(({ project }) => (project.flooring.optimisation = 'random'));
|
|
const results = [];
|
|
// Warm up the geometry and planners before taking timings.
|
|
planFlooring(cases[0].project.rooms, cases[0].project.flooring, cases[0].project.tolerance);
|
|
for (const { name, project: p } of cases) {
|
|
const start = performance.now();
|
|
const flooring = planFlooring(p.rooms, p.flooring, p.tolerance);
|
|
const floorMs = performance.now() - start;
|
|
const skirtStart = performance.now();
|
|
const skirting = planSkirting(p.rooms, p.skirting, p.tolerance);
|
|
results.push({
|
|
name,
|
|
flooring: metrics.flooringMetrics(flooring, p.flooring),
|
|
skirting: metrics.skirtingMetrics(skirting),
|
|
floorMs: Math.round(floorMs * 10) / 10,
|
|
skirtMs: Math.round((performance.now() - skirtStart) * 10) / 10,
|
|
});
|
|
}
|
|
const baselinePath = new URL('../benchmarks/baseline.json', import.meta.url);
|
|
if (process.argv.includes('--write-baseline')) {
|
|
await writeFile(baselinePath, JSON.stringify(results, null, 2) + '\n');
|
|
console.log(`Recorded ${results.length} baseline cases.`);
|
|
} else {
|
|
const comparison = process.argv
|
|
.find((arg) => arg.startsWith('--compare='))
|
|
?.slice('--compare='.length);
|
|
const baseline = JSON.parse(await readFile(comparison ?? baselinePath, 'utf8'));
|
|
const currentAppearance = baseline.every((row) => typeof row.flooring.nearH === 'number');
|
|
console.log(
|
|
currentAppearance
|
|
? 'Comparisons include the broad H-alignment penalty.'
|
|
: 'Historical comparisons use the original narrow repetition score; broad H counts follow separately.',
|
|
);
|
|
const sum = (rows, key, metric) => rows.reduce((total, row) => total + row[key][metric], 0);
|
|
const summary = [];
|
|
for (const kind of ['flooring', 'skirting']) {
|
|
const compare =
|
|
kind === 'flooring'
|
|
? (a, b) =>
|
|
(currentAppearance ? metrics.compareCurrentFlooring : metrics.compareFlooring)(
|
|
a,
|
|
b,
|
|
random,
|
|
)
|
|
: metrics.compareSkirting;
|
|
const changes = results.map((result) => {
|
|
const previous = baseline.find((row) => row.name === result.name);
|
|
if (!previous) throw new Error(`No baseline for ${result.name}`);
|
|
return { name: result.name, change: compare(result[kind], previous[kind]) };
|
|
});
|
|
summary.push({
|
|
planner: kind,
|
|
improved: changes.filter((row) => row.change < 0).length,
|
|
tied: changes.filter((row) => row.change === 0).length,
|
|
regressed: changes.filter((row) => row.change > 0).length,
|
|
'cuts before': sum(baseline, kind, 'cuts'),
|
|
'cuts after': sum(results, kind, 'cuts'),
|
|
'boards before': sum(baseline, kind, 'boards'),
|
|
'boards after': sum(results, kind, 'boards'),
|
|
errors: sum(results, kind, 'errors'),
|
|
});
|
|
const regressions = changes.filter((row) => row.change > 0).map((row) => row.name);
|
|
if (regressions.length) console.log(`${kind} regressions: ${regressions.join(', ')}`);
|
|
}
|
|
console.table(summary);
|
|
for (const kind of ['flooring', 'skirting']) {
|
|
const wastePercent = (rows) => {
|
|
const purchased = rows.reduce((total, row) => {
|
|
const { project } = cases.find((c) => c.name === row.name);
|
|
const size =
|
|
kind === 'flooring'
|
|
? project.flooring.length * project.flooring.width
|
|
: project.skirting.stockLength;
|
|
return total + row[kind].boards * size;
|
|
}, 0);
|
|
return ((sum(rows, kind, 'waste') / purchased) * 100).toFixed(2);
|
|
};
|
|
console.log(
|
|
`${kind} waste: ${wastePercent(baseline)}% → ${wastePercent(results)}% (weighted by purchased stock)`,
|
|
);
|
|
}
|
|
console.table(
|
|
['shapingCuts', 'ripCuts', 'patternPenalty', 'repeats', 'similarSteps', 'staircases'].map(
|
|
(metric) => ({
|
|
metric,
|
|
before: sum(baseline, 'flooring', metric),
|
|
after: sum(results, 'flooring', metric),
|
|
}),
|
|
),
|
|
);
|
|
const hBaseline = JSON.parse(
|
|
await readFile(new URL('../benchmarks/h-pattern-baseline.json', import.meta.url), 'utf8'),
|
|
);
|
|
const mode = random ? 'random' : 'cuts';
|
|
const hBefore = hBaseline.reduce((total, row) => total + row[mode].nearH, 0);
|
|
const hChanges = results.map((row) =>
|
|
metrics.compareCurrentFlooring(
|
|
row.flooring,
|
|
hBaseline.find((previous) => previous.name === row.name)[mode],
|
|
random,
|
|
),
|
|
);
|
|
console.log(
|
|
`Against the pre-H-fix layout: near-H row pairs ${hBefore} → ${sum(results, 'flooring', 'nearH')}; ` +
|
|
`${hChanges.filter((change) => change < 0).length} improved, ` +
|
|
`${hChanges.filter((change) => change === 0).length} tied, ` +
|
|
`${hChanges.filter((change) => change > 0).length} regressed under the current objective.`,
|
|
);
|
|
}
|
|
for (const key of ['floorMs', 'skirtMs']) {
|
|
const times = results.map((row) => row[key]).sort((a, b) => a - b);
|
|
console.log(
|
|
`${key}: total ${times.reduce((a, b) => a + b, 0).toFixed(1)} ms; p95 ${times[Math.floor(times.length * 0.95)]} ms; max ${times.at(-1)} ms`,
|
|
);
|
|
}
|
|
const output = process.argv.find((arg) => arg.startsWith('--output='))?.slice('--output='.length);
|
|
if (output) await writeFile(output, JSON.stringify(results, null, 2) + '\n');
|
|
} finally {
|
|
await server.close();
|
|
}
|