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 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
+ };
@@ -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
+ }