347 lines
11 KiB
JavaScript
347 lines
11 KiB
JavaScript
/**
|
|
* Tests for reactor specificClass (domain logic).
|
|
*
|
|
* Two reactor classes are exported: Reactor_CSTR and Reactor_PFR.
|
|
* Both extend a base Reactor class.
|
|
*
|
|
* Key methods tested:
|
|
* - _calcOTR: oxygen transfer rate calculation
|
|
* - _arrayClip2Zero: clip negative values to zero
|
|
* - setInfluent / getEffluent: influent/effluent data flow
|
|
* - setOTR: external OTR override
|
|
* - tick (CSTR): forward Euler state update
|
|
* - tick (PFR): finite difference state update
|
|
* - registerChild: dispatches to measurement / reactor handlers
|
|
*/
|
|
|
|
const { Reactor_CSTR, Reactor_PFR } = require('../src/specificClass');
|
|
|
|
// --------------- helpers ---------------
|
|
|
|
const NUM_SPECIES = 13;
|
|
|
|
function makeCSTRConfig(overrides = {}) {
|
|
return {
|
|
general: {
|
|
name: 'TestCSTR',
|
|
id: 'cstr-test-1',
|
|
logging: { enabled: false, logLevel: 'error' },
|
|
},
|
|
functionality: {
|
|
softwareType: 'reactor',
|
|
positionVsParent: 'atEquipment',
|
|
},
|
|
volume: 1000,
|
|
n_inlets: 1,
|
|
kla: 240,
|
|
timeStep: 1, // 1 second
|
|
initialState: new Array(NUM_SPECIES).fill(1.0),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makePFRConfig(overrides = {}) {
|
|
return {
|
|
general: {
|
|
name: 'TestPFR',
|
|
id: 'pfr-test-1',
|
|
logging: { enabled: false, logLevel: 'error' },
|
|
},
|
|
functionality: {
|
|
softwareType: 'reactor',
|
|
positionVsParent: 'atEquipment',
|
|
},
|
|
volume: 200,
|
|
length: 10,
|
|
resolution_L: 10,
|
|
n_inlets: 1,
|
|
kla: 240,
|
|
alpha: 0.5,
|
|
timeStep: 1,
|
|
initialState: new Array(NUM_SPECIES).fill(0.1),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
// --------------- CSTR tests ---------------
|
|
|
|
describe('Reactor_CSTR', () => {
|
|
|
|
describe('constructor / initialization', () => {
|
|
it('should create an instance and set state from initialState', () => {
|
|
const r = new Reactor_CSTR(makeCSTRConfig());
|
|
expect(r).toBeDefined();
|
|
expect(r.state).toEqual(new Array(NUM_SPECIES).fill(1.0));
|
|
});
|
|
|
|
it('should initialize Fs and Cs_in arrays based on n_inlets', () => {
|
|
const r = new Reactor_CSTR(makeCSTRConfig({ n_inlets: 3 }));
|
|
expect(r.Fs).toHaveLength(3);
|
|
expect(r.Cs_in).toHaveLength(3);
|
|
expect(r.Fs.every(v => v === 0)).toBe(true);
|
|
});
|
|
|
|
it('should store volume from config', () => {
|
|
const r = new Reactor_CSTR(makeCSTRConfig({ volume: 500 }));
|
|
expect(r.volume).toBe(500);
|
|
});
|
|
|
|
it('should initialize temperature to 20', () => {
|
|
const r = new Reactor_CSTR(makeCSTRConfig());
|
|
expect(r.temperature).toBe(20);
|
|
});
|
|
});
|
|
|
|
describe('_calcOTR()', () => {
|
|
let r;
|
|
beforeAll(() => { r = new Reactor_CSTR(makeCSTRConfig({ kla: 240 })); });
|
|
|
|
it('should return a positive value when S_O < saturation', () => {
|
|
const otr = r._calcOTR(0, 20);
|
|
expect(otr).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should return approximately zero when S_O equals saturation', () => {
|
|
// S_O_sat at T=20: 14.652 - 4.1022e-1*20 + 7.9910e-3*400 + 7.7774e-5*8000
|
|
const T = 20;
|
|
const S_O_sat = 14.652 - 4.1022e-1 * T + 7.9910e-3 * T * T + 7.7774e-5 * T * T * T;
|
|
const otr = r._calcOTR(S_O_sat, T);
|
|
expect(otr).toBeCloseTo(0, 5);
|
|
});
|
|
|
|
it('should return a negative value when S_O > saturation (supersaturated)', () => {
|
|
const otr = r._calcOTR(100, 20);
|
|
expect(otr).toBeLessThan(0);
|
|
});
|
|
|
|
it('should use T=20 as default temperature', () => {
|
|
const otr1 = r._calcOTR(0);
|
|
const otr2 = r._calcOTR(0, 20);
|
|
expect(otr1).toBe(otr2);
|
|
});
|
|
});
|
|
|
|
describe('_arrayClip2Zero()', () => {
|
|
let r;
|
|
beforeAll(() => { r = new Reactor_CSTR(makeCSTRConfig()); });
|
|
|
|
it('should clip negative values to zero', () => {
|
|
expect(r._arrayClip2Zero([-5, 3, -1, 0, 7])).toEqual([0, 3, 0, 0, 7]);
|
|
});
|
|
|
|
it('should leave all-positive arrays unchanged', () => {
|
|
expect(r._arrayClip2Zero([1, 2, 3])).toEqual([1, 2, 3]);
|
|
});
|
|
|
|
it('should handle nested arrays (2D)', () => {
|
|
const result = r._arrayClip2Zero([[-1, 2], [3, -4]]);
|
|
expect(result).toEqual([[0, 2], [3, 0]]);
|
|
});
|
|
|
|
it('should handle a single scalar', () => {
|
|
expect(r._arrayClip2Zero(-5)).toBe(0);
|
|
expect(r._arrayClip2Zero(5)).toBe(5);
|
|
});
|
|
});
|
|
|
|
describe('setInfluent / getEffluent', () => {
|
|
it('should store influent data via setter', () => {
|
|
const r = new Reactor_CSTR(makeCSTRConfig({ n_inlets: 2 }));
|
|
const input = {
|
|
payload: {
|
|
inlet: 0,
|
|
F: 100,
|
|
C: new Array(NUM_SPECIES).fill(5),
|
|
},
|
|
};
|
|
r.setInfluent = input;
|
|
expect(r.Fs[0]).toBe(100);
|
|
expect(r.Cs_in[0]).toEqual(new Array(NUM_SPECIES).fill(5));
|
|
});
|
|
|
|
it('should return effluent with the sum of Fs and the current state', () => {
|
|
const r = new Reactor_CSTR(makeCSTRConfig());
|
|
r.Fs[0] = 50;
|
|
const eff = r.getEffluent;
|
|
expect(eff.topic).toBe('Fluent');
|
|
expect(eff.payload.F).toBe(50);
|
|
expect(eff.payload.C).toEqual(r.state);
|
|
});
|
|
});
|
|
|
|
describe('setOTR', () => {
|
|
it('should set the OTR value', () => {
|
|
const r = new Reactor_CSTR(makeCSTRConfig({ kla: NaN }));
|
|
r.setOTR = { payload: 42 };
|
|
expect(r.OTR).toBe(42);
|
|
});
|
|
});
|
|
|
|
describe('tick()', () => {
|
|
it('should return a new state array of correct length', () => {
|
|
const r = new Reactor_CSTR(makeCSTRConfig());
|
|
const result = r.tick(0.001);
|
|
expect(result).toHaveLength(NUM_SPECIES);
|
|
});
|
|
|
|
it('should not produce NaN values', () => {
|
|
const r = new Reactor_CSTR(makeCSTRConfig());
|
|
r.Fs[0] = 10;
|
|
r.Cs_in[0] = new Array(NUM_SPECIES).fill(5);
|
|
const result = r.tick(0.001);
|
|
result.forEach(v => expect(Number.isNaN(v)).toBe(false));
|
|
});
|
|
|
|
it('should not produce negative concentrations', () => {
|
|
const r = new Reactor_CSTR(makeCSTRConfig());
|
|
// Run multiple ticks
|
|
for (let i = 0; i < 100; i++) {
|
|
r.tick(0.001);
|
|
}
|
|
r.state.forEach(v => expect(v).toBeGreaterThanOrEqual(0));
|
|
});
|
|
|
|
it('should reach steady state with zero flow (concentrations change only via reaction)', () => {
|
|
const r = new Reactor_CSTR(makeCSTRConfig());
|
|
// No inflow
|
|
const initial = [...r.state];
|
|
r.tick(0.0001);
|
|
// State should have changed due to reaction/OTR
|
|
const changed = r.state.some((v, i) => v !== initial[i]);
|
|
expect(changed).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('registerChild()', () => {
|
|
it('should not throw for "measurement" software type', () => {
|
|
const r = new Reactor_CSTR(makeCSTRConfig());
|
|
// Passing null child will trigger warn but not crash
|
|
expect(() => r.registerChild(null, 'measurement')).not.toThrow();
|
|
});
|
|
|
|
it('should not throw for "reactor" software type', () => {
|
|
const r = new Reactor_CSTR(makeCSTRConfig());
|
|
expect(() => r.registerChild(null, 'reactor')).not.toThrow();
|
|
});
|
|
|
|
it('should not throw for unknown software type', () => {
|
|
const r = new Reactor_CSTR(makeCSTRConfig());
|
|
expect(() => r.registerChild(null, 'unknown')).not.toThrow();
|
|
});
|
|
});
|
|
});
|
|
|
|
// --------------- PFR tests ---------------
|
|
|
|
describe('Reactor_PFR', () => {
|
|
|
|
describe('constructor / initialization', () => {
|
|
it('should create an instance with 2D state grid', () => {
|
|
const r = new Reactor_PFR(makePFRConfig());
|
|
expect(r).toBeDefined();
|
|
expect(r.state).toHaveLength(10); // resolution_L = 10
|
|
expect(r.state[0]).toHaveLength(NUM_SPECIES);
|
|
});
|
|
|
|
it('should compute d_x = length / n_x', () => {
|
|
const r = new Reactor_PFR(makePFRConfig({ length: 10, resolution_L: 5 }));
|
|
expect(r.d_x).toBe(2);
|
|
});
|
|
|
|
it('should compute cross-sectional area A = volume / length', () => {
|
|
const r = new Reactor_PFR(makePFRConfig({ volume: 200, length: 10 }));
|
|
expect(r.A).toBe(20);
|
|
});
|
|
|
|
it('should initialize D (dispersion) to 0', () => {
|
|
const r = new Reactor_PFR(makePFRConfig());
|
|
expect(r.D).toBe(0);
|
|
});
|
|
|
|
it('should create derivative operators of correct size', () => {
|
|
const r = new Reactor_PFR(makePFRConfig({ resolution_L: 8 }));
|
|
expect(r.D_op).toHaveLength(8);
|
|
expect(r.D_op[0]).toHaveLength(8);
|
|
expect(r.D2_op).toHaveLength(8);
|
|
expect(r.D2_op[0]).toHaveLength(8);
|
|
});
|
|
});
|
|
|
|
describe('setDispersion', () => {
|
|
it('should set the axial dispersion value', () => {
|
|
const r = new Reactor_PFR(makePFRConfig());
|
|
r.setDispersion = { payload: 0.5 };
|
|
expect(r.D).toBe(0.5);
|
|
});
|
|
});
|
|
|
|
describe('tick()', () => {
|
|
it('should return a 2D state grid of correct dimensions', () => {
|
|
const r = new Reactor_PFR(makePFRConfig());
|
|
r.D = 0.01;
|
|
const result = r.tick(0.0001);
|
|
expect(result).toHaveLength(10);
|
|
expect(result[0]).toHaveLength(NUM_SPECIES);
|
|
});
|
|
|
|
it('should not produce NaN values with small time step and dispersion', () => {
|
|
const r = new Reactor_PFR(makePFRConfig());
|
|
r.D = 0.01;
|
|
r.Fs[0] = 10;
|
|
r.Cs_in[0] = new Array(NUM_SPECIES).fill(5);
|
|
const result = r.tick(0.0001);
|
|
result.forEach(row => {
|
|
row.forEach(v => expect(Number.isNaN(v)).toBe(false));
|
|
});
|
|
});
|
|
|
|
it('should not produce negative concentrations', () => {
|
|
const r = new Reactor_PFR(makePFRConfig());
|
|
r.D = 0.01;
|
|
for (let i = 0; i < 10; i++) {
|
|
r.tick(0.0001);
|
|
}
|
|
r.state.forEach(row => {
|
|
row.forEach(v => expect(v).toBeGreaterThanOrEqual(0));
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('_applyBoundaryConditions()', () => {
|
|
it('should apply Neumann BC at outlet (last = second to last)', () => {
|
|
const r = new Reactor_PFR(makePFRConfig({ resolution_L: 5 }));
|
|
const state = Array.from({ length: 5 }, () => new Array(NUM_SPECIES).fill(1));
|
|
state[3] = new Array(NUM_SPECIES).fill(7);
|
|
r._applyBoundaryConditions(state);
|
|
// outlet BC: state[4] = state[3]
|
|
expect(state[4]).toEqual(new Array(NUM_SPECIES).fill(7));
|
|
});
|
|
|
|
it('should apply Neumann BC at inlet when no flow', () => {
|
|
const r = new Reactor_PFR(makePFRConfig({ resolution_L: 5 }));
|
|
r.Fs[0] = 0;
|
|
const state = Array.from({ length: 5 }, () => new Array(NUM_SPECIES).fill(1));
|
|
state[1] = new Array(NUM_SPECIES).fill(3);
|
|
r._applyBoundaryConditions(state);
|
|
// No flow: state[0] = state[1]
|
|
expect(state[0]).toEqual(new Array(NUM_SPECIES).fill(3));
|
|
});
|
|
});
|
|
|
|
describe('_arrayClip2Zero() (inherited)', () => {
|
|
it('should clip 2D arrays correctly', () => {
|
|
const r = new Reactor_PFR(makePFRConfig());
|
|
const result = r._arrayClip2Zero([[-1, 2], [3, -4]]);
|
|
expect(result).toEqual([[0, 2], [3, 0]]);
|
|
});
|
|
});
|
|
|
|
describe('_calcOTR() (inherited)', () => {
|
|
it('should work the same as in CSTR', () => {
|
|
const r = new Reactor_PFR(makePFRConfig({ kla: 240 }));
|
|
const otr = r._calcOTR(0, 20);
|
|
expect(otr).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
});
|