1import type { MethodDefinition } from "./methods/types";
3/**
4 * Wall-time estimates for the two stages of a run. This is a calibration,
5 * not physics: the sizes (the plan's `work` for the modes stage, samples
6 * times modes for the strike) come from the method's plan function running in
7 * numbl; only the milliseconds-per-unit constants live here, seeded from the
8 * method definition and refined by each finished stage.
9 */
10export class CostModel {
11 private rates = new Map<string, number>();
12 private readonly overheadMs = 150;
14 private rate(method: MethodDefinition, stage: "modes" | "strike"): number {
15 return (
16 this.rates.get(`${method.id}:${stage}`) ??
17 (stage === "modes" ? method.cost.msPerWork : method.cost.msPerSampleMode)
18 );
19 }
21 modesMs(method: MethodDefinition, work: number): number {
22 return work * this.rate(method, "modes") + this.overheadMs;
23 }
25 strikeMs(method: MethodDefinition, samples: number, modes: number): number {
26 return samples * Math.max(modes, 1) * this.rate(method, "strike") + this.overheadMs;
27 }
29 /** Blend a measurement in, weighting recent runs more. */
30 observe(method: MethodDefinition, stage: "modes" | "strike", units: number, wallMs: number) {
31 if (units <= 0 || wallMs <= 0) return;
32 const measured = (wallMs - this.overheadMs) / units;
33 if (!Number.isFinite(measured) || measured <= 0) return;
34 this.rates.set(`${method.id}:${stage}`, 0.5 * this.rate(method, stage) + 0.5 * measured);
35 }
36}