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>
This commit is contained in:
91
src/calibration/calibrator.js
Normal file
91
src/calibration/calibrator.js
Normal file
@@ -0,0 +1,91 @@
|
||||
'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;
|
||||
Reference in New Issue
Block a user