handy-diffusion 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADI.js +264 -0
- package/CrankNicolson.js +181 -0
- package/README.md +1 -0
- package/analyticSolution.js +113 -0
- package/effective.js +64 -0
- package/helpers.js +48 -0
- package/index.js +0 -0
- package/initArrays.js +63 -0
- package/package.json +24 -0
- package/thomasAlgorithm.js +132 -0
package/ADI.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { thomasAlgorithm } from "./thomasAlgorithm.js";
|
|
2
|
+
import { initADIArrays } from "./initArrays.js";
|
|
3
|
+
|
|
4
|
+
let WIDTH;
|
|
5
|
+
let HEIGHT;
|
|
6
|
+
let modifiedUpperDiagonal1, modifiedRightHandSide1, solution1;
|
|
7
|
+
let modifiedUpperDiagonal2, modifiedRightHandSide2, solution2;
|
|
8
|
+
let intermediateConcentration;
|
|
9
|
+
let a1, b1, c1, d1;
|
|
10
|
+
let a2, b2, c2, d2;
|
|
11
|
+
let alpha, halfDeltaT, oneMinus2AlphaMinusGamma, scaledSources;
|
|
12
|
+
|
|
13
|
+
export const setADIProperties = (
|
|
14
|
+
width,
|
|
15
|
+
height,
|
|
16
|
+
diffusionCoefficient,
|
|
17
|
+
deltaX,
|
|
18
|
+
deltaT,
|
|
19
|
+
decayRate = 0
|
|
20
|
+
) => {
|
|
21
|
+
WIDTH = width;
|
|
22
|
+
HEIGHT = height;
|
|
23
|
+
({
|
|
24
|
+
modifiedUpperDiagonal1,
|
|
25
|
+
modifiedRightHandSide1,
|
|
26
|
+
solution1,
|
|
27
|
+
modifiedUpperDiagonal2,
|
|
28
|
+
modifiedRightHandSide2,
|
|
29
|
+
solution2,
|
|
30
|
+
intermediateConcentration,
|
|
31
|
+
a1,
|
|
32
|
+
b1,
|
|
33
|
+
c1,
|
|
34
|
+
d1,
|
|
35
|
+
a2,
|
|
36
|
+
b2,
|
|
37
|
+
c2,
|
|
38
|
+
d2,
|
|
39
|
+
alpha,
|
|
40
|
+
halfDeltaT,
|
|
41
|
+
oneMinus2AlphaMinusGamma,
|
|
42
|
+
scaledSources,
|
|
43
|
+
} = initADIArrays(WIDTH, HEIGHT, diffusionCoefficient, deltaX, deltaT, decayRate));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const ADI = (
|
|
47
|
+
concentrationData,
|
|
48
|
+
sources,
|
|
49
|
+
totalNumberOfIterations,
|
|
50
|
+
allowNegativeValues = false
|
|
51
|
+
) => {
|
|
52
|
+
let reachedNegativeValue = false;
|
|
53
|
+
|
|
54
|
+
for (let idx = 0; idx < WIDTH * HEIGHT; idx++) {
|
|
55
|
+
scaledSources[idx] = sources[idx] * halfDeltaT;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const currentConcentrationData = concentrationData;
|
|
59
|
+
|
|
60
|
+
for (let iteration = 0; iteration < totalNumberOfIterations; iteration++) {
|
|
61
|
+
/////////////----- FIRST HALF-STEP -----/////////////
|
|
62
|
+
|
|
63
|
+
// INTERIOR POINTS
|
|
64
|
+
for (let j = 1; j < HEIGHT - 1; j++) {
|
|
65
|
+
const rowOffset = j * WIDTH;
|
|
66
|
+
for (let i = 0; i < WIDTH; i++) {
|
|
67
|
+
const idx = rowOffset + i;
|
|
68
|
+
|
|
69
|
+
const center = currentConcentrationData[idx];
|
|
70
|
+
const bottom = currentConcentrationData[(j - 1) * WIDTH + i];
|
|
71
|
+
const top = currentConcentrationData[(j + 1) * WIDTH + i];
|
|
72
|
+
|
|
73
|
+
d1[i] =
|
|
74
|
+
alpha * bottom +
|
|
75
|
+
oneMinus2AlphaMinusGamma * center +
|
|
76
|
+
alpha * top +
|
|
77
|
+
scaledSources[idx];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
thomasAlgorithm(
|
|
81
|
+
a1,
|
|
82
|
+
b1,
|
|
83
|
+
c1,
|
|
84
|
+
d1,
|
|
85
|
+
WIDTH,
|
|
86
|
+
modifiedUpperDiagonal1,
|
|
87
|
+
modifiedRightHandSide1,
|
|
88
|
+
solution1
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < WIDTH; i++) {
|
|
92
|
+
intermediateConcentration[rowOffset + i] = solution1[i];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// BOTTOM POINTS j = 0
|
|
97
|
+
const rowOffsetBot = 0 * WIDTH;
|
|
98
|
+
for (let i = 0; i < WIDTH; i++) {
|
|
99
|
+
const idx = rowOffsetBot + i;
|
|
100
|
+
|
|
101
|
+
const center = currentConcentrationData[idx];
|
|
102
|
+
const bottom = center;
|
|
103
|
+
const top = currentConcentrationData[1 * WIDTH + i];
|
|
104
|
+
|
|
105
|
+
d1[i] =
|
|
106
|
+
alpha * bottom +
|
|
107
|
+
oneMinus2AlphaMinusGamma * center +
|
|
108
|
+
alpha * top +
|
|
109
|
+
scaledSources[idx];
|
|
110
|
+
}
|
|
111
|
+
thomasAlgorithm(
|
|
112
|
+
a1,
|
|
113
|
+
b1,
|
|
114
|
+
c1,
|
|
115
|
+
d1,
|
|
116
|
+
WIDTH,
|
|
117
|
+
modifiedUpperDiagonal1,
|
|
118
|
+
modifiedRightHandSide1,
|
|
119
|
+
solution1
|
|
120
|
+
);
|
|
121
|
+
for (let i = 0; i < WIDTH; i++) {
|
|
122
|
+
intermediateConcentration[rowOffsetBot + i] = solution1[i];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// TOP POINTS j = HEIGHT-1
|
|
126
|
+
const rowOffsetTop = (HEIGHT - 1) * WIDTH;
|
|
127
|
+
for (let i = 0; i < WIDTH; i++) {
|
|
128
|
+
const idx = rowOffsetTop + i;
|
|
129
|
+
|
|
130
|
+
const center = currentConcentrationData[idx];
|
|
131
|
+
const bottom = currentConcentrationData[(HEIGHT - 2) * WIDTH + i];
|
|
132
|
+
const top = center;
|
|
133
|
+
|
|
134
|
+
d1[i] =
|
|
135
|
+
alpha * bottom +
|
|
136
|
+
oneMinus2AlphaMinusGamma * center +
|
|
137
|
+
alpha * top +
|
|
138
|
+
scaledSources[idx];
|
|
139
|
+
}
|
|
140
|
+
thomasAlgorithm(
|
|
141
|
+
a1,
|
|
142
|
+
b1,
|
|
143
|
+
c1,
|
|
144
|
+
d1,
|
|
145
|
+
WIDTH,
|
|
146
|
+
modifiedUpperDiagonal1,
|
|
147
|
+
modifiedRightHandSide1,
|
|
148
|
+
solution1
|
|
149
|
+
);
|
|
150
|
+
for (let i = 0; i < WIDTH; i++) {
|
|
151
|
+
intermediateConcentration[rowOffsetTop + i] = solution1[i];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/////////////----- SECOND HALF-STEP -----/////////////
|
|
155
|
+
// INTERIOR POINTS
|
|
156
|
+
for (let i = 1; i < WIDTH - 1; i++) {
|
|
157
|
+
for (let j = 0; j < HEIGHT; j++) {
|
|
158
|
+
const rowOffset = j * WIDTH;
|
|
159
|
+
const idx = rowOffset + i;
|
|
160
|
+
|
|
161
|
+
const center = intermediateConcentration[idx];
|
|
162
|
+
const right = intermediateConcentration[rowOffset + (i + 1)];
|
|
163
|
+
const left = intermediateConcentration[rowOffset + (i - 1)];
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
d2[j] =
|
|
167
|
+
alpha * left +
|
|
168
|
+
oneMinus2AlphaMinusGamma * center +
|
|
169
|
+
alpha * right +
|
|
170
|
+
scaledSources[idx];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
thomasAlgorithm(
|
|
174
|
+
a2,
|
|
175
|
+
b2,
|
|
176
|
+
c2,
|
|
177
|
+
d2,
|
|
178
|
+
HEIGHT,
|
|
179
|
+
modifiedUpperDiagonal2,
|
|
180
|
+
modifiedRightHandSide2,
|
|
181
|
+
solution2
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
for (let j = 0; j < HEIGHT; j++) {
|
|
185
|
+
const pos = j * WIDTH + i;
|
|
186
|
+
if (solution2[j] < 0) {
|
|
187
|
+
reachedNegativeValue = true;
|
|
188
|
+
}
|
|
189
|
+
currentConcentrationData[pos] = solution2[j];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// LEFT POINTS i = 0
|
|
194
|
+
for (let j = 0; j < HEIGHT; j++) {
|
|
195
|
+
const rowOffset = j * WIDTH;
|
|
196
|
+
const idx = rowOffset;
|
|
197
|
+
|
|
198
|
+
const center = intermediateConcentration[idx];
|
|
199
|
+
const right = intermediateConcentration[j * WIDTH + 1];
|
|
200
|
+
const left = center;
|
|
201
|
+
|
|
202
|
+
d2[j] =
|
|
203
|
+
alpha * left +
|
|
204
|
+
oneMinus2AlphaMinusGamma * center +
|
|
205
|
+
alpha * right +
|
|
206
|
+
scaledSources[idx];
|
|
207
|
+
}
|
|
208
|
+
thomasAlgorithm(
|
|
209
|
+
a2,
|
|
210
|
+
b2,
|
|
211
|
+
c2,
|
|
212
|
+
d2,
|
|
213
|
+
HEIGHT,
|
|
214
|
+
modifiedUpperDiagonal2,
|
|
215
|
+
modifiedRightHandSide2,
|
|
216
|
+
solution2
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
for (let j = 0; j < HEIGHT; j++) {
|
|
220
|
+
if (solution2[j] < 0) {
|
|
221
|
+
reachedNegativeValue = true;
|
|
222
|
+
}
|
|
223
|
+
currentConcentrationData[j * WIDTH] = solution2[j];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// RIGHT POINTS i = WIDTH-1
|
|
227
|
+
for (let j = 0; j < HEIGHT; j++) {
|
|
228
|
+
const rowOffset = j * WIDTH;
|
|
229
|
+
const idx = rowOffset + (WIDTH - 1);
|
|
230
|
+
|
|
231
|
+
const center = intermediateConcentration[idx];
|
|
232
|
+
const right = center;
|
|
233
|
+
const left = intermediateConcentration[j * WIDTH + (WIDTH - 2)];
|
|
234
|
+
|
|
235
|
+
d2[j] =
|
|
236
|
+
alpha * left +
|
|
237
|
+
oneMinus2AlphaMinusGamma * center +
|
|
238
|
+
alpha * right +
|
|
239
|
+
scaledSources[idx];
|
|
240
|
+
}
|
|
241
|
+
thomasAlgorithm(
|
|
242
|
+
a2,
|
|
243
|
+
b2,
|
|
244
|
+
c2,
|
|
245
|
+
d2,
|
|
246
|
+
HEIGHT,
|
|
247
|
+
modifiedUpperDiagonal2,
|
|
248
|
+
modifiedRightHandSide2,
|
|
249
|
+
solution2
|
|
250
|
+
);
|
|
251
|
+
for (let j = 0; j < HEIGHT; j++) {
|
|
252
|
+
if (solution2[j] < 0) {
|
|
253
|
+
reachedNegativeValue = true;
|
|
254
|
+
}
|
|
255
|
+
currentConcentrationData[j * WIDTH + (WIDTH - 1)] = solution2[j];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (reachedNegativeValue && !allowNegativeValues) {
|
|
260
|
+
console.warn("Concentration went negative at ADI");
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
return currentConcentrationData;
|
|
264
|
+
};
|
package/CrankNicolson.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { thomasAlgorithm } from "./thomasAlgorithm.js";
|
|
2
|
+
|
|
3
|
+
// Module-level variables for Crank-Nicolson
|
|
4
|
+
let LENGTH;
|
|
5
|
+
let lowerDiagonal, mainDiagonal, upperDiagonal;
|
|
6
|
+
let modifiedUpper, modifiedRHS, solution;
|
|
7
|
+
let u, uNext;
|
|
8
|
+
let lambda, beta, halfLambda, centerCoeff;
|
|
9
|
+
let dt;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Initialize properties for Crank-Nicolson method
|
|
13
|
+
* @param {number} length - Number of grid points
|
|
14
|
+
* @param {number} diffusionCoefficient - Diffusion coefficient (alpha)
|
|
15
|
+
* @param {number} deltaX - Spatial step size
|
|
16
|
+
* @param {number} deltaT - Time step size
|
|
17
|
+
* @param {number} decayRate - Decay rate constant (k)
|
|
18
|
+
*/
|
|
19
|
+
export const setCNProperties = (
|
|
20
|
+
length,
|
|
21
|
+
diffusionCoefficient,
|
|
22
|
+
deltaX,
|
|
23
|
+
deltaT,
|
|
24
|
+
decayRate = 0
|
|
25
|
+
) => {
|
|
26
|
+
LENGTH = length;
|
|
27
|
+
dt = deltaT;
|
|
28
|
+
|
|
29
|
+
// Calculate coefficients
|
|
30
|
+
lambda = (diffusionCoefficient * deltaT) / (deltaX * deltaX);
|
|
31
|
+
beta = (decayRate * deltaT) / 2.0;
|
|
32
|
+
halfLambda = 0.5 * lambda;
|
|
33
|
+
centerCoeff = 1.0 + lambda + beta;
|
|
34
|
+
|
|
35
|
+
// Tridiagonal coefficients for A (left-hand side)
|
|
36
|
+
lowerDiagonal = new Float64Array(LENGTH);
|
|
37
|
+
mainDiagonal = new Float64Array(LENGTH);
|
|
38
|
+
upperDiagonal = new Float64Array(LENGTH);
|
|
39
|
+
|
|
40
|
+
// Interior coefficients
|
|
41
|
+
for (let i = 1; i < LENGTH - 1; i++) {
|
|
42
|
+
lowerDiagonal[i] = -halfLambda;
|
|
43
|
+
mainDiagonal[i] = centerCoeff;
|
|
44
|
+
upperDiagonal[i] = -halfLambda;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Neumann (reflective) boundaries using centered differences with ghost cells
|
|
48
|
+
// u_{-1} = u_1 and u_n = u_{n-2} for zero flux
|
|
49
|
+
// Left boundary i = 0: stencil becomes (1+λ+β)u_0 - λu_1
|
|
50
|
+
lowerDiagonal[0] = 0.0;
|
|
51
|
+
mainDiagonal[0] = centerCoeff;
|
|
52
|
+
upperDiagonal[0] = -lambda;
|
|
53
|
+
|
|
54
|
+
// Right boundary i = LENGTH-1: stencil becomes -λu_{n-2} + (1+λ+β)u_{n-1}
|
|
55
|
+
lowerDiagonal[LENGTH - 1] = -lambda;
|
|
56
|
+
mainDiagonal[LENGTH - 1] = centerCoeff;
|
|
57
|
+
upperDiagonal[LENGTH - 1] = 0.0;
|
|
58
|
+
|
|
59
|
+
// Work arrays for Thomas algorithm
|
|
60
|
+
modifiedUpper = new Float64Array(LENGTH);
|
|
61
|
+
modifiedRHS = new Float64Array(LENGTH);
|
|
62
|
+
solution = new Float64Array(LENGTH);
|
|
63
|
+
|
|
64
|
+
// Solution arrays
|
|
65
|
+
u = new Float64Array(LENGTH);
|
|
66
|
+
uNext = new Float64Array(LENGTH);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Solve 1D diffusion equation using Crank-Nicolson method
|
|
71
|
+
* @param {Float64Array} concentrationData - Initial concentration values
|
|
72
|
+
* @param {Float64Array} sources - Source terms
|
|
73
|
+
* @param {number} totalNumberOfIterations - Number of time steps
|
|
74
|
+
* @param {boolean} allowNegativeValues - Whether to allow negative concentrations
|
|
75
|
+
*/
|
|
76
|
+
export const CrankNicolson = (
|
|
77
|
+
concentrationData,
|
|
78
|
+
sources,
|
|
79
|
+
totalNumberOfIterations,
|
|
80
|
+
allowNegativeValues = false
|
|
81
|
+
) => {
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
// Copy initial condition
|
|
85
|
+
for (let i = 0; i < LENGTH; i++) {
|
|
86
|
+
u[i] = concentrationData[i];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Time-stepping loop
|
|
90
|
+
for (let step = 0; step < totalNumberOfIterations; step++) {
|
|
91
|
+
// Build right-hand side d = B*u^n + dt*S, with Neumann BC via mirroring.
|
|
92
|
+
const d = modifiedRHS;
|
|
93
|
+
|
|
94
|
+
// Left boundary i = 0, using ghost cell u_{-1} = u_1 for zero flux
|
|
95
|
+
// B coefficients: (λ/2) u_{i-1}^n + (1 - λ - β) u_i^n + (λ/2) u_{i+1}^n
|
|
96
|
+
// Substituting u_{-1} = u_1: (λ/2) u_1 + (1 - λ - β) u_0 + (λ/2) u_1
|
|
97
|
+
{
|
|
98
|
+
const i = 0;
|
|
99
|
+
const u_im1 = u[1]; // ghost cell: u_{-1} = u_1
|
|
100
|
+
const u_i = u[0];
|
|
101
|
+
const u_ip1 = u[1];
|
|
102
|
+
|
|
103
|
+
const rhsVal =
|
|
104
|
+
halfLambda * u_im1 +
|
|
105
|
+
(1.0 - lambda - beta) * u_i +
|
|
106
|
+
halfLambda * u_ip1 +
|
|
107
|
+
dt * sources[i];
|
|
108
|
+
|
|
109
|
+
d[i] = rhsVal;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Interior points 1..LENGTH-2
|
|
113
|
+
for (let i = 1; i < LENGTH - 1; i++) {
|
|
114
|
+
const u_im1 = u[i - 1];
|
|
115
|
+
const u_i = u[i];
|
|
116
|
+
const u_ip1 = u[i + 1];
|
|
117
|
+
|
|
118
|
+
const rhsVal =
|
|
119
|
+
halfLambda * u_im1 +
|
|
120
|
+
(1.0 - lambda - beta) * u_i +
|
|
121
|
+
halfLambda * u_ip1 +
|
|
122
|
+
dt * sources[i];
|
|
123
|
+
|
|
124
|
+
d[i] = rhsVal;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Right boundary i = LENGTH-1, using ghost cell u_n = u_{n-2} for zero flux
|
|
128
|
+
// Substituting u_n = u_{n-2}: (λ/2) u_{n-2} + (1 - λ - β) u_{n-1} + (λ/2) u_{n-2}
|
|
129
|
+
{
|
|
130
|
+
const i = LENGTH - 1;
|
|
131
|
+
const u_im1 = u[LENGTH - 2];
|
|
132
|
+
const u_i = u[LENGTH - 1];
|
|
133
|
+
const u_ip1 = u[LENGTH - 2]; // ghost cell: u_n = u_{n-2}
|
|
134
|
+
|
|
135
|
+
const rhsVal =
|
|
136
|
+
halfLambda * u_im1 +
|
|
137
|
+
(1.0 - lambda - beta) * u_i +
|
|
138
|
+
halfLambda * u_ip1 +
|
|
139
|
+
dt * sources[i];
|
|
140
|
+
|
|
141
|
+
d[i] = rhsVal;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Solve A u^{n+1} = d
|
|
145
|
+
thomasAlgorithm(
|
|
146
|
+
lowerDiagonal,
|
|
147
|
+
mainDiagonal,
|
|
148
|
+
upperDiagonal,
|
|
149
|
+
d,
|
|
150
|
+
LENGTH,
|
|
151
|
+
modifiedUpper,
|
|
152
|
+
modifiedRHS,
|
|
153
|
+
solution
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Copy solution and optionally check for negativity
|
|
157
|
+
let hasNegative = false;
|
|
158
|
+
for (let i = 0; i < LENGTH; i++) {
|
|
159
|
+
uNext[i] = solution[i];
|
|
160
|
+
if (!allowNegativeValues && uNext[i] < 0) {
|
|
161
|
+
hasNegative = true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (hasNegative && !allowNegativeValues) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
"Negative concentrations encountered in CrankNicolson step " + step
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Swap u and uNext without reallocating
|
|
172
|
+
const tmp = u;
|
|
173
|
+
u = uNext;
|
|
174
|
+
uNext = tmp;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Copy result back to concentrationData
|
|
178
|
+
for (let i = 0; i < LENGTH; i++) {
|
|
179
|
+
concentrationData[i] = u[i];
|
|
180
|
+
}
|
|
181
|
+
};
|
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# diffusionForBacteria
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
function constantSourceTermOptimized(n, m, Lx, Ly, sources, activeSourceIndices, cosX, cosY, WIDTH, deltaX) {
|
|
4
|
+
const e_n = n === 0 ? 0.5 : 1;
|
|
5
|
+
const e_m = m === 0 ? 0.5 : 1;
|
|
6
|
+
const coefficient = (4 * e_n * e_m * deltaX * deltaX) / (Lx * Ly);
|
|
7
|
+
let sum = 0;
|
|
8
|
+
|
|
9
|
+
for (const idx of activeSourceIndices) {
|
|
10
|
+
const i = idx % WIDTH;
|
|
11
|
+
const j = Math.floor(idx / WIDTH);
|
|
12
|
+
sum += sources[idx] * cosX[n][i] * cosY[m][j];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return coefficient * sum;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const analyticSteadyState = (
|
|
19
|
+
WIDTH,
|
|
20
|
+
HEIGHT,
|
|
21
|
+
DIFFUSION_RATE,
|
|
22
|
+
DECAY_RATE,
|
|
23
|
+
deltaX,
|
|
24
|
+
sources,
|
|
25
|
+
maxMode
|
|
26
|
+
) => {
|
|
27
|
+
const steadyStateConcentration = new Float64Array(WIDTH * HEIGHT).fill(0);
|
|
28
|
+
|
|
29
|
+
// Precompute non-zero source locations
|
|
30
|
+
const activeSourceIndices = [];
|
|
31
|
+
for (let idx = 0; idx < sources.length; idx++) {
|
|
32
|
+
if (sources[idx] !== 0) activeSourceIndices.push(idx);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Early return if no sources
|
|
36
|
+
if (activeSourceIndices.length === 0) {
|
|
37
|
+
return steadyStateConcentration;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const Lx = WIDTH * deltaX;
|
|
41
|
+
const Ly = HEIGHT * deltaX;
|
|
42
|
+
const piSquared = Math.PI * Math.PI;
|
|
43
|
+
const LxSquared = Lx * Lx;
|
|
44
|
+
const LySquared = Ly * Ly;
|
|
45
|
+
const invLxSquared = 1 / LxSquared;
|
|
46
|
+
const invLySquared = 1 / LySquared;
|
|
47
|
+
|
|
48
|
+
// Precompute all x and y coordinates
|
|
49
|
+
const xCoords = new Float64Array(WIDTH);
|
|
50
|
+
const yCoords = new Float64Array(HEIGHT);
|
|
51
|
+
for (let i = 0; i < WIDTH; i++) xCoords[i] = (i + 0.5) * deltaX;
|
|
52
|
+
for (let j = 0; j < HEIGHT; j++) yCoords[j] = (j + 0.5) * deltaX;
|
|
53
|
+
|
|
54
|
+
// Precompute cosine values for all modes and positions
|
|
55
|
+
const cosX = Array(maxMode + 1);
|
|
56
|
+
const cosY = Array(maxMode + 1);
|
|
57
|
+
|
|
58
|
+
for (let n = 0; n <= maxMode; n++) {
|
|
59
|
+
const nPi_Lx = (Math.PI * n) / Lx;
|
|
60
|
+
cosX[n] = new Float64Array(WIDTH);
|
|
61
|
+
for (let i = 0; i < WIDTH; i++) {
|
|
62
|
+
cosX[n][i] = Math.cos(nPi_Lx * xCoords[i]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (let m = 0; m <= maxMode; m++) {
|
|
67
|
+
const mPi_Ly = (Math.PI * m) / Ly;
|
|
68
|
+
cosY[m] = new Float64Array(HEIGHT);
|
|
69
|
+
for (let j = 0; j < HEIGHT; j++) {
|
|
70
|
+
cosY[m][j] = Math.cos(mPi_Ly * yCoords[j]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Precompute squared mode numbers
|
|
75
|
+
const nSquared = new Float64Array(maxMode + 1);
|
|
76
|
+
const mSquared = new Float64Array(maxMode + 1);
|
|
77
|
+
for (let n = 0; n <= maxMode; n++) nSquared[n] = n * n;
|
|
78
|
+
for (let m = 0; m <= maxMode; m++) mSquared[m] = m * m;
|
|
79
|
+
|
|
80
|
+
// Compute steady-state solution
|
|
81
|
+
for (let m = 0; m <= maxMode; m++) {
|
|
82
|
+
for (let n = 0; n <= maxMode; n++) {
|
|
83
|
+
const eigenvalue = piSquared * (nSquared[n] * invLxSquared + mSquared[m] * invLySquared);
|
|
84
|
+
const K_mn = DIFFUSION_RATE * eigenvalue + DECAY_RATE;
|
|
85
|
+
const Q_mn = constantSourceTermOptimized(
|
|
86
|
+
n,
|
|
87
|
+
m,
|
|
88
|
+
Lx,
|
|
89
|
+
Ly,
|
|
90
|
+
sources,
|
|
91
|
+
activeSourceIndices,
|
|
92
|
+
cosX,
|
|
93
|
+
cosY,
|
|
94
|
+
WIDTH,
|
|
95
|
+
deltaX
|
|
96
|
+
);
|
|
97
|
+
const amplitude = Q_mn / K_mn;
|
|
98
|
+
|
|
99
|
+
// Skip modes with negligible contribution
|
|
100
|
+
if (Math.abs(amplitude) < 1e-15) continue;
|
|
101
|
+
|
|
102
|
+
// Compute and accumulate eigenfunction values directly
|
|
103
|
+
for (let j = 0; j < HEIGHT; j++) {
|
|
104
|
+
const cosYval = cosY[m][j];
|
|
105
|
+
for (let i = 0; i < WIDTH; i++) {
|
|
106
|
+
steadyStateConcentration[j * WIDTH + i] += amplitude * cosX[n][i] * cosYval;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return steadyStateConcentration;
|
|
113
|
+
};
|
package/effective.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
|
|
2
|
+
export const efectiveInfluence = (width, height, sources, lambda, scale) => {
|
|
3
|
+
const effectiveInfluenceArray = new Float64Array(width * height).fill(0);
|
|
4
|
+
|
|
5
|
+
// Precompute active sources once
|
|
6
|
+
const activeSources = [];
|
|
7
|
+
for (let idx = 0; idx < sources.length; idx++) {
|
|
8
|
+
if (sources[idx] !== 0) {
|
|
9
|
+
activeSources.push({
|
|
10
|
+
idx,
|
|
11
|
+
x: (idx % width) + 0.5,
|
|
12
|
+
y: Math.floor(idx / width) + 0.5,
|
|
13
|
+
strength: sources[idx]
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Distance cutoff: beyond 5*lambda, exp(-5) ≈ 0.007 (negligible)
|
|
19
|
+
const cutoffDistance = lambda * 5;
|
|
20
|
+
const cutoffDistanceSq = cutoffDistance * cutoffDistance;
|
|
21
|
+
|
|
22
|
+
// For each target cell
|
|
23
|
+
for (let j = 0; j < height; j++) {
|
|
24
|
+
for (let i = 0; i < width; i++) {
|
|
25
|
+
const targetX = i + 0.5;
|
|
26
|
+
const targetY = j + 0.5;
|
|
27
|
+
|
|
28
|
+
let localInfluence = 0;
|
|
29
|
+
let totalInfluence = 0;
|
|
30
|
+
|
|
31
|
+
// Single pass through all cells
|
|
32
|
+
for (let jj = 0; jj < height; jj++) {
|
|
33
|
+
for (let ii = 0; ii < width; ii++) {
|
|
34
|
+
const cellX = ii + 0.5;
|
|
35
|
+
const cellY = jj + 0.5;
|
|
36
|
+
const dx = cellX - targetX;
|
|
37
|
+
const dy = cellY - targetY;
|
|
38
|
+
const distSq = dx * dx + dy * dy;
|
|
39
|
+
|
|
40
|
+
// Apply distance cutoff
|
|
41
|
+
if (distSq > cutoffDistanceSq) continue;
|
|
42
|
+
|
|
43
|
+
const distance = Math.sqrt(distSq);
|
|
44
|
+
const influence = Math.exp(-distance / lambda);
|
|
45
|
+
|
|
46
|
+
totalInfluence += influence;
|
|
47
|
+
|
|
48
|
+
// Check if this cell is a source
|
|
49
|
+
const cellIdx = jj * width + ii;
|
|
50
|
+
if (sources[cellIdx] !== 0) {
|
|
51
|
+
localInfluence += sources[cellIdx] * influence;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const idx = j * width + i;
|
|
57
|
+
effectiveInfluenceArray[idx] = totalInfluence > 0 ?
|
|
58
|
+
scale * localInfluence / totalInfluence : 0;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return effectiveInfluenceArray;
|
|
63
|
+
}
|
|
64
|
+
|
package/helpers.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
|
|
2
|
+
export const createRandomSources = (width, height, probability) => {
|
|
3
|
+
const sources = new Float64Array(width * height);
|
|
4
|
+
for (let j = 0; j < height; j++) {
|
|
5
|
+
for (let i = 0; i < width; i++) {
|
|
6
|
+
const idx = j * width + i;
|
|
7
|
+
sources[idx] = Math.random() < probability ? 1.0 : 0.0;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return sources;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const checkForSteadyState = (prev, current, tolerance = 1e-5) => {
|
|
14
|
+
let maxDiff = 0;
|
|
15
|
+
for (let i = 0; i < prev.length; i++) {
|
|
16
|
+
const diff = Math.abs(current[i] - prev[i]);
|
|
17
|
+
if (diff > maxDiff) {
|
|
18
|
+
maxDiff = diff;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return maxDiff < tolerance;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Convert 1D arrays to 2D matrices for Plotly
|
|
25
|
+
export const convertTo2D = (array, width, height) => {
|
|
26
|
+
const matrix = [];
|
|
27
|
+
for (let j = 0; j < height; j++) {
|
|
28
|
+
const row = [];
|
|
29
|
+
for (let i = 0; i < width; i++) {
|
|
30
|
+
row.push(array[j * width + i]);
|
|
31
|
+
}
|
|
32
|
+
matrix.push(row);
|
|
33
|
+
}
|
|
34
|
+
return matrix;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
// Compute and display difference
|
|
39
|
+
export const calculateDifference = (grid1, grid2) => {
|
|
40
|
+
const difference = new Float64Array(grid1.length);
|
|
41
|
+
for (let i = 0; i < grid1.length; i++) {
|
|
42
|
+
difference[i] = Math.abs(grid1[i] - grid2[i]);
|
|
43
|
+
}
|
|
44
|
+
return difference;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
package/index.js
ADDED
|
File without changes
|
package/initArrays.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Utility functions for ADI method and diagonals generation
|
|
2
|
+
|
|
3
|
+
const generateDiagonals = (length, alpha, gamma) => {
|
|
4
|
+
const lowerDiagonal = new Float64Array(length).fill(-alpha);
|
|
5
|
+
const mainDiagonal = new Float64Array(length).fill(1 + 2 * alpha + gamma);
|
|
6
|
+
const upperDiagonal = new Float64Array(length).fill(-alpha);
|
|
7
|
+
const rightHandSide = new Float64Array(length);
|
|
8
|
+
mainDiagonal[0] = 1 + alpha + gamma;
|
|
9
|
+
mainDiagonal[length - 1] = 1 + alpha + gamma;
|
|
10
|
+
lowerDiagonal[0] = 0;
|
|
11
|
+
upperDiagonal[length - 1] = 0;
|
|
12
|
+
return { lowerDiagonal, mainDiagonal, upperDiagonal, rightHandSide };
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const initADIArrays = (WIDTH, HEIGHT, DIFFUSION_RATE, deltaX, deltaT, decayRate) => {
|
|
16
|
+
const modifiedUpperDiagonal1 = new Float64Array(WIDTH);
|
|
17
|
+
const modifiedRightHandSide1 = new Float64Array(WIDTH);
|
|
18
|
+
const solution1 = new Float64Array(WIDTH);
|
|
19
|
+
const modifiedUpperDiagonal2 = new Float64Array(HEIGHT);
|
|
20
|
+
const modifiedRightHandSide2 = new Float64Array(HEIGHT);
|
|
21
|
+
const solution2 = new Float64Array(HEIGHT);
|
|
22
|
+
const intermediateConcentration = new Float64Array(WIDTH * HEIGHT);
|
|
23
|
+
const scaledSources = new Float64Array(WIDTH * HEIGHT);
|
|
24
|
+
|
|
25
|
+
const alpha = (DIFFUSION_RATE * deltaT) / (2 * deltaX * deltaX);
|
|
26
|
+
const gamma = (decayRate * deltaT) / 4;
|
|
27
|
+
const {
|
|
28
|
+
lowerDiagonal: a1,
|
|
29
|
+
mainDiagonal: b1,
|
|
30
|
+
upperDiagonal: c1,
|
|
31
|
+
rightHandSide: d1,
|
|
32
|
+
} = generateDiagonals(WIDTH, alpha, gamma);
|
|
33
|
+
const {
|
|
34
|
+
lowerDiagonal: a2,
|
|
35
|
+
mainDiagonal: b2,
|
|
36
|
+
upperDiagonal: c2,
|
|
37
|
+
rightHandSide: d2,
|
|
38
|
+
} = generateDiagonals(HEIGHT, alpha, gamma);
|
|
39
|
+
const halfDeltaT = deltaT / 2;
|
|
40
|
+
const oneMinus2AlphaMinusGamma = 1 - 2 * alpha - gamma;
|
|
41
|
+
return {
|
|
42
|
+
modifiedUpperDiagonal1,
|
|
43
|
+
modifiedRightHandSide1,
|
|
44
|
+
solution1,
|
|
45
|
+
modifiedUpperDiagonal2,
|
|
46
|
+
modifiedRightHandSide2,
|
|
47
|
+
solution2,
|
|
48
|
+
intermediateConcentration,
|
|
49
|
+
a1,
|
|
50
|
+
b1,
|
|
51
|
+
c1,
|
|
52
|
+
d1,
|
|
53
|
+
a2,
|
|
54
|
+
b2,
|
|
55
|
+
c2,
|
|
56
|
+
d2,
|
|
57
|
+
alpha,
|
|
58
|
+
halfDeltaT,
|
|
59
|
+
oneMinus2AlphaMinusGamma,
|
|
60
|
+
scaledSources,
|
|
61
|
+
deltaT,
|
|
62
|
+
};
|
|
63
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "handy-diffusion",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "algorithms to simulate diffusion",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/CritalMediumBlue/diffusionForBacteria.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"diffusion",
|
|
15
|
+
"ADI",
|
|
16
|
+
"Crank-Nicolson"
|
|
17
|
+
],
|
|
18
|
+
"author": "Ricardo",
|
|
19
|
+
"license": "ISC",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/CritalMediumBlue/diffusionForBacteria/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/CritalMediumBlue/diffusionForBacteria#readme"
|
|
24
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Numerical tolerance for pivot detection to prevent division by zero.
|
|
3
|
+
* MATLAB and other solver documentation recommend tolerances around 1e-10.
|
|
4
|
+
* @constant {number}
|
|
5
|
+
*/
|
|
6
|
+
const tolerance = 1e-10;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Solves a tridiagonal system of linear equations using the Thomas algorithm.
|
|
10
|
+
*
|
|
11
|
+
* The Thomas algorithm is a specialized form of Gaussian elimination for tridiagonal matrices
|
|
12
|
+
* that runs in O(n) time complexity. It's particularly useful for solving systems arising from
|
|
13
|
+
* finite difference discretizations of differential equations, such as diffusion equations.
|
|
14
|
+
*
|
|
15
|
+
* The algorithm solves systems of the form:
|
|
16
|
+
* ```
|
|
17
|
+
* [b₀ c₀ 0 0 ...] [x₀] [d₀]
|
|
18
|
+
* [a₁ b₁ c₁ 0 ...] [x₁] [d₁]
|
|
19
|
+
* [ 0 a₂ b₂ c₂ ...] [x₂] = [d₂]
|
|
20
|
+
* [ 0 0 a₃ b₃ ...] [x₃] [d₃]
|
|
21
|
+
* [... ] [..] [..]
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @param {Float64Array|Array<number>} lowerDiagonal - Lower diagonal elements (a₁, a₂, ..., aₙ₋₁).
|
|
25
|
+
* First element (index 0) is ignored as it doesn't exist.
|
|
26
|
+
* @param {Float64Array|Array<number>} mainDiagonal - Main diagonal elements (b₀, b₁, ..., bₙ₋₁).
|
|
27
|
+
* @param {Float64Array|Array<number>} upperDiagonal - Upper diagonal elements (c₀, c₁, ..., cₙ₋₂).
|
|
28
|
+
* Last element (index n-1) is ignored as it doesn't exist.
|
|
29
|
+
* @param {Float64Array|Array<number>} rightHandSide - Right-hand side vector (d₀, d₁, ..., dₙ₋₁).
|
|
30
|
+
* @param {number} n - Size of the system (number of equations/unknowns).
|
|
31
|
+
* @param {Float64Array|Array<number>} modifiedUpperDiagonal - Pre-allocated array to store modified upper diagonal
|
|
32
|
+
* during forward elimination. Must have length n.
|
|
33
|
+
* @param {Float64Array|Array<number>} modifiedRightHandSide - Pre-allocated array to store modified right-hand side
|
|
34
|
+
* during forward elimination. Must have length n.
|
|
35
|
+
* @param {Float64Array|Array<number>} solution - Pre-allocated array where the solution vector will be stored.
|
|
36
|
+
* Must have length n. Contains (x₀, x₁, ..., xₙ₋₁) after execution.
|
|
37
|
+
*
|
|
38
|
+
* @returns {void} The function modifies the `solution` array in-place.
|
|
39
|
+
*
|
|
40
|
+
* @note The algorithm handles near-singular matrices by replacing pivots smaller than
|
|
41
|
+
* 1e-10 with the tolerance value to prevent division by zero. For truly singular matrices,
|
|
42
|
+
* results may be numerically unstable.
|
|
43
|
+
*
|
|
44
|
+
* @complexity Time: O(n), Space: O(1) additional (uses pre-allocated arrays)
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // Solve a simple 3x3 tridiagonal system
|
|
48
|
+
* const n = 3;
|
|
49
|
+
* const lower = [0, 1, 1]; // First element unused
|
|
50
|
+
* const main = [2, 2, 2];
|
|
51
|
+
* const upper = [1, 1, 0]; // Last element unused
|
|
52
|
+
* const rhs = [3, 4, 3];
|
|
53
|
+
*
|
|
54
|
+
* // Pre-allocate working arrays
|
|
55
|
+
* const modUpper = new Float64Array(n);
|
|
56
|
+
* const modRHS = new Float64Array(n);
|
|
57
|
+
* const solution = new Float64Array(n);
|
|
58
|
+
*
|
|
59
|
+
* thomasAlgorithm(lower, main, upper, rhs, n, modUpper, modRHS, solution);
|
|
60
|
+
* console.log(solution); // [1, 1, 1]
|
|
61
|
+
*
|
|
62
|
+
* @see {@link ADI} Uses this algorithm for implicit solving
|
|
63
|
+
* @see {@link initADIArrays} Pre-allocates working arrays
|
|
64
|
+
*
|
|
65
|
+
*/
|
|
66
|
+
export function thomasAlgorithm(
|
|
67
|
+
lowerDiagonal,
|
|
68
|
+
mainDiagonal,
|
|
69
|
+
upperDiagonal,
|
|
70
|
+
rightHandSide,
|
|
71
|
+
n,
|
|
72
|
+
modifiedUpperDiagonal,
|
|
73
|
+
modifiedRightHandSide,
|
|
74
|
+
solution
|
|
75
|
+
) {
|
|
76
|
+
// =====================================================================
|
|
77
|
+
// FORWARD ELIMINATION PHASE
|
|
78
|
+
// =====================================================================
|
|
79
|
+
// Transform the tridiagonal matrix to upper triangular form by eliminating
|
|
80
|
+
// the lower diagonal elements. This modifies the upper diagonal and RHS.
|
|
81
|
+
|
|
82
|
+
// Handle the first row (i = 0)
|
|
83
|
+
// Check for near-zero pivot to prevent numerical instability
|
|
84
|
+
let pivot = mainDiagonal[0];
|
|
85
|
+
if (Math.abs(pivot) < tolerance) {
|
|
86
|
+
// Replace near-zero pivot with tolerance to avoid division by zero
|
|
87
|
+
pivot = pivot >= 0 ? tolerance : -tolerance;
|
|
88
|
+
}
|
|
89
|
+
const invPivot = 1.0 / pivot;
|
|
90
|
+
|
|
91
|
+
// Normalize first row: divide upper diagonal and RHS by pivot
|
|
92
|
+
modifiedUpperDiagonal[0] = upperDiagonal[0] * invPivot;
|
|
93
|
+
modifiedRightHandSide[0] = rightHandSide[0] * invPivot;
|
|
94
|
+
|
|
95
|
+
// Process remaining rows (i = 1 to n-1)
|
|
96
|
+
for (let i = 1; i < n; i++) {
|
|
97
|
+
const l_i = lowerDiagonal[i]; // Lower diagonal element at row i
|
|
98
|
+
const u_prime_prev = modifiedUpperDiagonal[i - 1]; // Modified upper diagonal from previous row
|
|
99
|
+
const d_prime_prev = modifiedRightHandSide[i - 1]; // Modified RHS from previous row
|
|
100
|
+
|
|
101
|
+
// Calculate the new diagonal element after eliminating lower diagonal
|
|
102
|
+
// This is: b_i - a_i * c'_{i-1}
|
|
103
|
+
let currentDenominator = mainDiagonal[i] - l_i * u_prime_prev;
|
|
104
|
+
|
|
105
|
+
// Check for near-zero denominator to prevent numerical instability
|
|
106
|
+
if (Math.abs(currentDenominator) < tolerance) {
|
|
107
|
+
// Replace near-zero denominator with tolerance to avoid division by zero
|
|
108
|
+
currentDenominator = currentDenominator >= 0 ? tolerance : -tolerance;
|
|
109
|
+
}
|
|
110
|
+
const invDenominator = 1.0 / currentDenominator;
|
|
111
|
+
|
|
112
|
+
// Update modified arrays for current row
|
|
113
|
+
// Modified upper diagonal: c_i / (b_i - a_i * c'_{i-1})
|
|
114
|
+
modifiedUpperDiagonal[i] = upperDiagonal[i] * invDenominator;
|
|
115
|
+
// Modified RHS: (d_i - a_i * d'_{i-1}) / (b_i - a_i * c'_{i-1})
|
|
116
|
+
modifiedRightHandSide[i] = (rightHandSide[i] - l_i * d_prime_prev) * invDenominator;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// =====================================================================
|
|
120
|
+
// BACKWARD SUBSTITUTION PHASE
|
|
121
|
+
// =====================================================================
|
|
122
|
+
// Solve the upper triangular system by working backwards from the last equation.
|
|
123
|
+
|
|
124
|
+
// Last equation is already solved: x_{n-1} = d'_{n-1}
|
|
125
|
+
solution[n - 1] = modifiedRightHandSide[n - 1];
|
|
126
|
+
|
|
127
|
+
// Solve remaining equations working backwards
|
|
128
|
+
for (let i = n - 2; i >= 0; i--) {
|
|
129
|
+
// x_i = d'_i - c'_i * x_{i+1}
|
|
130
|
+
solution[i] = modifiedRightHandSide[i] - modifiedUpperDiagonal[i] * solution[i + 1];
|
|
131
|
+
}
|
|
132
|
+
}
|