111 lines
3.5 KiB
JavaScript
111 lines
3.5 KiB
JavaScript
// Shared climate utilities: smoothing, ITCZ lookup, and percentile selection.
|
|
|
|
// ── Laplacian smoothing ──────────────────────────────────────────────────────
|
|
|
|
export function smoothField(mesh, field, passes) {
|
|
const { adjOffset, adjList, numRegions } = mesh;
|
|
const tmp = new Float32Array(numRegions);
|
|
let src = field, dst = tmp;
|
|
|
|
for (let pass = 0; pass < passes; pass++) {
|
|
for (let r = 0; r < numRegions; r++) {
|
|
let sum = src[r];
|
|
let count = 1;
|
|
const end = adjOffset[r + 1];
|
|
for (let ni = adjOffset[r]; ni < end; ni++) {
|
|
sum += src[adjList[ni]];
|
|
count++;
|
|
}
|
|
dst[r] = sum / count;
|
|
}
|
|
const swap = src; src = dst; dst = swap;
|
|
}
|
|
// If result ended up in tmp, copy back to field
|
|
if (src !== field) field.set(src);
|
|
}
|
|
|
|
// ── ITCZ latitude lookup (linear interpolation with wrapping) ────────────────
|
|
|
|
export function makeItczLookup(itczLons, itczLats) {
|
|
const n = itczLons.length;
|
|
const step = (2 * Math.PI) / n;
|
|
const lonStart = -Math.PI + step * 0.5;
|
|
|
|
return function (lon) {
|
|
let fi = (lon - lonStart) / step;
|
|
fi = ((fi % n) + n) % n;
|
|
const i0 = Math.floor(fi);
|
|
const i1 = (i0 + 1) % n;
|
|
const frac = fi - i0;
|
|
return itczLats[i0] * (1 - frac) + itczLats[i1] * frac;
|
|
};
|
|
}
|
|
|
|
// ── Floyd-Rivest selection (O(N) expected percentile) ────────────────────────
|
|
|
|
function floydRivest(arr, left, right, k) {
|
|
while (right > left) {
|
|
if (right - left > 600) {
|
|
const n = right - left + 1;
|
|
const i = k - left + 1;
|
|
const z = Math.log(n);
|
|
const s = 0.5 * Math.exp(2 * z / 3);
|
|
const sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (i - n / 2 < 0 ? -1 : 1);
|
|
const newLeft = Math.max(left, Math.floor(k - i * s / n + sd));
|
|
const newRight = Math.min(right, Math.floor(k + (n - i) * s / n + sd));
|
|
floydRivest(arr, newLeft, newRight, k);
|
|
}
|
|
|
|
const t = arr[k];
|
|
if (t !== t) return; // NaN pivot — cannot partition, bail out
|
|
let i = left;
|
|
let j = right;
|
|
|
|
arr[k] = arr[left];
|
|
arr[left] = t;
|
|
|
|
if (arr[right] > t) {
|
|
arr[left] = arr[right];
|
|
arr[right] = t;
|
|
}
|
|
|
|
while (i < j) {
|
|
const tmp = arr[i];
|
|
arr[i] = arr[j];
|
|
arr[j] = tmp;
|
|
i++;
|
|
j--;
|
|
while (arr[i] < t) i++;
|
|
while (arr[j] > t) j--;
|
|
}
|
|
|
|
if (arr[left] === t) {
|
|
const tmp = arr[left];
|
|
arr[left] = arr[j];
|
|
arr[j] = tmp;
|
|
} else {
|
|
j++;
|
|
const tmp = arr[j];
|
|
arr[j] = arr[right];
|
|
arr[right] = tmp;
|
|
}
|
|
|
|
if (j <= k) left = j + 1;
|
|
if (k <= j) right = j - 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compute the p-th percentile of a numeric array in O(N) expected time.
|
|
* Returns the value at index floor(n * p) of the sorted order.
|
|
* Makes a copy so the input is not mutated. Returns 1 if the result is 0.
|
|
*/
|
|
export function percentile(arr, p) {
|
|
const n = arr.length;
|
|
if (n === 0) return 1;
|
|
const work = new Float32Array(arr);
|
|
const k = Math.floor(n * p);
|
|
floydRivest(work, 0, n - 1, k);
|
|
return work[k] || 1;
|
|
}
|