diff --git a/src/benchmark/convergence/coefficientOfVariance.ts b/src/benchmark/convergence/coefficientOfVariance.ts new file mode 100644 index 0000000..fdf0cf4 --- /dev/null +++ b/src/benchmark/convergence/coefficientOfVariance.ts @@ -0,0 +1,73 @@ +import Debug from "debug"; +import {BenchmarkOpts, ConvergenceCheckFn} from "../../types.js"; +import {calcMean, calcMedian, calcVariance, filterOutliers, OutlierSensitivity, sortData} from "../../utils/math.js"; + +const debug = Debug("@chainsafe/benchmark/convergence"); + +export function createCVConvergenceCriteria( + startMs: number, + {maxMs, maxRuns, minRuns, minMs, convergeFactor}: Required +): ConvergenceCheckFn { + let lastConvergenceSample = startMs; + let sampleEveryMs = Math.min(100, minMs); + const minSamples = Math.max(5, minRuns); + const maxSamplesForCV = 1000; + + return function canTerminate(runIdx: number, totalNs: bigint, runsNs: bigint[]): boolean { + const currentMs = Date.now(); + const elapsedMs = currentMs - startMs; + const timeSinceLastCheck = currentMs - lastConvergenceSample; + const mustStop = elapsedMs >= maxMs || runIdx >= maxRuns; + const mayStop = elapsedMs >= minMs && runIdx >= minRuns && runIdx >= minSamples; + + debug( + "trying to converge benchmark via cv mustStop=%o, mayStop=%o, timeSinceLastCheck=%o", + mustStop, + mayStop, + timeSinceLastCheck + ); + + // Must stop + if (mustStop) return true; + if (!mayStop) return false; + + // Only attempt to compute the confidence interval every sampleEveryMs + if (timeSinceLastCheck < sampleEveryMs) { + // If last call was wade 50% faster than the sampleEveryMs, let's reduce the sample interval to 10% + if (sampleEveryMs > 2 && sampleEveryMs / 2 > timeSinceLastCheck) { + sampleEveryMs -= sampleEveryMs * 0.1; + } + return false; + } + + if (timeSinceLastCheck < sampleEveryMs) return false; + lastConvergenceSample = currentMs; + // For all statistical calculations we don't want to loose the precision so have to convert to numbers first + const samples = filterOutliers(sortData(runsNs.map((n) => Number(n))), true, OutlierSensitivity.Mild); + + // If CV does not stabilize we fallback to the median approach + if (runsNs.length > maxSamplesForCV) { + const median = calcMedian(samples, true); + const mean = calcMean(samples); + const medianFactor = Math.abs(Number(mean - median)) / Number(median); + + debug("checking convergence median convergeFactor=%o, medianFactor=%o", convergeFactor, medianFactor); + + return medianFactor < convergeFactor; + } + + const mean = calcMean(samples); + const variance = calcVariance(samples, mean); + const cv = Math.sqrt(variance) / mean; + + debug( + "checking convergence via cv convergeFactor=%o, cv=%o, samples=%o, outliers=%o", + convergeFactor, + cv, + runsNs.length, + runsNs.length - samples.length + ); + + return cv < convergeFactor; + }; +} diff --git a/src/benchmark/convergence/linearAverage.ts b/src/benchmark/convergence/linearAverage.ts new file mode 100644 index 0000000..3d82885 --- /dev/null +++ b/src/benchmark/convergence/linearAverage.ts @@ -0,0 +1,66 @@ +import Debug from "debug"; +import {BenchmarkOpts, ConvergenceCheckFn} from "../../types.js"; + +const debug = Debug("@chainsafe/benchmark/convergence"); + +export function createLinearConvergenceCriteria( + startMs: number, + {maxMs, maxRuns, minRuns, minMs, convergeFactor}: Required +): ConvergenceCheckFn { + let prevAvg0 = 0; + let prevAvg1 = 0; + let lastConvergenceSample = startMs; + const sampleEveryMs = 100; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + return function canTerminate(runIdx: number, totalNs: bigint, _runNs: bigint[]): boolean { + const currentMs = Date.now(); + const elapsedMs = currentMs - startMs; + const timeSinceLastCheck = currentMs - lastConvergenceSample; + const mustStop = elapsedMs >= maxMs || runIdx >= maxRuns; + const mayStop = elapsedMs >= minMs && runIdx >= minRuns; + + debug( + "trying to converge benchmark via confidence-interval mustStop=%o, mayStop=%o, timeSinceLastCheck=%o", + mustStop, + mayStop, + timeSinceLastCheck + ); + + // Must stop + if (mustStop) return true; + + // When is a good time to stop a benchmark? A naive answer is after N milliseconds or M runs. + // This code aims to stop the benchmark when the average fn run time has converged at a value + // within a given convergence factor. To prevent doing expensive math to often for fast fn, + // it only takes samples every `sampleEveryMs`. It stores two past values to be able to compute + // a very rough linear and quadratic convergence.a + if (timeSinceLastCheck <= sampleEveryMs) return false; + + lastConvergenceSample = currentMs; + const avg = Number(totalNs / BigInt(runIdx)); + + // Compute convergence (1st order + 2nd order) + const a = prevAvg0; + const b = prevAvg1; + const c = avg; + + if (mayStop) { + // Approx linear convergence + const convergence1 = Math.abs(c - a); + // Approx quadratic convergence + const convergence2 = Math.abs(b - (a + c) / 2); + // Take the greater of both to enforce linear and quadratic are below convergeFactor + const convergence = Math.max(convergence1, convergence2) / a; + + debug("checking convergence convergeFactor=%o, convergence=%o", convergeFactor, convergence); + + // Okay to stop + has converged, stop now + if (convergence < convergeFactor) return true; + } + + prevAvg0 = prevAvg1; + prevAvg1 = avg; + return false; + }; +} diff --git a/src/benchmark/options.ts b/src/benchmark/options.ts index 937b5b7..14999cf 100644 --- a/src/benchmark/options.ts +++ b/src/benchmark/options.ts @@ -1,4 +1,4 @@ -import {BenchmarkOpts} from "../types.js"; +import {AverageCalculation, BenchmarkOpts, Convergence} from "../types.js"; export const defaultBenchmarkOptions: Required = { minRuns: 1, @@ -17,8 +17,8 @@ export const defaultBenchmarkOptions: Required = { skip: false, only: false, threshold: 2, - convergence: "linear", - averageCalculation: "simple", + convergence: Convergence.Linear, + averageCalculation: AverageCalculation.Simple, }; export function getBenchmarkOptionsWithDefaults(opts: BenchmarkOpts): Required { diff --git a/src/benchmark/runBenchmarkFn.ts b/src/benchmark/runBenchmarkFn.ts index b093633..beafffd 100644 --- a/src/benchmark/runBenchmarkFn.ts +++ b/src/benchmark/runBenchmarkFn.ts @@ -1,12 +1,17 @@ -import {BenchmarkResult, BenchmarkOpts} from "../types.js"; +import Debug from "debug"; +import {BenchmarkResult, BenchmarkOpts, Convergence, ConvergenceCheckFn} from "../types.js"; import {calcSum, filterOutliers, OutlierSensitivity} from "../utils/math.js"; import {getBenchmarkOptionsWithDefaults} from "./options.js"; -import {createCVConvergenceCriteria, createLinearConvergenceCriteria} from "./termination.js"; +import {createLinearConvergenceCriteria} from "./convergence/linearAverage.js"; +import {createCVConvergenceCriteria} from "./convergence/coefficientOfVariance.js"; -const convergenceCriteria = { - ["linear"]: createLinearConvergenceCriteria, - ["cv"]: createCVConvergenceCriteria, -}; +const debug = Debug("@chainsafe/benchmark/run"); + +const convergenceCriteria: Record) => ConvergenceCheckFn> = + { + [Convergence.Linear]: createLinearConvergenceCriteria, + [Convergence.CV]: createCVConvergenceCriteria, + }; export type BenchmarkRunOpts = BenchmarkOpts & { id: string; @@ -23,6 +28,7 @@ export async function runBenchFn( opts: BenchmarkRunOptsWithFn ): Promise<{result: BenchmarkResult; runsNs: bigint[]}> { const {id, before, beforeEach, fn, ...rest} = opts; + debug("running %o", id); const benchOptions = getBenchmarkOptionsWithDefaults(rest); const {maxMs, maxRuns, maxWarmUpMs, maxWarmUpRuns, runsFactor, threshold, convergence, averageCalculation} = benchOptions; @@ -39,8 +45,8 @@ export async function runBenchFn( throw new Error(`Average calculation logic is not defined. ${averageCalculation}`); } - if (convergence !== "linear" && convergence !== "cv") { - throw new Error(`Unknown convergence value ${convergence}`); + if (!Object.values(Convergence).includes(convergence)) { + throw new Error(`Unknown convergence value ${convergence}. Valid values are ${Object.values(Convergence)}`); } // Ratio of maxMs that the warmup is allow to take from elapsedMs @@ -59,12 +65,17 @@ export async function runBenchFn( let totalWarmUpRuns = 0; let isWarmUpPhase = maxWarmUpNs > 0 && maxWarmUpRuns > 0; + debug("starting before"); const inputAll = before ? await before() : (undefined as unknown as T2); + debug("finished before"); while (true) { + debug("executing individual run isWarmUpPhase=%o", isWarmUpPhase); const elapsedMs = Date.now() - startRunMs; + debug("starting beforeEach"); const input = beforeEach ? await beforeEach(inputAll, runIdx) : (undefined as unknown as T); + debug("finished beforeEach"); const startNs = process.hrtime.bigint(); await fn(input); diff --git a/src/benchmark/termination.ts b/src/benchmark/termination.ts deleted file mode 100644 index 6c94145..0000000 --- a/src/benchmark/termination.ts +++ /dev/null @@ -1,101 +0,0 @@ -import {BenchmarkOpts} from "../types.js"; -import {calcMean, calcMedian, calcVariance, filterOutliers, OutlierSensitivity, sortData} from "../utils/math.js"; - -export type TerminationCriteria = (runIdx: number, totalNs: bigint, runNs: bigint[]) => boolean; - -export function createLinearConvergenceCriteria( - startMs: number, - {maxMs, maxRuns, minRuns, minMs, convergeFactor}: Required -): TerminationCriteria { - let prevAvg0 = 0; - let prevAvg1 = 0; - let lastConvergenceSample = startMs; - const sampleEveryMs = 100; - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - return function canTerminate(runIdx: number, totalNs: bigint, _runNs: bigint[]): boolean { - const currentMs = Date.now(); - const elapsedMs = currentMs - startMs; - const mustStop = elapsedMs >= maxMs || runIdx >= maxRuns; - const mayStop = elapsedMs >= minMs && runIdx >= minRuns; - - // Must stop - if (mustStop) return true; - - // When is a good time to stop a benchmark? A naive answer is after N milliseconds or M runs. - // This code aims to stop the benchmark when the average fn run time has converged at a value - // within a given convergence factor. To prevent doing expensive math to often for fast fn, - // it only takes samples every `sampleEveryMs`. It stores two past values to be able to compute - // a very rough linear and quadratic convergence.a - if (Date.now() - lastConvergenceSample <= sampleEveryMs) return false; - - lastConvergenceSample = currentMs; - const avg = Number(totalNs / BigInt(runIdx)); - - // Compute convergence (1st order + 2nd order) - const a = prevAvg0; - const b = prevAvg1; - const c = avg; - - if (mayStop) { - // Approx linear convergence - const convergence1 = Math.abs(c - a); - // Approx quadratic convergence - const convergence2 = Math.abs(b - (a + c) / 2); - // Take the greater of both to enforce linear and quadratic are below convergeFactor - const convergence = Math.max(convergence1, convergence2) / a; - - // Okay to stop + has converged, stop now - if (convergence < convergeFactor) return true; - } - - prevAvg0 = prevAvg1; - prevAvg1 = avg; - return false; - }; -} - -export function createCVConvergenceCriteria( - startMs: number, - {maxMs, maxRuns, minRuns, minMs, convergeFactor}: Required -): TerminationCriteria { - let lastConvergenceSample = startMs; - const sampleEveryMs = 100; - const minSamples = minRuns > 5 ? minRuns : 5; - const maxSamplesForCV = 1000; - - return function canTerminate(runIdx: number, totalNs: bigint, runsNs: bigint[]): boolean { - const currentMs = Date.now(); - const elapsedMs = currentMs - startMs; - const mustStop = elapsedMs >= maxMs || runIdx >= maxRuns; - const mayStop = elapsedMs >= minMs && runIdx >= minRuns && runIdx > minSamples; - - // Must stop - if (mustStop) return true; - - if (Date.now() - lastConvergenceSample <= sampleEveryMs) return false; - - if (mayStop) { - lastConvergenceSample = currentMs; - - const mean = calcMean(runsNs); - const variance = calcVariance(runsNs, mean); - const cv = Math.sqrt(Number(variance)) / Number(mean); - - if (cv < convergeFactor) return true; - - // If CV does not stabilize we fallback to the median approach - if (runsNs.length > maxSamplesForCV) { - const sorted = sortData(runsNs); - const cleanedRunsNs = filterOutliers(sorted, true, OutlierSensitivity.Mild); - const median = calcMedian(cleanedRunsNs, true); - const mean = calcMean(cleanedRunsNs); - const medianFactor = Math.abs(Number(mean - median)) / Number(median); - - if (medianFactor < convergeFactor) return true; - } - } - - return false; - }; -} diff --git a/src/cli/options.ts b/src/cli/options.ts index 056e9ba..9d98f05 100644 --- a/src/cli/options.ts +++ b/src/cli/options.ts @@ -1,5 +1,5 @@ import {Options} from "yargs"; -import {StorageOptions, BenchmarkOpts, FileCollectionOptions} from "../types.js"; +import {StorageOptions, BenchmarkOpts, FileCollectionOptions, Convergence, AverageCalculation} from "../types.js"; import {defaultBenchmarkOptions} from "../benchmark/options.js"; export const optionsDefault = { @@ -209,14 +209,14 @@ export const benchmarkOptions: ICliCommandOptions = { type: "string", description: "The algorithm used to detect the convergence to stop benchmark runs", default: defaultBenchmarkOptions.convergence, - choices: ["linear", "cv"], + choices: Object.values(Convergence), group: benchmarkGroup, }, averageCalculation: { type: "string", description: "Use simple average of all runs or clean the outliers before calculating average", default: defaultBenchmarkOptions.averageCalculation, - choices: ["simple", "clean-outliers"], + choices: Object.values(AverageCalculation), group: benchmarkGroup, }, }; diff --git a/src/types.ts b/src/types.ts index e11c970..4bb4ad3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -67,12 +67,10 @@ export type BenchmarkOpts = { triggerGC?: boolean; /** * The algorithm to detect the convergence to stop the benchmark function runs. - * linear - Calculate the moving average among last 3 runs average and compare through quadratic formula - * cv - Coefficient Variance is a statistical tool which calculates data pattern on all runs and calculate median * */ - convergence?: "linear" | "cv"; + convergence?: Convergence; /** Use simple average of all runs or clean the outliers before calculating average */ - averageCalculation?: "simple" | "clean-outliers"; + averageCalculation?: AverageCalculation; }; // Create partial only for specific keys @@ -161,3 +159,58 @@ export type BenchmarkComparisonResult = { isFailed: boolean; isImproved: boolean; }; + +/** Algorithms to detect when to stop the benchmark runs */ +export enum Convergence { + /** + * **Linear**: + * + * Uses a moving-average approach to check for convergence by comparing + * how consecutive averages change over time. Concretely, the logic tracks + * a few past average values (e.g. last 3 averages) and determines if: + * + * 1. The difference between the most recent average and the oldest + * average is sufficiently small (linear convergence). + * 2. Additionally, it may check that the “midpoint” or intermediate average + * is consistent with the trend (quadratic element in the code). + * + * This approach works best in relatively stable environments or for + * functions whose execution times fluctuate minimally. However, if there is + * high noise or if runs are extremely fast (on the nanosecond scale), + * you might see premature stopping or extended run times. + */ + Linear = "linear", + + /** + * **Coefficient of Variation (CV)**: + * + * This approach calculates the ratio of the sample standard deviation + * to the mean (σ/μ) over all data points collected so far. + * - If the CV falls below a specified threshold (convergeFactor), + * we consider that “stable” and stop. + * - As a fallback, if too many runs occur without convergence, + * the code may compare mean vs. median to decide if it’s time to stop. + * + * Strengths: + * - Good for benchmarks with moderate noise: it normalizes variation by + * the mean, so it handles scale differences well. + * - Straightforward to implement and interpret (CV < x%). + * + * Limitations: + * - Highly noisy benchmarks or micro benchmarks (few nanoseconds) can + * cause erratic CV values. If the noise is large relative to the mean, + * convergence may never be triggered. Conversely, extremely uniform runs + * can cause an instant (potentially premature) stop. + */ + CV = "cv", +} + +/** How to calculate average for output */ +export enum AverageCalculation { + /** Calculate simple average */ + Simple = "simple", + /** Clean the outliers first then calculate the average */ + CleanOutliers = "clean-outliers", +} + +export type ConvergenceCheckFn = (runIdx: number, totalNs: bigint, runNs: bigint[]) => boolean; diff --git a/src/utils/math.ts b/src/utils/math.ts index 34ee6ec..46a12a0 100644 --- a/src/utils/math.ts +++ b/src/utils/math.ts @@ -1,13 +1,20 @@ +export const MAX_FRACTION = 8; + +export function roundDecimal(n: number): number { + return Number(n.toFixed(MAX_FRACTION)); +} + /** * Computes the total of all values in the array by sequentially adding each element. * Handles both positive and negative BigInt values without precision loss. */ -export function calcSum(arr: bigint[]): bigint { +export function calcSum(arr: T[]): bigint { let s = BigInt(0); for (const n of arr) { - s += n; + s = s + BigInt(n); } + return s; } @@ -15,30 +22,51 @@ export function calcSum(arr: bigint[]): bigint { * Determines the central tendency by dividing the total sum by the number of elements. * Uses integer division that naturally truncates decimal remainders. */ -export function calcMean(arr: bigint[]): bigint { - return BigInt(calcSum(arr) / BigInt(arr.length)); +export function calcMean(arr: T[]): number { + if (arr.length < 1) throw new Error("Can not find mean of any empty array"); + + return roundDecimal(Number(calcSum(arr)) / arr.length); +} + +/** + * Quantifies data spread by averaging squared deviations from the mean. + * A value of zero indicates identical values, larger values show greater dispersion. + */ +export function calcVariance(arr: T[], mean: number): number { + if (arr.length === 0) throw new Error("Can not find variance of an empty array"); + let base = 0; + + for (const n of arr) { + const diff = Number(n) - mean; + base += diff * diff; + } + + return roundDecimal(base / arr.length); } /** * Quantifies data spread by averaging squared deviations from the mean. * A value of zero indicates identical values, larger values show greater dispersion. */ -export function calcVariance(arr: bigint[], mean: bigint): bigint { - let base = BigInt(0); +export function calcUnbiasedVariance(arr: T[], mean: number): number { + if (arr.length === 0) throw new Error("Can not find variance of an empty array"); + let base = 0; for (const n of arr) { - const diff = n - mean; + const diff = Number(n) - mean; base += diff * diff; } - return base / BigInt(arr.length); + if (arr.length < 2) return roundDecimal(base); + + return roundDecimal(base / arr.length - 1); } /** * Organizes values from smallest to largest while preserving the original array. * Essential for percentile-based calculations like median and quartiles. */ -export function sortData(arr: bigint[]): bigint[] { +export function sortData(arr: T[]): T[] { return [...arr].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); } @@ -46,16 +74,18 @@ export function sortData(arr: bigint[]): bigint[] { * Identifies the middle value that separates higher and lower halves of the dataset. * For even-sized arrays, averages the two central values to find the midpoint. */ -export function calcMedian(arr: bigint[], sorted: boolean): bigint { +export function calcMedian(arr: T[], sorted: boolean): number { + if (arr.length === 0) throw new Error("Can not calculate median for empty values"); + // 1. Sort the BigInt array const data = sorted ? arr : sortData(arr); // 3. Calculate median const mid = Math.floor(data.length / 2); if (data.length % 2 === 0) { - return (data[mid - 1] + data[mid]) / BigInt(2); // Average two middle values + return roundDecimal((Number(data[mid - 1]) + Number(data[mid])) / 2); // Average two middle values } else { - return data[mid]; // Single middle value + return roundDecimal(Number(data[mid])); // Single middle value } } @@ -63,7 +93,7 @@ export function calcMedian(arr: bigint[], sorted: boolean): bigint { * Determines cutoff points that divide data into four equal-frequency segments. * Uses linear interpolation to estimate values between actual data points. */ -export function calcQuartile(arr: bigint[], sorted: boolean, percentile: number): number { +export function calcQuartile(arr: T[], sorted: boolean, percentile: number): number { const sortedData = sorted ? arr : sortData(arr); const index = (sortedData.length - 1) * percentile; @@ -71,10 +101,12 @@ export function calcQuartile(arr: bigint[], sorted: boolean, percentile: number) const fraction = index - floor; if (sortedData[floor + 1] !== undefined) { - return Number(sortedData[floor]) + fraction * Number(sortedData[floor + 1] - sortedData[floor]); + const base = Number(sortedData[floor]); + const next = Number(sortedData[floor + 1]); + return roundDecimal(base + fraction * (next - base)); } - return Number(sortedData[floor]); + return roundDecimal(Number(sortedData[floor])); } /** @@ -106,7 +138,11 @@ export enum OutlierSensitivity { * The `OutlierSensitivity` is scaling factors applied to the IQR to determine how far data points * can deviate from the quartiles before being considered outliers. */ -export function filterOutliers(arr: bigint[], sorted: boolean, sensitivity: OutlierSensitivity): bigint[] { +export function filterOutliers( + arr: T[], + sorted: boolean, + sensitivity: OutlierSensitivity +): T[] { if (arr.length < 4) return arr; // Too few data points const data = sorted ? arr : sortData(arr); diff --git a/test/unit/utils/math.test.ts b/test/unit/utils/math.test.ts index 8fc2915..555d196 100644 --- a/test/unit/utils/math.test.ts +++ b/test/unit/utils/math.test.ts @@ -36,60 +36,65 @@ describe("math utility functions", () => { describe("calcMean", () => { it("should throw or behave predictably for an empty array", () => { - // By default, dividing by BigInt(0) will throw in JavaScript. - // If you want a different behavior, you can wrap your function or catch errors here. expect(() => calcMean([])).toThrow(); }); it("should correctly calculate the mean of a single-element array", () => { const arr = [5n]; - expect(calcMean(arr)).toBe(5n); + expect(calcMean(arr)).toBe(5); }); it("should correctly calculate the mean of multiple BigInts", () => { const arr = [2n, 4n, 6n]; // sum=12, length=3 => mean=4 - expect(calcMean(arr)).toBe(4n); + expect(calcMean(arr)).toBe(4); }); it("should handle negative values correctly", () => { const arr = [-5n, -15n, 10n]; // sum=-10, length=3 => mean=-3.333..., but truncated to BigInt => -3n if using integer division - expect(calcMean(arr)).toBe(-3n); + expect(calcMean(arr)).toBe(-3.33333333); }); }); describe("calcVariance", () => { it("should compute variance for a small sample of integers", () => { const arr = [2n, 4n, 4n, 6n, 8n]; - // mean = (2+4+4+6+8)/5 = 24/5 = 4.8 => truncated to 4n if using integer division - // If mean=4n, diffs = (-2,0,0,2,4), squares = (4,0,0,4,16), sum=24 => var=24/5=4.8 => truncated to 4n + // sum = 24, length = 5 + // mean = 4.8 + // diffs = (-2.8, -0.7999, -0.7999, 1.2, 3.2) + // squares = (7.839999999999999, 0.6399999999999997, 0.6399999999999997, 1.4400000000000004, 10.240000000000002) = 20.8 + // var = 20.8 / 5 = 4.16 const meanBigInt = calcMean(arr); const varianceBigInt = calcVariance(arr, meanBigInt); - expect(varianceBigInt).toBe(4n); + expect(varianceBigInt).toBe(4.16); }); it("should handle a single-element array (variance=0)", () => { const arr = [100n]; const mean = calcMean(arr); // 100n const variance = calcVariance(arr, mean); - expect(variance).toBe(0n); + expect(variance).toBe(0); }); it("should handle negative values", () => { const arr = [-10n, -4n, -2n]; - // sum = -16, length=3 => mean = floor(-16/3) = -5n - // diffs = (-5,1,3), squares=(25,1,9)=35 => var=35/3=11 => 11n + // sum = -16, length=3 + // mean = -16/3 = 5.333333333333333 + // diffs = (-4.666666666666667, 1.333333333333333, 3.333333333333333), + // squares = (21.777777777777782, 1.777777777777777, 11.111111111111109) = 34.66666666666667 + // var = 34.66666666666667 / 3 = 11.555555555555557 const mean = calcMean(arr); + console.log(mean); const variance = calcVariance(arr, mean); - expect(variance).toBe(11n); + expect(variance).toBe(11.55555556); }); it("should return 0 for an array of identical values", () => { const arr = [5n, 5n, 5n]; const mean = calcMean(arr); const variance = calcVariance(arr, mean); - expect(variance).toBe(0n); + expect(variance).toBe(0); }); }); @@ -125,20 +130,20 @@ describe("math utility functions", () => { it("should return the middle element when the array length is odd", () => { const arr = [3n, 1n, 2n]; // sorted = [1n, 2n, 3n], median = 2n - expect(calcMedian(arr, false)).toBe(2n); + expect(calcMedian(arr, false)).toBe(2); }); it("should return the average of two middle elements when the array length is even", () => { const arr = [3n, 1n, 2n, 4n]; // sorted = [1n, 2n, 3n, 4n] - // middle indices = 1,2 => average => (2n+3n)/2n=2n - expect(calcMedian(arr, false)).toBe(2n); + // middle indices = 1,2 => average => (2+3)/2=2.5 + expect(calcMedian(arr, false)).toBe(2.5); }); it("should skip re-sorting if 'sorted=true' is provided", () => { // already sorted const arr = [1n, 2n, 3n, 4n]; - expect(calcMedian(arr, true)).toBe(2n); // middle indices => 1n,2n => average=2n + expect(calcMedian(arr, true)).toBe(2.5); }); }); @@ -146,12 +151,10 @@ describe("math utility functions", () => { const sortedData = sortData([1n, 2n, 4n, 10n, 20n, 100n]); it("should return the first quartile (Q1) => percentile=0.25", () => { - // sorted array = [1n, 2n, 4n, 10n, 20n, 100n] - // length=6 => index = (6-1)*0.25=1.25 => floor=1 => fraction=0.25 - // base=2n, next=4n => difference=2n => fraction=0.25 => 2 + 0.25*2=2.5 => ~ BigInt(2.5) - // Because we must do BigInt arithmetic carefully, the function does Number(...) inside - // => the result = 2n + 0.25*(4-2)=2n + 0.5=2.5 => cast => 2n if trunc - // But the function does => BigInt(2 + fraction*(4-2)) => 2 + 0.25*2 => 2.5 + // sorted array = [1n, 2n, 4n, 10n, 20n, 100n], length=6 + // index = (6-1)*0.25=1.25, floor=1, fraction=0.25 + // base=2, next=4, difference=2 + // result = 2 + 0.25 * (4 - 2) = 2.5 const q1 = calcQuartile(sortedData, true, 0.25); expect(q1).toBe(2.5); });