Files
measurement/src/calibration/calibrator.js
znetsixe b990f67df1 P3 wave 1: extract measurement simulator/calibration/commands + CONTRACT
src/simulation/simulator.js  random-walk generator (was simulateInput inline)
  src/calibration/calibrator.js  calibrate + isStable + evaluateRepeatability,
                                using generalFunctions/stats. NB: isStable
                                tautology preserved verbatim — see
                                OPEN_QUESTIONS.md 2026-05-10 for the bug.
  src/commands/                  registry + handlers (canonical names from start)
  CONTRACT.md                    inputs/outputs/events surface

77 basic tests pass (62 pre-refactor + 15 new across the three new files).
specificClass.js / nodeClass.js untouched — integration is P3 wave 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:32:26 +02:00

92 lines
3.5 KiB
JavaScript

'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;