refactor(units): route all conversions through UnitPolicy.convert

Delete the legacy _convertUnitValue helper on the domain and the
duplicate convertUnitValue export on curveNormalizer; both were
identical to UnitPolicy.convert. Callers in flowController, the
curve normalizer, and buildQHCurve now go through this.unitPolicy.
The contract in .claude/refactor/CONTRACTS.md §6 named these as the
target migration; this finishes the rollout for rotatingMachine.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-23 13:43:26 +02:00
parent a18aec32b9
commit 455f15dc55
6 changed files with 32 additions and 51 deletions

View File

@@ -1,39 +1,24 @@
const { convert } = require('generalFunctions');
/**
* Strict numeric unit conversion. Mirrors specificClass._convertUnitValue
* so the curve normalizer is testable without a Machine instance.
*/
function convertUnitValue(value, fromUnit, toUnit, contextLabel = 'unit conversion') {
const numeric = Number(value);
if (!Number.isFinite(numeric)) {
throw new Error(`${contextLabel}: value '${value}' is not finite`);
}
if (!fromUnit || !toUnit || fromUnit === toUnit) return numeric;
return convert(numeric).from(fromUnit).to(toUnit);
}
/**
* Convert one curve section (nq or np) from supplied units to canonical
* units. Logs a warning when the per-pressure median y jumps by more than
* 3x relative to the previous pressure level — that almost always means the
* curve file is corrupt (mixed units, swapped rows) and the predict module
* would otherwise silently produce nonsense values.
* units using the host UnitPolicy. Logs a warning when the per-pressure
* median y jumps by more than 3x relative to the previous pressure level —
* that almost always means the curve file is corrupt (mixed units, swapped
* rows) and the predict module would otherwise silently produce nonsense.
*/
function normalizeCurveSection(section, fromYUnit, toYUnit, fromPressureUnit, toPressureUnit, sectionName, logger) {
function normalizeCurveSection(section, unitPolicy, fromYUnit, toYUnit, fromPressureUnit, toPressureUnit, sectionName, logger) {
const normalized = {};
let prevMedianY = null;
for (const [pressureKey, pair] of Object.entries(section || {})) {
const canonicalPressure = convertUnitValue(
const canonicalPressure = unitPolicy.convert(
Number(pressureKey),
fromPressureUnit,
toPressureUnit,
`${sectionName} pressure axis`
`${sectionName} pressure axis`,
);
const xArray = Array.isArray(pair?.x) ? pair.x.map(Number) : [];
const yArray = Array.isArray(pair?.y)
? pair.y.map((v) => convertUnitValue(v, fromYUnit, toYUnit, `${sectionName} output`))
? pair.y.map((v) => unitPolicy.convert(v, fromYUnit, toYUnit, `${sectionName} output`))
: [];
if (!xArray.length || !yArray.length || xArray.length !== yArray.length) {
throw new Error(`Invalid ${sectionName} section at pressure '${pressureKey}'.`);
@@ -74,21 +59,23 @@ function normalizeMachineCurve(rawCurve, unitPolicy, logger) {
return {
nq: normalizeCurveSection(
rawCurve.nq,
unitPolicy,
curveUnits.flow,
canonicalFlow,
curveUnits.pressure,
canonicalPressure,
'nq',
logger
logger,
),
np: normalizeCurveSection(
rawCurve.np,
unitPolicy,
curveUnits.power,
canonicalPower,
curveUnits.pressure,
canonicalPressure,
'np',
logger
logger,
),
};
}
@@ -114,4 +101,4 @@ function readCanonical(unitPolicy, type) {
return (unitPolicy.canonical || {})[type] || null;
}
module.exports = { normalizeMachineCurve, normalizeCurveSection, convertUnitValue };
module.exports = { normalizeMachineCurve, normalizeCurveSection };

View File

@@ -79,6 +79,12 @@ function buildQHCurve(predictors, ctrlPct, options = {}) {
if (!pf.inputCurve || typeof pf.inputCurve !== 'object') {
return { error: NO_CURVE_ERROR, points: [] };
}
const policy = options.unitPolicy || predictors.unitPolicy;
if (!policy) {
return { error: 'No unitPolicy available for Q-axis conversion', points: [] };
}
const flowFrom = policy.canonical?.flow || policy.canonical?.('flow');
const flowTo = policy.output?.flow || policy.output?.('flow');
const x = Number.isFinite(+ctrlPct) ? +ctrlPct : (pf.currentX ?? 0);
const RHO = 999.1; // kg/m³ — water at ~15 °C
const G = 9.80665; // m/s²
@@ -103,7 +109,8 @@ function buildQHCurve(predictors, ctrlPct, options = {}) {
for (const p of pressures) {
pf.fDimension = p;
const QM3s = pf.y(x);
points.push({ Q: QM3s * 3600, H: p / (RHO * G), dpPa: p });
const Q = policy.convert(QM3s, flowFrom, flowTo, 'buildQHCurve Q-axis');
points.push({ Q, H: p / (RHO * G), dpPa: p });
}
} finally {
pf.fDimension = originalF;

View File

@@ -50,7 +50,7 @@ class FlowController {
return await host.executeSequence(parameter);
case 'flowmovement': {
const canonicalFlowSetpoint = host._convertUnitValue(
const canonicalFlowSetpoint = host.unitPolicy.convert(
parameter,
host.unitPolicy.output.flow,
host.unitPolicy.canonical.flow,

View File

@@ -247,12 +247,6 @@ class Machine extends BaseDomain {
if (!this.isUnitValidForType(type, u)) throw new Error(`Unsupported unit '${u}' for ${type} measurement.`);
return u;
}
_convertUnitValue(value, from, to, ctx = 'unit conversion') {
const n = Number(value);
if (!Number.isFinite(n)) throw new Error(`${ctx}: value '${value}' is not finite`);
if (!from || !to || from === to) return n;
return convert(n).from(from).to(to);
}
_measurementPositionForMetric(metricId) { return metricId === 'power' ? 'atEquipment' : 'downstream'; }
_resolveProcessRangeForMetric(metricId, predicted, measured) {
let processMin = NaN; let processMax = NaN;