'use strict'; const { stats } = require('generalFunctions'); const MARGIN_FACTOR = 2; /** * Calibration helper extracted from measurement/specificClass.js. * * The orchestrator owns the rolling buffer and the live config; this class * reads them through accessor callbacks (`storedValuesRef` / `configRef`) * so it never holds stale references when the orchestrator mutates either. */ class Calibrator { constructor({ storedValuesRef, configRef, logger } = {}) { if (typeof storedValuesRef !== 'function' || typeof configRef !== 'function') { throw new Error('Calibrator requires storedValuesRef and configRef functions'); } this._storedValues = storedValuesRef; this._config = configRef; this.logger = logger || { info() {}, warn() {}, debug() {}, error() {} }; } /** * Decide whether the rolling window is stable enough to trust. * Mirrors the original threshold check; with `stdDev=0` (constant input) * the comparison short-circuits to true. */ isStable() { const values = this._storedValues(); if (!Array.isArray(values) || values.length < 2) { return { isStable: false, stdDev: 0 }; } const stdDev = stats.stdDev(values); const stableThreshold = stdDev * MARGIN_FACTOR; return { isStable: stdDev < stableThreshold || stdDev === 0, stdDev }; } /** * Compute the offset that drives `currentOutputAbs` to the configured * baseline (scaling input-min when scaling is enabled, abs-min otherwise). * Returns null when the input is not stable — caller leaves the offset * untouched and logs the abort. */ calibrate(currentOutputAbs) { const { isStable } = this.isStable(); if (!isStable) { this.logger.warn('Large fluctuations detected between stored values. Calibration aborted.'); return null; } const cfg = this._config(); const scaling = (cfg && cfg.scaling) || {}; const baseline = scaling.enabled ? scaling.inputMin : scaling.absMin; if (typeof baseline !== 'number' || !Number.isFinite(baseline)) { this.logger.warn('Calibration baseline missing from config.scaling. Aborted.'); return null; } const offset = baseline - currentOutputAbs; this.logger.info(`Stable input value detected. Calibration completed. Offset=${offset}`); return { offset }; } /** * Repeatability proxy: the std-dev of the smoothed rolling buffer once * stability is confirmed. Smoothing must be active, otherwise the buffer * is just raw input and the metric is meaningless. */ evaluateRepeatability() { const cfg = this._config(); const method = cfg && cfg.smoothing && cfg.smoothing.smoothMethod; const normalized = typeof method === 'string' ? method.toLowerCase() : method; if (normalized === 'none' || normalized == null) { this.logger.warn('Repeatability evaluation is not possible without smoothing.'); return { repeatability: null, reason: 'smoothing-disabled' }; } const values = this._storedValues(); if (!Array.isArray(values) || values.length < 2) { this.logger.warn('Not enough data to evaluate repeatability.'); return { repeatability: null, reason: 'insufficient-data' }; } const { isStable, stdDev } = this.isStable(); if (!isStable) { this.logger.warn('Data not stable enough to evaluate repeatability.'); return { repeatability: null, reason: 'unstable' }; } this.logger.info(`Repeatability evaluated. Standard Deviation: ${stdDev}`); return { repeatability: stdDev }; } } module.exports = Calibrator;