Refactor of reactor to use BaseNodeAdapter + commandRegistry + statusBadge. reactor follows the platform refactor plan in .claude/refactor/MODULE_SPLIT.md. Tests stay green; CONTRACT.md generated; legacy aliases preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
140 lines
5.0 KiB
JavaScript
140 lines
5.0 KiB
JavaScript
'use strict';
|
|
|
|
const EventEmitter = require('events');
|
|
const ASM3 = require('../reaction_modules/asm3_class.js');
|
|
const { create, all } = require('mathjs');
|
|
const { childRegistrationUtils, logger, MeasurementContainer, POSITIONS } = require('generalFunctions');
|
|
|
|
const math = create(all, { matrix: 'Array' });
|
|
|
|
const S_O_INDEX = 0;
|
|
const NUM_SPECIES = 13;
|
|
|
|
// Abstract reactor engine. Holds the influent/OTR/temperature state plus
|
|
// the parent-side child registration that the original Reactor class
|
|
// exposed. Concrete CSTR / PFR subclasses provide tick().
|
|
class BaseReactorEngine {
|
|
constructor(config) {
|
|
this.config = config;
|
|
this.logger = new logger(
|
|
this.config.general.logging.enabled,
|
|
this.config.general.logging.logLevel,
|
|
config.general.name,
|
|
);
|
|
this.emitter = new EventEmitter();
|
|
this.measurements = new MeasurementContainer();
|
|
this.upstreamReactor = null;
|
|
this.childRegistrationUtils = new childRegistrationUtils(this);
|
|
|
|
this.asm = new ASM3();
|
|
this.volume = config.volume;
|
|
|
|
this.Fs = Array(config.n_inlets).fill(0);
|
|
this.Cs_in = Array.from(Array(config.n_inlets), () => new Array(NUM_SPECIES).fill(0));
|
|
this.OTR = 0.0;
|
|
this.temperature = 20;
|
|
|
|
this.kla = config.kla;
|
|
this.currentTime = Date.now();
|
|
// timeStep stored in days (the integrator uses [d] internally).
|
|
this.timeStep = (1 / (24 * 60 * 60)) * this.config.timeStep;
|
|
this.speedUpFactor = config.speedUpFactor ?? 1;
|
|
}
|
|
|
|
set setInfluent(input) {
|
|
const index_in = input.payload.inlet;
|
|
this.Fs[index_in] = input.payload.F;
|
|
this.Cs_in[index_in] = input.payload.C;
|
|
}
|
|
|
|
set setOTR(input) { this.OTR = input.payload; }
|
|
|
|
set setTemperature(input) {
|
|
const p = input?.payload;
|
|
const raw = (p && typeof p === 'object' && p.value !== undefined) ? p.value : p;
|
|
const v = Number(raw);
|
|
if (!Number.isFinite(v)) { this.logger.warn(`Invalid temperature input: ${raw}`); return; }
|
|
this.temperature = v;
|
|
}
|
|
|
|
get getEffluent() {
|
|
const last = Array.isArray(this.state.at?.(-1)) ? this.state.at(-1) : this.state;
|
|
return { topic: 'Fluent', payload: { inlet: 0, F: math.sum(this.Fs), C: last }, timestamp: this.currentTime };
|
|
}
|
|
|
|
get getGridProfile() { return null; }
|
|
|
|
_calcOTR(S_O, T = 20.0) {
|
|
const sat = this._calcOxygenSaturation(T);
|
|
return this.kla * (sat - S_O);
|
|
}
|
|
|
|
_calcOxygenSaturation(T = 20.0) {
|
|
return 14.652 - 4.1022e-1 * T + 7.9910e-3 * T * T + 7.7774e-5 * T * T * T;
|
|
}
|
|
|
|
_capDissolvedOxygen(state) {
|
|
const sat = this._calcOxygenSaturation(this.temperature);
|
|
const capRow = (row) => {
|
|
if (!Array.isArray(row)) return row;
|
|
const next = row.slice();
|
|
if (Number.isFinite(next[S_O_INDEX])) next[S_O_INDEX] = Math.max(0, Math.min(next[S_O_INDEX], sat));
|
|
return next;
|
|
};
|
|
return (Array.isArray(state) && Array.isArray(state[0])) ? state.map(capRow) : capRow(state);
|
|
}
|
|
|
|
_arrayClip2Zero(arr) {
|
|
if (Array.isArray(arr)) return arr.map((x) => this._arrayClip2Zero(x));
|
|
return arr < 0 ? 0 : arr;
|
|
}
|
|
|
|
registerChild(child, softwareType) {
|
|
switch (softwareType) {
|
|
case 'measurement': this._connectMeasurement(child); break;
|
|
case 'reactor': this._connectReactor(child); break;
|
|
default: this.logger.error(`Unrecognized softwareType: ${softwareType}`);
|
|
}
|
|
}
|
|
|
|
_connectMeasurement(measurement) {
|
|
if (!measurement) { this.logger.warn('Invalid measurement provided.'); return; }
|
|
const fn = measurement.config.functionality;
|
|
const position = fn.distance !== 'undefined' ? fn.distance : fn.positionVsParent;
|
|
const measurementType = measurement.config.asset.type;
|
|
const eventName = `${measurementType}.measured.${position}`;
|
|
measurement.measurements.emitter.on(eventName, (eventData) => {
|
|
this.measurements
|
|
.type(measurementType).variant('measured').position(position)
|
|
.value(eventData.value, eventData.timestamp, eventData.unit);
|
|
this._updateMeasurement(measurementType, eventData.value, position, eventData);
|
|
});
|
|
}
|
|
|
|
_connectReactor(reactor) {
|
|
if (!reactor) { this.logger.warn('Invalid reactor provided.'); return; }
|
|
this.upstreamReactor = reactor;
|
|
reactor.emitter.on('stateChange', (data) => this.updateState(data));
|
|
}
|
|
|
|
_updateMeasurement(measurementType, value, position) {
|
|
if (measurementType === 'temperature' && position === POSITIONS.AT_EQUIPMENT) {
|
|
this.temperature = value;
|
|
return;
|
|
}
|
|
this.logger.error(`Type '${measurementType}' not recognized for measured update.`);
|
|
}
|
|
|
|
updateState(newTime = Date.now()) {
|
|
const day2ms = 1000 * 60 * 60 * 24;
|
|
if (this.upstreamReactor) this.setInfluent = this.upstreamReactor.getEffluent;
|
|
const n_iter = Math.floor(this.speedUpFactor * (newTime - this.currentTime) / (this.timeStep * day2ms));
|
|
if (!n_iter) return;
|
|
for (let n = 0; n < n_iter; n += 1) this.tick(this.timeStep);
|
|
this.currentTime += (n_iter * this.timeStep * day2ms) / this.speedUpFactor;
|
|
this.emitter.emit('stateChange', this.currentTime);
|
|
}
|
|
}
|
|
|
|
module.exports = { BaseReactorEngine, math, S_O_INDEX, NUM_SPECIES };
|