diff --git a/reactor.html b/reactor.html index a591479..b34d2ec 100644 --- a/reactor.html +++ b/reactor.html @@ -39,6 +39,7 @@ X_TS_init: { value: 125.0009, required: true }, timeStep: { value: 1, required: true }, + speedUpFactor: { value: 1 }, enableLog: { value: false }, logLevel: { value: "error" }, @@ -115,6 +116,10 @@ type:"num", types:["num"] }) + $("#node-input-speedUpFactor").typedInput({ + type:"num", + types:["num"] + }) // Set initial visibility on dialog open const initialType = $("#node-input-reactor_type").typedInput("value"); if (initialType === "CSTR") { @@ -243,6 +248,10 @@ +
+ + +
diff --git a/src/nodeClass.js b/src/nodeClass.js index 62b8b66..d03fa43 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -112,7 +112,8 @@ class nodeClass { parseFloat(uiConfig.X_A_init), parseFloat(uiConfig.X_TS_init) ], - timeStep: parseFloat(uiConfig.timeStep) + timeStep: parseFloat(uiConfig.timeStep), + speedUpFactor: Number(uiConfig.speedUpFactor) || 1 } } @@ -159,6 +160,10 @@ class nodeClass { } _tick(){ + const gridProfile = this.source.getGridProfile; + if (gridProfile) { + this.node.send([{ topic: "GridProfile", payload: gridProfile }, null, null]); + } this.node.send([this.source.getEffluent, null, null]); } diff --git a/src/specificClass.js b/src/specificClass.js index e8d71ae..5e16745 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -41,7 +41,7 @@ class Reactor { this.currentTime = Date.now(); // milliseconds since epoch [ms] this.timeStep = 1 / (24*60*60) * this.config.timeStep; // time step in seconds, converted to days. - this.speedUpFactor = 60; // speed up factor for simulation, 60 means 1 minute per simulated second + this.speedUpFactor = config.speedUpFactor ?? 1; // speed up factor for simulation } /** @@ -91,6 +91,8 @@ class Reactor { return { topic: "Fluent", payload: { inlet: 0, F: math.sum(this.Fs), C: this.state }, timestamp: this.currentTime }; } + get getGridProfile() { return null; } + /** * Calculate the oxygen transfer rate (OTR) based on the dissolved oxygen concentration and temperature. * @param {number} S_O - Dissolved oxygen concentration [g O2 m-3]. @@ -275,6 +277,18 @@ class Reactor_PFR extends Reactor { assertNoNaN(this.D2_op, "Second derivative operator"); } + get getGridProfile() { + return { + grid: this.state.map(row => row.slice()), + n_x: this.n_x, + d_x: this.d_x, + length: this.length, + species: ['S_O','S_I','S_S','S_NH','S_N2','S_NO','S_HCO', + 'X_I','X_S','X_H','X_STO','X_A','X_TS'], + timestamp: this.currentTime + }; + } + /** * Setter for axial dispersion. * @param {object} input - Input object (msg) containing payload with dispersion value [m2 d-1]. diff --git a/test/basic/grid-profile.basic.test.js b/test/basic/grid-profile.basic.test.js new file mode 100644 index 0000000..4efbfcd --- /dev/null +++ b/test/basic/grid-profile.basic.test.js @@ -0,0 +1,45 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const { Reactor_CSTR, Reactor_PFR } = require('../../src/specificClass'); +const { makeReactorConfig } = require('../helpers/factories'); + +test('CSTR getGridProfile returns null', () => { + const reactor = new Reactor_CSTR(makeReactorConfig({ reactor_type: 'CSTR' })); + assert.equal(reactor.getGridProfile, null); +}); + +test('PFR getGridProfile returns state matrix with correct dimensions', () => { + const n_x = 8; + const length = 40; + const reactor = new Reactor_PFR( + makeReactorConfig({ reactor_type: 'PFR', resolution_L: n_x, length }), + ); + + const profile = reactor.getGridProfile; + assert.notEqual(profile, null); + assert.equal(profile.n_x, n_x); + assert.equal(profile.d_x, length / n_x); + assert.equal(profile.length, length); + assert.equal(profile.grid.length, n_x, 'grid should have n_x rows'); + assert.equal(profile.grid[0].length, 13, 'each row should have 13 species'); + assert.ok(Array.isArray(profile.species), 'species list should be an array'); + assert.equal(profile.species.length, 13); + assert.equal(profile.species[3], 'S_NH'); + assert.equal(typeof profile.timestamp, 'number'); +}); + +test('PFR getGridProfile is mutation-safe', () => { + const reactor = new Reactor_PFR( + makeReactorConfig({ reactor_type: 'PFR', resolution_L: 5, length: 10 }), + ); + + const profile = reactor.getGridProfile; + const originalValue = reactor.state[0][3]; // S_NH at cell 0 + + // Mutate the returned grid + profile.grid[0][3] = 999; + + // Reactor internal state should be unchanged + assert.equal(reactor.state[0][3], originalValue, 'mutating grid copy must not affect reactor state'); +}); diff --git a/test/basic/speedup-factor.basic.test.js b/test/basic/speedup-factor.basic.test.js new file mode 100644 index 0000000..f7a7c93 --- /dev/null +++ b/test/basic/speedup-factor.basic.test.js @@ -0,0 +1,68 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const { Reactor_CSTR } = require('../../src/specificClass'); +const nodeClass = require('../../src/nodeClass'); +const { makeReactorConfig, makeUiConfig, makeNodeStub, makeREDStub } = require('../helpers/factories'); + +/** + * Smoke tests for Fix 3: configurable speedUpFactor on Reactor. + */ + +test('specificClass defaults speedUpFactor to 1 when not in config', () => { + const config = makeReactorConfig(); + const reactor = new Reactor_CSTR(config); + assert.equal(reactor.speedUpFactor, 1, 'speedUpFactor should default to 1'); +}); + +test('specificClass accepts speedUpFactor from config', () => { + const config = makeReactorConfig(); + config.speedUpFactor = 10; + const reactor = new Reactor_CSTR(config); + assert.equal(reactor.speedUpFactor, 10, 'speedUpFactor should be read from config'); +}); + +test('specificClass accepts speedUpFactor = 60 for accelerated simulation', () => { + const config = makeReactorConfig(); + config.speedUpFactor = 60; + const reactor = new Reactor_CSTR(config); + assert.equal(reactor.speedUpFactor, 60, 'speedUpFactor=60 should be accepted'); +}); + +test('nodeClass passes speedUpFactor from uiConfig to reactor config', () => { + const uiConfig = makeUiConfig({ speedUpFactor: 5 }); + const node = makeNodeStub(); + const RED = makeREDStub(); + + const nc = new nodeClass(uiConfig, RED, node, 'test-reactor'); + assert.equal(nc.source.speedUpFactor, 5, 'nodeClass should pass speedUpFactor=5 to specificClass'); +}); + +test('nodeClass defaults speedUpFactor to 1 when not in uiConfig', () => { + const uiConfig = makeUiConfig(); + // Ensure speedUpFactor is not set + delete uiConfig.speedUpFactor; + + const node = makeNodeStub(); + const RED = makeREDStub(); + + const nc = new nodeClass(uiConfig, RED, node, 'test-reactor'); + assert.equal(nc.source.speedUpFactor, 1, 'nodeClass should default speedUpFactor to 1'); +}); + +test('updateState with speedUpFactor=1 advances roughly real-time', () => { + const config = makeReactorConfig(); + config.speedUpFactor = 1; + config.n_inlets = 1; + const reactor = new Reactor_CSTR(config); + + // Set a known start time + const t0 = reactor.currentTime; + // Advance by 2 seconds real time + reactor.updateState(t0 + 2000); + + // With speedUpFactor=1, simulation should have advanced ~2 seconds worth + // (not 120 seconds like with the old hardcoded 60x factor) + const elapsed = reactor.currentTime - t0; + assert.ok(elapsed < 5000, `Elapsed ${elapsed}ms should be close to 2000ms, not 120000ms (old 60x factor)`); +});