2 * Grid and spectral layout definitions, following SHTNS conventions:
3 *
4 * - Spectral coefficients Q_lm are complex, stored for m >= 0 only (real
5 * fields), interleaved [re, im], with SHTNS "m-major" ordering:
6 * for m = 0..mmax: for l = m..lmax. Index of (l, m) is lm(l, m).
7 * - Spatial fields are real, phi-contiguous: spat[ilat * nphi + iphi],
8 * with ilat ordered by increasing colatitude theta (north to south)
9 * and iphi covering [0, 2*pi) uniformly.
10 * - Normalization: orthonormal spherical harmonics INCLUDING the
11 * Condon-Shortley phase (SHTNS default: sht_orthonormal).
12 * A real field is f = sum_{l,m>=0} Q_lm Y_lm + c.c.(m>0), i.e.
13 * Q_{l,-m} = (-1)^m conj(Q_lm) is implied. m=0 coefficients must
14 * have zero imaginary part.
15 */
17export interface ShtConfig {
18 lmax: number;
19 mmax: number;
20 nlat: number;
21 nphi: number;
22}
24export function nlmCalc(lmax: number, mmax: number): number {
25 // sum over m=0..mmax of (lmax - m + 1)
26 return (mmax + 1) * (lmax + 1) - (mmax * (mmax + 1)) / 2;
27}
29/** Index of coefficient (l, m) in the spectral array (SHTNS LM ordering). */
30export function lmIndex(lmax: number, l: number, m: number): number {
31 return m * (lmax + 1) - (m * (m - 1)) / 2 + (l - m);
32}
34export function validateConfig(cfg: ShtConfig): void {
35 const { lmax, mmax, nlat, nphi } = cfg;
36 if (!Number.isInteger(lmax) || lmax < 1) throw new Error(`lmax must be an integer >= 1 (got ${lmax})`);
37 if (!Number.isInteger(mmax) || mmax < 0 || mmax > lmax)
38 throw new Error(`mmax must be an integer in [0, lmax] (got ${mmax})`);
39 if (!Number.isInteger(nlat) || nlat <= lmax)
40 throw new Error(`nlat must be an integer > lmax for exact Gauss quadrature (got nlat=${nlat}, lmax=${lmax})`);
41 if (!Number.isInteger(nphi) || nphi < 2 * mmax + 1)
42 throw new Error(`nphi must be an integer >= 2*mmax+1 to avoid aliasing (got nphi=${nphi}, mmax=${mmax})`);
43}
45export function isPowerOfTwo(n: number): boolean {
46 return n > 0 && (n & (n - 1)) === 0;
47}