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)`);
+});