Files
rotatingMachine/src/specificClass.js
znetsixe f363ee53ef Merge commit '4cf46f3' into HEAD
# Conflicts:
#	src/nodeClass.js
#	src/specificClass.js
2026-03-31 18:23:38 +02:00

1563 lines
58 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const EventEmitter = require('events');
const {loadCurve,gravity,logger,configUtils,configManager,state, nrmse, MeasurementContainer, predict, interpolation , childRegistrationUtils,coolprop, convert, POSITIONS} = require('generalFunctions');
const CANONICAL_UNITS = Object.freeze({
pressure: 'Pa',
atmPressure: 'Pa',
flow: 'm3/s',
power: 'W',
temperature: 'K',
});
const DEFAULT_IO_UNITS = Object.freeze({
pressure: 'mbar',
flow: 'm3/h',
power: 'kW',
temperature: 'C',
});
const DEFAULT_CURVE_UNITS = Object.freeze({
pressure: 'mbar',
flow: 'm3/h',
power: 'kW',
control: '%',
});
/**
* Rotating machine domain model.
* Combines machine curves, state transitions and measurement reconciliation
* to produce flow/power/efficiency behavior for pumps and similar assets.
*/
class Machine {
/*------------------- Construct and set vars -------------------*/
constructor(machineConfig = {}, stateConfig = {}, errorMetricsConfig = {}) {
//basic setup
this.emitter = new EventEmitter(); // Own EventEmitter
this.logger = new logger(machineConfig.general.logging.enabled,machineConfig.general.logging.logLevel, machineConfig.general.name);
this.configManager = new configManager();
this.defaultConfig = this.configManager.getConfig('rotatingMachine'); // Load default config for rotating machine ( use software type name ? )
this.configUtils = new configUtils(this.defaultConfig);
// Load a specific curve
this.model = machineConfig.asset.model; // Get the model from the machineConfig
this.rawCurve = this.model ? loadCurve(this.model) : null;
this.curve = null;
//Init config and check if it is valid
this.config = this.configUtils.initConfig(machineConfig);
//add unique name for this node.
this.config = this.configUtils.updateConfig(this.config, {general:{name: this.config.functionality?.softwareType + "_" + machineConfig.general.id}}); // add unique name if not present
this.unitPolicy = this._buildUnitPolicy(this.config);
this.config = this.configUtils.updateConfig(this.config, {
general: { unit: this.unitPolicy.output.flow },
asset: {
...this.config.asset,
unit: this.unitPolicy.output.flow,
curveUnits: this.unitPolicy.curve,
},
});
if (!this.model || !this.rawCurve) {
this.logger.error(`${!this.model ? 'Model not specified' : 'Curve not found for model ' + this.model} in machineConfig. Cannot make predictions.`);
// Set prediction objects to null to prevent method calls
this.predictFlow = null;
this.predictPower = null;
this.predictCtrl = null;
this.hasCurve = false;
}
else{
try {
this.hasCurve = true;
this.curve = this._normalizeMachineCurve(this.rawCurve);
this.config = this.configUtils.updateConfig(this.config, { asset: { ...this.config.asset, machineCurve: this.curve } });
//machineConfig = { ...machineConfig, asset: { ...machineConfig.asset, machineCurve: this.curve } }; // Merge curve into machineConfig
this.predictFlow = new predict({ curve: this.config.asset.machineCurve.nq }); // load nq (x : ctrl , y : flow relationship)
this.predictPower = new predict({ curve: this.config.asset.machineCurve.np }); // load np (x : ctrl , y : power relationship)
this.predictCtrl = new predict({ curve: this.reverseCurve(this.config.asset.machineCurve.nq) }); // load reversed nq (x: flow, y: ctrl relationship)
} catch (error) {
this.logger.error(`Curve normalization failed for model '${this.model}': ${error.message}`);
this.predictFlow = null;
this.predictPower = null;
this.predictCtrl = null;
this.hasCurve = false;
}
}
this.state = new state(stateConfig, this.logger); // Init State manager and pass logger
this.errorMetrics = new nrmse(errorMetricsConfig, this.logger);
// Initialize measurements
this.measurements = new MeasurementContainer({
autoConvert: true,
windowSize: 50,
defaultUnits: {
pressure: this.unitPolicy.output.pressure,
flow: this.unitPolicy.output.flow,
power: this.unitPolicy.output.power,
temperature: this.unitPolicy.output.temperature,
atmPressure: 'Pa',
},
preferredUnits: {
pressure: this.unitPolicy.output.pressure,
flow: this.unitPolicy.output.flow,
power: this.unitPolicy.output.power,
temperature: this.unitPolicy.output.temperature,
atmPressure: 'Pa',
},
canonicalUnits: this.unitPolicy.canonical,
storeCanonical: true,
strictUnitValidation: true,
throwOnInvalidUnit: true,
requireUnitForTypes: ['pressure', 'flow', 'power', 'temperature', 'atmPressure'],
}, this.logger);
this.interpolation = new interpolation();
this.flowDrift = null;
this.powerDrift = null;
this.pressureDrift = { level: 0, flags: ["nominal"], source: null };
this.driftProfiles = {
flow: {
windowSize: 30,
minSamplesForLongTerm: 10,
ewmaAlpha: 0.15,
alignmentToleranceMs: 2500,
strictValidation: true,
},
power: {
windowSize: 30,
minSamplesForLongTerm: 10,
ewmaAlpha: 0.15,
alignmentToleranceMs: 2500,
strictValidation: true,
},
};
this.errorMetrics.registerMetric("flow", this.driftProfiles.flow);
this.errorMetrics.registerMetric("power", this.driftProfiles.power);
this.predictionHealth = {
quality: "invalid",
confidence: 0,
pressureSource: null,
flags: ["not_initialized"],
};
this.currentMode = this.config.mode.current;
this.currentEfficiencyCurve = {};
this.cog = 0;
this.NCog = 0;
this.cogIndex = 0;
this.minEfficiency = 0;
this.absDistFromPeak = 0;
this.relDistFromPeak = 0;
// When position state changes, update position
this.state.emitter.on("positionChange", (data) => {
this.logger.debug(`Position change detected: ${data}`);
this.updatePosition();
});
//When state changes look if we need to do other updates
this.state.emitter.on("stateChange", (newState) => {
this.logger.debug(`State change detected: ${newState}`);
this._updateState();
});
//perform init for certain values
this._init();
this.child = {}; // object to hold child information so we know on what to subscribe
this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility
this.virtualPressureChildIds = {
upstream: "dashboard-sim-upstream",
downstream: "dashboard-sim-downstream",
};
this.virtualPressureChildren = {};
this.realPressureChildIds = {
upstream: new Set(),
downstream: new Set(),
};
this.childMeasurementListeners = new Map();
this._initVirtualPressureChildren();
}
_initVirtualPressureChildren() {
const createVirtualChild = (position) => {
const id = this.virtualPressureChildIds[position];
const name = `dashboard-sim-${position}`;
const measurements = new MeasurementContainer({
autoConvert: true,
defaultUnits: {
pressure: this.unitPolicy.output.pressure,
flow: this.unitPolicy.output.flow,
power: this.unitPolicy.output.power,
temperature: this.unitPolicy.output.temperature,
},
preferredUnits: {
pressure: this.unitPolicy.output.pressure,
flow: this.unitPolicy.output.flow,
power: this.unitPolicy.output.power,
temperature: this.unitPolicy.output.temperature,
},
canonicalUnits: this.unitPolicy.canonical,
storeCanonical: true,
strictUnitValidation: true,
throwOnInvalidUnit: true,
requireUnitForTypes: ['pressure'],
}, this.logger);
measurements.setChildId(id);
measurements.setChildName(name);
measurements.setParentRef(this);
return {
config: {
general: { id, name },
functionality: {
softwareType: "measurement",
positionVsParent: position,
},
asset: {
type: "pressure",
unit: this.unitPolicy.output.pressure,
},
},
measurements,
};
};
const upstreamChild = createVirtualChild("upstream");
const downstreamChild = createVirtualChild("downstream");
this.virtualPressureChildren.upstream = upstreamChild;
this.virtualPressureChildren.downstream = downstreamChild;
this.registerChild(upstreamChild, "measurement");
this.registerChild(downstreamChild, "measurement");
}
_init(){
//assume standard temperature is 20degrees
this.measurements.type('temperature').variant('measured').position('atEquipment').value(15, Date.now(), this.unitPolicy.output.temperature);
//assume standard atm pressure is at sea level
this.measurements.type('atmPressure').variant('measured').position('atEquipment').value(101325, Date.now(), 'Pa');
//populate min and max when curve data is available
const flowunit = this.unitPolicy.canonical.flow;
if (this.predictFlow) {
this.measurements.type('flow').variant('predicted').position('max').value(this.predictFlow.currentFxyYMax, Date.now() , flowunit);
this.measurements.type('flow').variant('predicted').position('min').value(this.predictFlow.currentFxyYMin, Date.now(), flowunit);
} else {
this.measurements.type('flow').variant('predicted').position('max').value(0, Date.now(), flowunit);
this.measurements.type('flow').variant('predicted').position('min').value(0, Date.now(), flowunit);
}
}
_updateState(){
const isOperational = this._isOperationalState();
if(!isOperational){
//overrule the last prediction this should be 0 now
this.measurements.type("flow").variant("predicted").position("downstream").value(0,Date.now(),this.unitPolicy.canonical.flow);
this.measurements.type("flow").variant("predicted").position("atEquipment").value(0,Date.now(),this.unitPolicy.canonical.flow);
this.measurements.type("power").variant("predicted").position("atEquipment").value(0,Date.now(),this.unitPolicy.canonical.power);
}
this._updatePredictionHealth();
}
/*------------------- Register child events -------------------*/
registerChild(child, softwareType) {
const resolvedSoftwareType = softwareType || child?.config?.functionality?.softwareType || "measurement";
this.logger.debug('Setting up child event for softwaretype ' + resolvedSoftwareType);
if(resolvedSoftwareType === "measurement"){
const position = String(child.config.functionality.positionVsParent || "atEquipment").toLowerCase();
const measurementType = child.config.asset.type;
const childId = child.config?.general?.id || `${measurementType}-${position}-unknown`;
const isVirtualPressureChild = Object.values(this.virtualPressureChildIds).includes(childId);
if (measurementType === "pressure" && !isVirtualPressureChild) {
this.realPressureChildIds[position]?.add(childId);
}
//rebuild to measurementype.variant no position and then switch based on values not strings or names.
const eventName = `${measurementType}.measured.${position}`;
const listenerKey = `${childId}:${eventName}`;
const existingListener = this.childMeasurementListeners.get(listenerKey);
if (existingListener) {
if (typeof existingListener.emitter.off === "function") {
existingListener.emitter.off(existingListener.eventName, existingListener.handler);
} else if (typeof existingListener.emitter.removeListener === "function") {
existingListener.emitter.removeListener(existingListener.eventName, existingListener.handler);
}
}
this.logger.debug(`Setting up listener for ${eventName} from child ${child.config.general.name}`);
// Register event listener for measurement updates
const listener = (eventData) => {
this.logger.debug(`🔄 ${position} ${measurementType} from ${eventData.childName}: ${eventData.value} ${eventData.unit}`);
this.logger.debug(` Emitting... ${eventName} with data:`);
// Route through centralized handlers so unit validation/conversion is applied once.
this._callMeasurementHandler(measurementType, eventData.value, position, eventData);
};
child.measurements.emitter.on(eventName, listener);
this.childMeasurementListeners.set(listenerKey, {
emitter: child.measurements.emitter,
eventName,
handler: listener,
});
}
}
// Centralized handler dispatcher
_callMeasurementHandler(measurementType, value, position, context) {
switch (measurementType) {
case 'pressure':
this.updateMeasuredPressure(value, position, context);
break;
case 'flow':
this.updateMeasuredFlow(value, position, context);
break;
case 'power':
this.updateMeasuredPower(value, position, context);
break;
case 'temperature':
this.updateMeasuredTemperature(value, position, context);
break;
default:
this.logger.warn(`No handler for measurement type: ${measurementType}`);
// Generic handler - just update position
this.updatePosition();
break;
}
}
//---------------- END child stuff -------------//
_buildUnitPolicy(config) {
const flowOutputUnit = this._resolveUnitOrFallback(
config?.general?.unit,
'volumeFlowRate',
DEFAULT_IO_UNITS.flow,
'general.flow'
);
const pressureOutputUnit = this._resolveUnitOrFallback(
config?.asset?.pressureUnit,
'pressure',
DEFAULT_IO_UNITS.pressure,
'asset.pressure'
);
const powerOutputUnit = this._resolveUnitOrFallback(
config?.asset?.powerUnit,
'power',
DEFAULT_IO_UNITS.power,
'asset.power'
);
const temperatureOutputUnit = this._resolveUnitOrFallback(
config?.asset?.temperatureUnit,
'temperature',
DEFAULT_IO_UNITS.temperature,
'asset.temperature'
);
const curveUnits = this._resolveCurveUnits(config?.asset?.curveUnits || {}, flowOutputUnit);
return {
canonical: { ...CANONICAL_UNITS },
output: {
pressure: pressureOutputUnit,
flow: flowOutputUnit,
power: powerOutputUnit,
temperature: temperatureOutputUnit,
atmPressure: 'Pa',
},
curve: curveUnits,
};
}
_resolveCurveUnits(curveUnits = {}, fallbackFlowUnit = DEFAULT_CURVE_UNITS.flow) {
const pressure = this._resolveUnitOrFallback(
curveUnits.pressure,
'pressure',
DEFAULT_CURVE_UNITS.pressure,
'asset.curveUnits.pressure'
);
const flow = this._resolveUnitOrFallback(
curveUnits.flow,
'volumeFlowRate',
fallbackFlowUnit || DEFAULT_CURVE_UNITS.flow,
'asset.curveUnits.flow'
);
const power = this._resolveUnitOrFallback(
curveUnits.power,
'power',
DEFAULT_CURVE_UNITS.power,
'asset.curveUnits.power'
);
const control = typeof curveUnits.control === 'string' && curveUnits.control.trim()
? curveUnits.control.trim()
: DEFAULT_CURVE_UNITS.control;
return { pressure, flow, power, control };
}
_resolveUnitOrFallback(candidate, expectedMeasure, fallbackUnit, label) {
const fallback = String(fallbackUnit || '').trim();
const raw = typeof candidate === 'string' ? candidate.trim() : '';
if (!raw) return fallback;
try {
const desc = convert().describe(raw);
if (expectedMeasure && desc.measure !== expectedMeasure) {
throw new Error(`expected ${expectedMeasure} but got ${desc.measure}`);
}
return raw;
} catch (error) {
this.logger.warn(`Invalid ${label} unit '${raw}' (${error.message}). Falling back to '${fallback}'.`);
return fallback;
}
}
_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);
}
_normalizeCurveSection(section, fromYUnit, toYUnit, fromPressureUnit, toPressureUnit, sectionName) {
const normalized = {};
for (const [pressureKey, pair] of Object.entries(section || {})) {
const canonicalPressure = this._convertUnitValue(
Number(pressureKey),
fromPressureUnit,
toPressureUnit,
`${sectionName} pressure axis`
);
const xArray = Array.isArray(pair?.x) ? pair.x.map(Number) : [];
const yArray = Array.isArray(pair?.y) ? pair.y.map((v) => this._convertUnitValue(v, fromYUnit, toYUnit, `${sectionName} output`)) : [];
if (!xArray.length || !yArray.length || xArray.length !== yArray.length) {
throw new Error(`Invalid ${sectionName} section at pressure '${pressureKey}'.`);
}
normalized[String(canonicalPressure)] = {
x: xArray,
y: yArray,
};
}
return normalized;
}
_normalizeMachineCurve(rawCurve, curveUnits = this.unitPolicy.curve) {
if (!rawCurve || typeof rawCurve !== 'object' || !rawCurve.nq || !rawCurve.np) {
throw new Error('Machine curve is missing required nq/np sections.');
}
return {
nq: this._normalizeCurveSection(
rawCurve.nq,
curveUnits.flow,
this.unitPolicy.canonical.flow,
curveUnits.pressure,
this.unitPolicy.canonical.pressure,
'nq'
),
np: this._normalizeCurveSection(
rawCurve.np,
curveUnits.power,
this.unitPolicy.canonical.power,
curveUnits.pressure,
this.unitPolicy.canonical.pressure,
'np'
),
};
}
isUnitValidForType(type, unit) {
return this.measurements?.isUnitCompatible?.(type, unit) === true;
}
_resolveMeasurementUnit(type, providedUnit) {
const unit = typeof providedUnit === 'string' ? providedUnit.trim() : '';
if (!unit) {
throw new Error(`Missing unit for ${type} measurement.`);
}
if (!this.isUnitValidForType(type, unit)) {
throw new Error(`Unsupported unit '${unit}' for ${type} measurement.`);
}
return unit;
}
_measurementPositionForMetric(metricId) {
if (metricId === "power") return "atEquipment";
return "downstream";
}
_resolveProcessRangeForMetric(metricId, predictedValue, measuredValue) {
let processMin = NaN;
let processMax = NaN;
if (metricId === "flow") {
processMin = Number(this.predictFlow?.currentFxyYMin);
processMax = Number(this.predictFlow?.currentFxyYMax);
} else if (metricId === "power") {
processMin = Number(this.predictPower?.currentFxyYMin);
processMax = Number(this.predictPower?.currentFxyYMax);
}
if (!Number.isFinite(processMin) || !Number.isFinite(processMax) || processMax <= processMin) {
const p = Number(predictedValue);
const m = Number(measuredValue);
const localMin = Math.min(p, m);
const localMax = Math.max(p, m);
processMin = Number.isFinite(localMin) ? localMin : 0;
processMax = Number.isFinite(localMax) && localMax > processMin ? localMax : processMin + 1;
}
return { processMin, processMax };
}
_updateMetricDrift(metricId, measuredValue, context = {}) {
const position = this._measurementPositionForMetric(metricId);
const predictedValue = Number(
this.measurements
.type(metricId)
.variant("predicted")
.position(position)
.getCurrentValue()
);
const measured = Number(measuredValue);
if (!Number.isFinite(predictedValue) || !Number.isFinite(measured)) return null;
const { processMin, processMax } = this._resolveProcessRangeForMetric(metricId, predictedValue, measured);
const timestamp = Number(context.timestamp || Date.now());
const profile = this.driftProfiles[metricId] || {};
try {
const drift = this.errorMetrics.assessPoint(metricId, predictedValue, measured, {
...profile,
processMin,
processMax,
predictedTimestamp: timestamp,
measuredTimestamp: timestamp,
});
if (drift && drift.valid) {
if (metricId === "flow") this.flowDrift = drift;
if (metricId === "power") this.powerDrift = drift;
}
return drift;
} catch (error) {
this.logger.warn(`Drift update failed for metric '${metricId}': ${error.message}`);
return null;
}
}
_updatePressureDriftStatus() {
const status = this.getPressureInitializationStatus();
const flags = [];
let level = 0;
if (!status.initialized) {
level = 2;
flags.push("no_pressure_input");
} else if (!status.hasDifferential) {
level = 1;
flags.push("single_side_pressure");
}
if (status.hasDifferential) {
const upstream = this._getPreferredPressureValue("upstream");
const downstream = this._getPreferredPressureValue("downstream");
const diff = Number(downstream) - Number(upstream);
if (Number.isFinite(diff) && diff < 0) {
level = Math.max(level, 3);
flags.push("negative_pressure_differential");
}
}
this.pressureDrift = {
level,
source: status.source,
flags: flags.length ? flags : ["nominal"],
};
return this.pressureDrift;
}
assessDrift(measurement, processMin, processMax) {
const metricId = String(measurement || "").toLowerCase();
const position = this._measurementPositionForMetric(metricId);
const predictedMeasurement = this.measurements.type(metricId).variant("predicted").position(position).getAllValues();
const measuredMeasurement = this.measurements.type(metricId).variant("measured").position(position).getAllValues();
if (!predictedMeasurement?.values || !measuredMeasurement?.values) return null;
return this.errorMetrics.assessDrift(
predictedMeasurement.values,
measuredMeasurement.values,
processMin,
processMax,
{
metricId,
predictedTimestamps: predictedMeasurement.timestamps,
measuredTimestamps: measuredMeasurement.timestamps,
...(this.driftProfiles[metricId] || {}),
}
);
}
_applyDriftPenalty(drift, confidence, flags, prefix) {
if (!drift || !drift.valid || !Number.isFinite(drift.nrmse)) return confidence;
if (drift.immediateLevel >= 3) {
confidence -= 0.3;
flags.push(`${prefix}_high_immediate_drift`);
} else if (drift.immediateLevel === 2) {
confidence -= 0.2;
flags.push(`${prefix}_medium_immediate_drift`);
} else if (drift.immediateLevel === 1) {
confidence -= 0.1;
flags.push(`${prefix}_low_immediate_drift`);
}
if (drift.longTermLevel >= 2) {
confidence -= 0.1;
flags.push(`${prefix}_long_term_drift`);
}
return confidence;
}
_updatePredictionHealth() {
const status = this.getPressureInitializationStatus();
const pressureDrift = this._updatePressureDriftStatus();
const flags = [...pressureDrift.flags];
let confidence = 0;
const pressureSource = status.source;
if (pressureSource === "differential") {
confidence = 0.9;
} else if (pressureSource === "upstream" || pressureSource === "downstream") {
confidence = 0.55;
} else {
confidence = 0.2;
}
if (!this._isOperationalState()) {
confidence = 0;
flags.push("not_operational");
}
if (pressureDrift.level >= 3) confidence -= 0.35;
else if (pressureDrift.level === 2) confidence -= 0.2;
else if (pressureDrift.level === 1) confidence -= 0.1;
const currentPosition = Number(this.state?.getCurrentPosition?.());
const { min, max } = this._resolveSetpointBounds();
if (Number.isFinite(currentPosition) && Number.isFinite(min) && Number.isFinite(max) && max > min) {
const span = max - min;
const edgeDistance = Math.min(Math.abs(currentPosition - min), Math.abs(max - currentPosition));
if (edgeDistance < span * 0.05) {
confidence -= 0.1;
flags.push("near_curve_edge");
}
}
confidence = this._applyDriftPenalty(this.flowDrift, confidence, flags, "flow");
confidence = this._applyDriftPenalty(this.powerDrift, confidence, flags, "power");
confidence = Math.max(0, Math.min(1, confidence));
let quality = "invalid";
if (confidence >= 0.8) quality = "high";
else if (confidence >= 0.55) quality = "medium";
else if (confidence >= 0.3) quality = "low";
this.predictionHealth = {
quality,
confidence,
pressureSource,
flags: flags.length ? Array.from(new Set(flags)) : ["nominal"],
};
return this.predictionHealth;
}
reverseCurve(curve) {
const reversedCurve = {};
for (const [pressure, values] of Object.entries(curve)) {
reversedCurve[pressure] = {
x: [...values.y], // Previous y becomes new x
y: [...values.x] // Previous x becomes new y
};
}
return reversedCurve;
}
// -------- Config -------- //
updateConfig(newConfig) {
this.config = this.configUtils.updateConfig(this.config, newConfig);
}
// -------- Mode and Input Management -------- //
isValidSourceForMode(source, mode) {
const allowedSourcesSet = this.config.mode.allowedSources[mode] || [];
const allowed = allowedSourcesSet.has(source);
allowed?
this.logger.debug(`source is allowed proceeding with ${source} for mode ${mode}`) :
this.logger.warn(`${source} is not allowed in mode ${mode}`);
return allowed;
}
isValidActionForMode(action, mode) {
const allowedActionsSet = this.config.mode.allowedActions[mode] || [];
const allowed = allowedActionsSet.has(action);
allowed ?
this.logger.debug(`Action is allowed proceeding with ${action} for mode ${mode}`) :
this.logger.warn(`${action} is not allowed in mode ${mode}`);
return allowed;
}
async handleInput(source, action, parameter) {
//sanitize input
if( typeof action !== 'string'){this.logger.error(`Action must be string`); return;}
//convert to lower case to avoid to many mistakes in commands
action = action.toLowerCase();
// check for validity of the request
if(!this.isValidActionForMode(action,this.currentMode)){return ;}
if (!this.isValidSourceForMode(source, this.currentMode)) {return ;}
this.logger.info(`Handling input from source '${source}' with action '${action}' in mode '${this.currentMode}'.`);
try {
switch (action) {
case "execsequence":
return await this.executeSequence(parameter);
case "execmovement":
return await this.setpoint(parameter);
case "entermaintenance":
return await this.executeSequence(parameter);
case "exitmaintenance":
return await this.executeSequence(parameter);
case "flowmovement":
// External flow setpoint is interpreted in configured output flow unit.
const canonicalFlowSetpoint = this._convertUnitValue(
parameter,
this.unitPolicy.output.flow,
this.unitPolicy.canonical.flow,
'flowmovement setpoint'
);
// Calculate the control value for a desired flow
const pos = this.calcCtrl(canonicalFlowSetpoint);
// Move to the desired setpoint
return await this.setpoint(pos);
case "emergencystop":
this.logger.warn(`Emergency stop activated by '${source}'.`);
return await this.executeSequence("emergencyStop");
case "statuscheck":
this.logger.info(`Status Check: Mode = '${this.currentMode}', Source = '${source}'.`);
break;
default:
this.logger.warn(`Action '${action}' is not implemented.`);
break;
}
this.logger.debug(`Action '${action}' successfully executed`);
return {status : true , feedback: `Action '${action}' successfully executed.`};
} catch (error) {
this.logger.error(`Error handling input: ${error}`);
}
}
abortMovement(reason = "group override") {
if (this.state?.abortCurrentMovement) {
this.state.abortCurrentMovement(reason);
}
}
setMode(newMode) {
const availableModes = this.defaultConfig.mode.current.rules.values.map(v => v.value);
if (!availableModes.includes(newMode)) {
this.logger.warn(`Invalid mode '${newMode}'. Allowed modes are: ${availableModes.join(', ')}`);
return;
}
this.currentMode = newMode;
this.logger.info(`Mode successfully changed to '${newMode}'.`);
}
// -------- Sequence Handlers -------- //
async executeSequence(sequenceName) {
const sequence = this.config.sequences[sequenceName];
if (!sequence || sequence.size === 0) {
this.logger.warn(`Sequence '${sequenceName}' not defined.`);
return;
}
if (this.state.getCurrentState() == "operational" && sequenceName == "shutdown") {
this.logger.info(`Machine will ramp down to position 0 before performing ${sequenceName} sequence`);
await this.setpoint(0);
}
this.logger.info(` --------- Executing sequence: ${sequenceName} -------------`);
for (const state of sequence) {
try {
await this.state.transitionToState(state);
// Update measurements after state change
} catch (error) {
this.logger.error(`Error during sequence '${sequenceName}': ${error}`);
break; // Exit sequence execution on error
}
}
//recalc flow and power
this.updatePosition();
}
async setpoint(setpoint) {
try {
// Validate and normalize setpoint
if (!Number.isFinite(setpoint)) {
this.logger.error("Invalid setpoint: Setpoint must be a finite number.");
return;
}
const { min, max } = this._resolveSetpointBounds();
const constrainedSetpoint = Math.min(Math.max(setpoint, min), max);
if (constrainedSetpoint !== setpoint) {
this.logger.warn(`Requested setpoint ${setpoint} constrained to ${constrainedSetpoint} (min=${min}, max=${max})`);
}
this.logger.info(`Setting setpoint to ${constrainedSetpoint}. Current position: ${this.state.getCurrentPosition()}`);
// Move to the desired setpoint
await this.state.moveTo(constrainedSetpoint);
} catch (error) {
this.logger.error(`Error setting setpoint: ${error}`);
}
}
_resolveSetpointBounds() {
const stateMin = Number(this.state?.movementManager?.minPosition);
const stateMax = Number(this.state?.movementManager?.maxPosition);
const curveMin = Number(this.predictFlow?.currentFxyXMin);
const curveMax = Number(this.predictFlow?.currentFxyXMax);
const minCandidates = [stateMin, curveMin].filter(Number.isFinite);
const maxCandidates = [stateMax, curveMax].filter(Number.isFinite);
const fallbackMin = Number.isFinite(stateMin) ? stateMin : 0;
const fallbackMax = Number.isFinite(stateMax) ? stateMax : 100;
let min = minCandidates.length ? Math.max(...minCandidates) : fallbackMin;
let max = maxCandidates.length ? Math.min(...maxCandidates) : fallbackMax;
if (min > max) {
this.logger.warn(`Invalid setpoint bounds detected (min=${min}, max=${max}). Falling back to movement bounds.`);
min = fallbackMin;
max = fallbackMax;
}
return { min, max };
}
// Calculate flow based on current pressure and position
calcFlow(x) {
if(this.hasCurve) {
if (!this._isOperationalState()) {
this.measurements.type("flow").variant("predicted").position("downstream").value(0,Date.now(),this.unitPolicy.canonical.flow);
this.measurements.type("flow").variant("predicted").position("atEquipment").value(0,Date.now(),this.unitPolicy.canonical.flow);
this.logger.debug(`Machine is not operational. Setting predicted flow to 0.`);
return 0;
}
const cFlow = this.predictFlow.y(x);
this.measurements.type("flow").variant("predicted").position("downstream").value(cFlow,Date.now(),this.unitPolicy.canonical.flow);
this.measurements.type("flow").variant("predicted").position("atEquipment").value(cFlow,Date.now(),this.unitPolicy.canonical.flow);
//this.logger.debug(`Calculated flow: ${cFlow} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
return cFlow;
}
// If no curve data is available, log a warning and return 0
this.logger.warn(`No curve data available for flow calculation. Returning 0.`);
this.measurements.type("flow").variant("predicted").position("downstream").value(0, Date.now(),this.unitPolicy.canonical.flow);
this.measurements.type("flow").variant("predicted").position("atEquipment").value(0, Date.now(),this.unitPolicy.canonical.flow);
return 0;
}
// Calculate power based on current pressure and position
calcPower(x) {
if(this.hasCurve) {
if (!this._isOperationalState()) {
this.measurements.type("power").variant("predicted").position('atEquipment').value(0, Date.now(), this.unitPolicy.canonical.power);
this.logger.debug(`Machine is not operational. Setting predicted power to 0.`);
return 0;
}
//this.predictPower.currentX = x; Decrepated
const cPower = this.predictPower.y(x);
this.measurements.type("power").variant("predicted").position('atEquipment').value(cPower, Date.now(), this.unitPolicy.canonical.power);
//this.logger.debug(`Calculated power: ${cPower} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
return cPower;
}
// If no curve data is available, log a warning and return 0
this.logger.warn(`No curve data available for power calculation. Returning 0.`);
this.measurements.type("power").variant("predicted").position('atEquipment').value(0, Date.now(), this.unitPolicy.canonical.power);
return 0;
}
// calculate the power consumption using only flow and pressure
inputFlowCalcPower(flow) {
if(this.hasCurve) {
this.predictCtrl.currentX = flow;
const cCtrl = this.predictCtrl.y(flow);
this.predictPower.currentX = cCtrl;
const cPower = this.predictPower.y(cCtrl);
return cPower;
}
// If no curve data is available, log a warning and return 0
this.logger.warn(`No curve data available for power calculation. Returning 0.`);
this.measurements.type("power").variant("predicted").position('atEquipment').value(0, Date.now(), this.unitPolicy.canonical.power);
return 0;
}
// Function to predict control value for a desired flow
calcCtrl(x) {
if(this.hasCurve) {
this.predictCtrl.currentX = x;
const cCtrl = this.predictCtrl.y(x);
this.measurements.type("ctrl").variant("predicted").position('atEquipment').value(cCtrl);
//this.logger.debug(`Calculated ctrl: ${cCtrl} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
return cCtrl;
}
// If no curve data is available, log a warning and return 0
this.logger.warn(`No curve data available for control calculation. Returning 0.`);
this.measurements.type("ctrl").variant("predicted").position('atEquipment').value(0, Date.now());
return 0;
}
// returns the best available pressure measurement to use in the prediction calculation
// this will be either the differential pressure, downstream or upstream pressure
getMeasuredPressure() {
if(this.hasCurve === false){
this.logger.error(`No valid curve available to calculate prediction using last known pressure`);
return 0;
}
const upstreamPressure = this._getPreferredPressureValue("upstream");
const downstreamPressure = this._getPreferredPressureValue("downstream");
// Both upstream & downstream => differential
if (upstreamPressure != null && downstreamPressure != null) {
const pressureDiffValue = downstreamPressure - upstreamPressure;
this.logger.debug(`Pressure differential: ${pressureDiffValue}`);
this.predictFlow.fDimension = pressureDiffValue;
this.predictPower.fDimension = pressureDiffValue;
this.predictCtrl.fDimension = pressureDiffValue;
//update the cog
const { cog, minEfficiency } = this.calcCog();
// calc efficiency
const efficiency = this.calcEfficiency(this.predictPower.outputY, this.predictFlow.outputY, "predicted");
//update the distance from peak
this.calcDistanceBEP(efficiency,cog,minEfficiency);
return pressureDiffValue;
}
// Only downstream => use it, warn that it's partial
if (downstreamPressure != null) {
this.logger.warn(`Using downstream pressure only for prediction: ${downstreamPressure} This is less acurate!!`);
this.predictFlow.fDimension = downstreamPressure;
this.predictPower.fDimension = downstreamPressure;
this.predictCtrl.fDimension = downstreamPressure;
//update the cog
const { cog, minEfficiency } = this.calcCog();
// calc efficiency
const efficiency = this.calcEfficiency(this.predictPower.outputY, this.predictFlow.outputY, "predicted");
//update the distance from peak
this.calcDistanceBEP(efficiency,cog,minEfficiency);
return downstreamPressure;
}
// Only upstream => use it, warn that it's partial
if (upstreamPressure != null) {
this.logger.warn(`Using upstream pressure only for prediction: ${upstreamPressure} This is less acurate!!`);
this.predictFlow.fDimension = upstreamPressure;
this.predictPower.fDimension = upstreamPressure;
this.predictCtrl.fDimension = upstreamPressure;
//update the cog
const { cog, minEfficiency } = this.calcCog();
// calc efficiency
const efficiency = this.calcEfficiency(this.predictPower.outputY, this.predictFlow.outputY, "predicted");
//update the distance from peak
this.calcDistanceBEP(efficiency,cog,minEfficiency);
return upstreamPressure;
}
this.logger.error(`No valid pressure measurements available to calculate prediction using last known pressure`);
//set default at 0 => lowest pressure possible
this.predictFlow.fDimension = 0;
this.predictPower.fDimension = 0;
this.predictCtrl.fDimension = 0;
//update the cog
const { cog, minEfficiency } = this.calcCog();
// calc efficiency
const efficiency = this.calcEfficiency(this.predictPower.outputY, this.predictFlow.outputY, "predicted");
//update the distance from peak
this.calcDistanceBEP(efficiency,cog,minEfficiency);
//place min and max flow capabilities in containerthis.predictFlow.currentFxyYMax - this.predictFlow.currentFxyYMin
this.measurements.type('flow').variant('predicted').position('max').value(this.predictFlow.currentFxyYMax, Date.now(), this.unitPolicy.canonical.flow);
this.measurements.type('flow').variant('predicted').position('min').value(this.predictFlow.currentFxyYMin, Date.now(), this.unitPolicy.canonical.flow);
return 0;
}
_getPreferredPressureValue(position) {
const realIds = Array.from(this.realPressureChildIds[position] || []);
for (const childId of realIds) {
const value = this.measurements
.type("pressure")
.variant("measured")
.position(position)
.child(childId)
.getCurrentValue();
if (value != null) return value;
}
const virtualId = this.virtualPressureChildIds[position];
if (virtualId) {
const simulatedValue = this.measurements
.type("pressure")
.variant("measured")
.position(position)
.child(virtualId)
.getCurrentValue();
if (simulatedValue != null) return simulatedValue;
}
return this.measurements
.type("pressure")
.variant("measured")
.position(position)
.getCurrentValue();
}
getPressureInitializationStatus() {
const upstreamPressure = this._getPreferredPressureValue("upstream");
const downstreamPressure = this._getPreferredPressureValue("downstream");
const hasUpstream = upstreamPressure != null;
const hasDownstream = downstreamPressure != null;
const hasDifferential = hasUpstream && hasDownstream;
return {
hasUpstream,
hasDownstream,
hasDifferential,
initialized: hasUpstream || hasDownstream || hasDifferential,
source: hasDifferential ? 'differential' : hasDownstream ? 'downstream' : hasUpstream ? 'upstream' : null,
};
}
updateSimulatedMeasurement(type, position, value, context = {}) {
const normalizedType = String(type || "").toLowerCase();
const normalizedPosition = String(position || "atEquipment").toLowerCase();
if (normalizedType !== "pressure") {
this._callMeasurementHandler(normalizedType, value, normalizedPosition, context);
return;
}
if (!this.virtualPressureChildIds[normalizedPosition]) {
this.logger.warn(`Unsupported simulated pressure position '${normalizedPosition}'`);
return;
}
const child = this.virtualPressureChildren[normalizedPosition];
if (!child?.measurements) {
this.logger.error(`Virtual pressure child '${normalizedPosition}' is missing`);
return;
}
let measurementUnit;
try {
measurementUnit = this._resolveMeasurementUnit('pressure', context.unit);
} catch (error) {
this.logger.warn(`Rejected simulated pressure measurement: ${error.message}`);
return;
}
child.measurements
.type("pressure")
.variant("measured")
.position(normalizedPosition)
.value(value, context.timestamp || Date.now(), measurementUnit);
}
handleMeasuredFlow() {
const flowDiff = this.measurements.type('flow').variant('measured').difference();
// If both are present
if (flowDiff != null) {
// In theory, mass flow in = mass flow out, so they should match or be close.
if (flowDiff.value < 0.001) {
// flows match within tolerance
this.logger.debug(`Flow match: ${flowDiff.value}`);
return flowDiff.value;
} else {
// Mismatch => decide how to handle. Maybe take the average?
// Or bail out with an error. Example: we bail out here.
this.logger.error(`Something wrong with down or upstream flow measurement. Bailing out!`);
return null;
}
}
// get
const upstreamFlow = this.measurements.type('flow').variant('measured').position('upstream').getCurrentValue();
// Only upstream => might still accept it, but warn
if (upstreamFlow != null) {
this.logger.warn(`Only upstream flow is present. Using it but results may be incomplete!`);
return upstreamFlow;
}
// get
const downstreamFlow = this.measurements.type('flow').variant('measured').position('downstream').getCurrentValue();
// Only downstream => might still accept it, but warn
if (downstreamFlow != null) {
this.logger.warn(`Only downstream flow is present. Using it but results may be incomplete!`);
return downstreamFlow;
}
// Neither => error
this.logger.error(`No upstream or downstream flow measurement. Bailing out!`);
return null;
}
handleMeasuredPower() {
const power = this.measurements.type("power").variant("measured").position("atEquipment").getCurrentValue();
// If your system calls it "upstream" or just a single "value", adjust accordingly
if (power != null) {
this.logger.debug(`Measured power: ${power}`);
return power;
} else {
this.logger.error(`No measured power found. Bailing out!`);
return null;
}
}
updateMeasuredTemperature(value, position, context = {}) {
this.logger.debug(`Temperature update: ${value} at ${position} from ${context.childName || 'child'} (${context.childId || 'unknown-id'})`);
let measurementUnit;
try {
measurementUnit = this._resolveMeasurementUnit('temperature', context.unit);
} catch (error) {
this.logger.warn(`Rejected temperature update: ${error.message}`);
return;
}
this.measurements.type("temperature").variant("measured").position(position || 'atEquipment').child(context.childId).value(value, context.timestamp, measurementUnit);
}
// context handler for pressure updates
updateMeasuredPressure(value, position, context = {}) {
this.logger.debug(`Pressure update: ${value} at ${position} from ${context.childName || 'child'} (${context.childId || 'unknown-id'})`);
let measurementUnit;
try {
measurementUnit = this._resolveMeasurementUnit('pressure', context.unit);
} catch (error) {
this.logger.warn(`Rejected pressure update: ${error.message}`);
return;
}
// Store in parent's measurement container
this.measurements.type("pressure").variant("measured").position(position).child(context.childId).value(value, context.timestamp, measurementUnit);
// Determine what kind of value to use as pressure (upstream , downstream or difference)
const pressure = this.getMeasuredPressure();
this.updatePosition();
this._updatePressureDriftStatus();
this._updatePredictionHealth();
this.logger.debug(`Using pressure: ${pressure} for calculations`);
}
// NEW: Flow handler
updateMeasuredFlow(value, position, context = {}) {
if (!this._isOperationalState()) {
this.logger.warn(`Machine not operational, skipping flow update from ${context.childName || 'unknown'}`);
return;
}
this.logger.debug(`Flow update: ${value} at ${position} from ${context.childName || 'child'}`);
let measurementUnit;
try {
measurementUnit = this._resolveMeasurementUnit('flow', context.unit);
} catch (error) {
this.logger.warn(`Rejected flow update: ${error.message}`);
return;
}
// Store in parent's measurement container
this.measurements.type("flow").variant("measured").position(position).child(context.childId).value(value, context.timestamp, measurementUnit);
// Update predicted flow if you have prediction capability
if (this.predictFlow) {
this.measurements.type("flow").variant("predicted").position("downstream").value(this.predictFlow.outputY || 0, Date.now(), this.unitPolicy.canonical.flow);
this.measurements.type("flow").variant("predicted").position("atEquipment").value(this.predictFlow.outputY || 0, Date.now(), this.unitPolicy.canonical.flow);
}
const measuredCanonical = this.measurements
.type("flow")
.variant("measured")
.position(position)
.getCurrentValue(this.unitPolicy.canonical.flow);
this._updateMetricDrift("flow", measuredCanonical, context);
this._updatePredictionHealth();
}
updateMeasuredPower(value, position, context = {}) {
if (!this._isOperationalState()) {
this.logger.warn(`Machine not operational, skipping power update from ${context.childName || 'unknown'}`);
return;
}
this.logger.debug(`Power update: ${value} at ${position} from ${context.childName || 'child'}`);
let measurementUnit;
try {
measurementUnit = this._resolveMeasurementUnit('power', context.unit);
} catch (error) {
this.logger.warn(`Rejected power update: ${error.message}`);
return;
}
this.measurements.type("power").variant("measured").position(position).child(context.childId).value(value, context.timestamp, measurementUnit);
if (this.predictPower) {
this.measurements.type("power").variant("predicted").position("atEquipment").value(this.predictPower.outputY || 0, Date.now(), this.unitPolicy.canonical.power);
}
const measuredCanonical = this.measurements
.type("power")
.variant("measured")
.position(position)
.getCurrentValue(this.unitPolicy.canonical.power);
this._updateMetricDrift("power", measuredCanonical, context);
this._updatePredictionHealth();
}
// Helper method for operational state check
_isOperationalState() {
const state = this.state.getCurrentState();
const activeStates = ["operational", "warmingup", "accelerating", "decelerating"];
this.logger.debug(`Checking operational state ${this.state.getCurrentState()} ? ${activeStates.includes(state)}`);
return activeStates.includes(state);
}
//what is the internal functions that need updating when something changes that has influence on this.
updatePosition() {
if (this._isOperationalState()) {
const currentPosition = this.state.getCurrentPosition();
// Update the predicted values based on the new position
const { cPower, cFlow } = this.calcFlowPower(currentPosition);
// Calc predicted efficiency
const efficiency = this.calcEfficiency(cPower, cFlow, "predicted");
//update the cog
const { cog, minEfficiency } = this.calcCog();
//update the distance from peak
this.calcDistanceBEP(efficiency,cog,minEfficiency);
}
this._updatePredictionHealth();
}
calcDistanceFromPeak(currentEfficiency,peakEfficiency){
return Math.abs(currentEfficiency - peakEfficiency);
}
calcRelativeDistanceFromPeak(currentEfficiency,maxEfficiency,minEfficiency){
let distance = 1;
if(currentEfficiency != null){
distance = this.interpolation.interpolate_lin_single_point(currentEfficiency,maxEfficiency, minEfficiency, 0, 1);
}
return distance;
}
showWorkingCurves() {
// Show the current curves for debugging
const { powerCurve, flowCurve } = this.getCurrentCurves();
return {
powerCurve: powerCurve,
flowCurve: flowCurve,
cog: this.cog,
cogIndex: this.cogIndex,
NCog: this.NCog,
minEfficiency: this.minEfficiency,
currentEfficiencyCurve: this.currentEfficiencyCurve,
absDistFromPeak: this.absDistFromPeak,
relDistFromPeak: this.relDistFromPeak
};
}
// Calculate the center of gravity for current pressure
calcCog() {
//fetch current curve data for power and flow
const { powerCurve, flowCurve } = this.getCurrentCurves();
const {efficiencyCurve, peak, peakIndex, minEfficiency } = this.calcEfficiencyCurve(powerCurve, flowCurve);
// Calculate the normalized center of gravity
const NCog = (flowCurve.y[peakIndex] - this.predictFlow.currentFxyYMin) / (this.predictFlow.currentFxyYMax - this.predictFlow.currentFxyYMin); //
//store in object for later retrieval
this.currentEfficiencyCurve = efficiencyCurve;
this.cog = peak;
this.cogIndex = peakIndex;
this.NCog = NCog;
this.minEfficiency = minEfficiency;
return { cog: peak, cogIndex: peakIndex, NCog: NCog, minEfficiency: minEfficiency };
}
calcEfficiencyCurve(powerCurve, flowCurve) {
const efficiencyCurve = [];
let peak = 0;
let peakIndex = 0;
let minEfficiency = 0;
// Calculate efficiency curve based on power and flow curves
powerCurve.y.forEach((power, index) => {
// Get flow for the current power
const flow = flowCurve.y[index];
// higher efficiency is better
efficiencyCurve.push( Math.round( ( flow / power ) * 100 ) / 100);
// Keep track of peak efficiency
peak = Math.max(peak, efficiencyCurve[index]);
peakIndex = peak == efficiencyCurve[index] ? index : peakIndex;
minEfficiency = Math.min(...efficiencyCurve);
});
return { efficiencyCurve, peak, peakIndex, minEfficiency };
}
//calc flow power based on pressure and current position
calcFlowPower(x) {
// Calculate flow and power
const cFlow = this.calcFlow(x);
const cPower = this.calcPower(x);
return { cPower, cFlow };
}
calcEfficiency(power,flow,variant) {
// Request a pressure differential explicitly in Pascal for hydraulic efficiency.
const pressureDiff = this.measurements
.type('pressure')
.variant('measured')
.difference({ unit: 'Pa' });
const g = gravity.getStandardGravity();
const temp = this.measurements.type('temperature').variant('measured').position('atEquipment').getCurrentValue('K');
const atmPressure = this.measurements.type('atmPressure').variant('measured').position('atEquipment').getCurrentValue('Pa');
let rho = null;
try {
rho = coolprop.PropsSI('D', 'T', temp, 'P', atmPressure, 'WasteWater');
} catch (error) {
// coolprop can throw transient initialization errors; keep machine calculations running.
this.logger.warn(`CoolProp density lookup failed: ${error.message}. Using fallback density.`);
rho = 1000; // kg/m3 fallback for water-like fluids
}
this.logger.debug(`temp: ${temp} atmPressure : ${atmPressure} rho : ${rho} pressureDiff: ${pressureDiff?.value || 0}`);
const flowM3s = this.measurements.type('flow').variant('predicted').position('atEquipment').getCurrentValue('m3/s');
const powerWatt = this.measurements.type('power').variant('predicted').position('atEquipment').getCurrentValue('W');
this.logger.debug(`Flow : ${flowM3s} power: ${powerWatt}`);
if (power != 0 && flow != 0) {
const specificFlow = flow / power;
const specificEnergyConsumption = power / flow;
this.measurements.type("efficiency").variant(variant).position('atEquipment').value(specificFlow);
this.measurements.type("specificEnergyConsumption").variant(variant).position('atEquipment').value(specificEnergyConsumption);
if (pressureDiff?.value != null && Number.isFinite(flowM3s) && Number.isFinite(powerWatt) && powerWatt > 0) {
// Engineering references: P_h = Q * Δp = ρ g Q H, η_h = P_h / P_in
const pressureDiffPa = Number(pressureDiff.value);
const headMeters = (Number.isFinite(rho) && rho > 0) ? pressureDiffPa / (rho * g) : null;
const hydraulicPowerW = pressureDiffPa * flowM3s;
const nHydraulicEfficiency = hydraulicPowerW / powerWatt;
if (Number.isFinite(headMeters)) {
this.measurements.type("pumpHead").variant(variant).position('atEquipment').value(headMeters, Date.now(), 'm');
}
this.measurements.type("hydraulicPower").variant(variant).position('atEquipment').value(hydraulicPowerW, Date.now(), 'W');
this.measurements.type("nHydraulicEfficiency").variant(variant).position('atEquipment').value(nHydraulicEfficiency);
}
}
//change this to nhydrefficiency ?
return this.measurements.type("efficiency").variant(variant).position('atEquipment').getCurrentValue();
}
updateCurve(newCurve) {
this.logger.info(`Updating machine curve`);
const normalizedCurve = this._normalizeMachineCurve(newCurve);
const newConfig = {
asset: {
machineCurve: normalizedCurve,
curveUnits: this.unitPolicy.curve,
},
};
//validate input of new curve fed to the machine
this.config = this.configUtils.updateConfig(this.config, newConfig);
//After we passed validation load the curves into their predictors
this.predictFlow.updateCurve(this.config.asset.machineCurve.nq);
this.predictPower.updateCurve(this.config.asset.machineCurve.np);
this.predictCtrl.updateCurve(this.reverseCurve(this.config.asset.machineCurve.nq));
}
getCompleteCurve() {
const powerCurve = this.predictPower.inputCurveData;
const flowCurve = this.predictFlow.inputCurveData;
return { powerCurve, flowCurve };
}
getCurrentCurves() {
const powerCurve = this.predictPower.currentFxyCurve[this.predictPower.currentF];
const flowCurve = this.predictFlow.currentFxyCurve[this.predictFlow.currentF];
return { powerCurve, flowCurve };
}
calcDistanceBEP(efficiency,maxEfficiency,minEfficiency) {
const absDistFromPeak = this.calcDistanceFromPeak(efficiency,maxEfficiency);
const relDistFromPeak = this.calcRelativeDistanceFromPeak(efficiency,maxEfficiency,minEfficiency);
//store internally
this.absDistFromPeak = absDistFromPeak ;
this.relDistFromPeak = relDistFromPeak;
return { absDistFromPeak: absDistFromPeak, relDistFromPeak: relDistFromPeak };
}
getOutput() {
// Improved output object generation
const output = this.measurements.getFlattenedOutput({
requestedUnits: this.unitPolicy.output,
});
//fill in the rest of the output object
output["state"] = this.state.getCurrentState();
output["runtime"] = this.state.getRunTimeHours();
output["ctrl"] = this.state.getCurrentPosition();
output["moveTimeleft"] = this.state.getMoveTimeLeft();
output["mode"] = this.currentMode;
output["cog"] = this.cog; // flow / power efficiency
output["NCog"] = this.NCog; // normalized cog
output["NCogPercent"] = Math.round(this.NCog * 100 * 100) / 100 ;
output["maintenanceTime"] = this.state.getMaintenanceTimeHours();
if(this.flowDrift != null){
const flowDrift = this.flowDrift;
output["flowNrmse"] = flowDrift.nrmse;
output["flowLongterNRMSD"] = flowDrift.longTermNRMSD;
output["flowLongTermNRMSD"] = flowDrift.longTermNRMSD;
output["flowImmediateLevel"] = flowDrift.immediateLevel;
output["flowLongTermLevel"] = flowDrift.longTermLevel;
output["flowDriftValid"] = flowDrift.valid;
}
if(this.powerDrift != null){
const powerDrift = this.powerDrift;
output["powerNrmse"] = powerDrift.nrmse;
output["powerLongTermNRMSD"] = powerDrift.longTermNRMSD;
output["powerImmediateLevel"] = powerDrift.immediateLevel;
output["powerLongTermLevel"] = powerDrift.longTermLevel;
output["powerDriftValid"] = powerDrift.valid;
}
output["pressureDriftLevel"] = this.pressureDrift.level;
output["pressureDriftSource"] = this.pressureDrift.source;
output["pressureDriftFlags"] = this.pressureDrift.flags;
output["predictionQuality"] = this.predictionHealth.quality;
output["predictionConfidence"] = Math.round(this.predictionHealth.confidence * 1000) / 1000;
output["predictionPressureSource"] = this.predictionHealth.pressureSource;
output["predictionFlags"] = this.predictionHealth.flags;
//should this all go in the container of measurements?
output["effDistFromPeak"] = this.absDistFromPeak;
output["effRelDistFromPeak"] = this.relDistFromPeak;
//this.logger.debug(`Output: ${JSON.stringify(output)}`);
return output;
}
} // end of class
module.exports = Machine;