Skip to content

Commit 60772d7

Browse files
committed
added NFMT module and tests
1 parent ac88125 commit 60772d7

5 files changed

Lines changed: 320 additions & 28 deletions

File tree

src/pyGroupedTransforms/GroupedTransform.py

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,7 @@ def get_superposition_set(d, ds):
3535
return [tuple(item) for item in returnset]
3636

3737

38-
def get_setting(
39-
system, N, U=None, d=None, ds=None
40-
): # I have to add bases and NFMT stuff here!
38+
def get_setting(system, N, U=None, d=None, ds=None, basis_vect=None):
4139

4240
if system not in systems:
4341
raise ValueError("System not found.")
@@ -52,18 +50,18 @@ def get_setting(
5250
bwl = [np.full(len(u), tmp[len(u)], "int32") for u in U]
5351

5452
if systems[system] == NFMTtools:
55-
if len(basis_vect) == 0:
56-
ValueError(
53+
if basis_vect is None or len(basis_vect) == 0:
54+
raise ValueError(
5755
"please call get_setting with basis_vect for a NFMT transform."
5856
)
5957
if len(basis_vect) < d:
60-
ValueError("basis_vect must have an entry for every dimension.")
58+
raise ValueError("basis_vect must have an entry for every dimension.")
6159
return [
6260
Setting(
6361
u=U[idx],
6462
mode=systems[system],
6563
bandwidths=np.array(bwl[idx], "int32"),
66-
bases=basis_vect[U[idk]],
64+
bases=[basis_vect[j] for j in U[idx]],
6765
)
6866
for idx in range(len(U))
6967
]
@@ -92,18 +90,18 @@ def get_setting(
9290
bwl[i] = np.full(len(u), N[i])
9391

9492
if systems[system] == NFMTtools:
95-
if len(basis_vect) == 0:
96-
ValueError(
93+
if basis_vect is None or len(basis_vect) == 0:
94+
raise ValueError(
9795
"please call get_setting with basis_vect for a NFMT transform."
9896
)
99-
if len(basis_vect) < max(max(u) for u in U):
100-
ValueError("basis_vect must have an entry for every dimension.")
97+
if len(basis_vect) <= max(max(u) for u in U if len(u) > 0):
98+
raise ValueError("basis_vect must have an entry for every dimension.")
10199
return [
102100
Setting(
103101
u=u,
104102
mode=systems[system],
105103
bandwidths=np.array(bwl[i], "int32"),
106-
bases=basis_vect[u],
104+
bases=[basis_vect[j] for j in u],
107105
)
108106
for i, u in enumerate(U)
109107
]
@@ -131,18 +129,18 @@ def get_setting(
131129
bwl[i] = N[i]
132130

133131
if systems[system] == NFMTtools:
134-
if len(basis_vect) == 0:
135-
ValueError(
132+
if basis_vect is None or len(basis_vect) == 0:
133+
raise ValueError(
136134
"please call get_setting with basis_vect for a NFMT transform."
137135
)
138-
if len(basis_vect) < max(max(u) for u in U):
139-
ValueError("basis_vect must have an entry for every dimension.")
136+
if len(basis_vect) <= max(max(u) for u in U if len(u) > 0):
137+
raise ValueError("basis_vect must have an entry for every dimension.")
140138
return [
141139
Setting(
142140
u=U[idx],
143141
mode=systems[system],
144142
bandwidths=np.array(bwl[idx], "int32"),
145-
bases=basis_vect[U[idk]],
143+
bases=[basis_vect[j] for j in U[idx]],
146144
)
147145
for idx in range(len(U))
148146
]
@@ -197,11 +195,11 @@ def __init__(
197195

198196
if system == "mixed":
199197
if len(basis_vect) == 0:
200-
ValueError(
198+
raise ValueError(
201199
"please call GroupedTransform with basis_vect for a NFMT transform."
202200
)
203201
if len(basis_vect) != X.shape[1]:
204-
ValueError("basis_vect must have an entry for every dimension.")
202+
raise ValueError("basis_vect must have an entry for every dimension.")
205203

206204
if system in {"exp", "chui1", "chui2", "chui3", "chui4"}:
207205
if np.min(X) < -0.5 or np.max(X) >= 0.5:
@@ -214,14 +212,14 @@ def __init__(
214212

215213
cosine_mask = basis_vals > 0
216214
if np.sum(cosine_mask) > 0:
217-
if (np.min(X[cosine_mask, :]) < 0) or (np.max(X[cosine_mask, :]) > 1):
215+
if (np.min(X[:, cosine_mask]) < 0) or (np.max(X[:, cosine_mask]) > 1):
218216
raise ValueError(
219217
"Nodes must be between 0 and 1 for cosine or Chebyshev dimensions."
220218
)
221219

222220
exp_mask = ~cosine_mask
223221
if np.sum(exp_mask) > 0:
224-
if (np.min(X[exp_mask, :]) < -0.5) or (np.max(X[exp_mask, :]) > 0.5):
222+
if (np.min(X[:, exp_mask]) < -0.5) or (np.max(X[:, exp_mask]) > 0.5):
225223
raise ValueError(
226224
"Nodes must be between -0.5 and 0.5 for exponentional dimensions."
227225
)
@@ -236,7 +234,9 @@ def __init__(
236234
self.parallel = parallel
237235

238236
if len(settings) == 0:
239-
self.settings = get_setting(system=system, N=N, U=U, d=d, ds=ds)
237+
self.settings = get_setting(
238+
system=system, N=N, U=U, d=d, ds=ds, basis_vect=basis_vect
239+
)
240240
else:
241241
self.settings = settings
242242

@@ -278,8 +278,9 @@ def __init__(
278278
)
279279
elif system == "mixed":
280280
matrix = np.array(
281-
s1.mode.get_matrix(bandwidths=s1.bandwidths, X=X[:, u1].T),
282-
bases=s1.bases,
281+
s1.mode.get_matrix(
282+
bandwidths=s1.bandwidths, X=X[:, u1].T, bases=s1.bases
283+
)
283284
)
284285
for s in self.settings[1:]:
285286
if len(s.bandwidths) == 0:
@@ -290,8 +291,9 @@ def __init__(
290291
[
291292
matrix,
292293
np.array(
293-
s.mode.get_matrix(s.bandwidths, X[:, u].T),
294-
bases=s.bases,
294+
s.mode.get_matrix(
295+
s.bandwidths, X[:, u].T, bases=s.bases
296+
)
295297
),
296298
]
297299
)
@@ -433,7 +435,7 @@ def get_matrix(self):
433435
u1 = (0,)
434436
else:
435437
u1 = s1.u
436-
F_direct = s1.mode.get_matrix(s.bdanwidths, self.X[:, u1].T, bases=s.bases)
438+
F_direct = s1.mode.get_matrix(s1.bandwidths, self.X[:, u1].T, bases=s1.bases)
437439
for idx, s in enumerate(self.settings):
438440
if idx == 0:
439441
continue
@@ -443,6 +445,7 @@ def get_matrix(self):
443445
u = s.u
444446
mat = s.mode.get_matrix(s.bandwidths, self.X[:, u].T, s.bases)
445447
F_direct = np.hstack([F_direct, mat])
448+
return F_direct
446449
else:
447450
s1 = self.settings[0]
448451
if len(s1.bandwidths) == 0:
Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,126 @@
1-
RuntimeError("NFMTtools is not implemented yet!")
1+
import numpy as np
2+
3+
from pyGroupedTransforms import *
4+
from pyNFFT3.NFMT import BASES
5+
6+
7+
def datalength(bandwidths: np.ndarray) -> int:
8+
"""Return the number of mixed-basis Fourier coefficients (zero-frequencies excluded).
9+
10+
For each dimension j the frequency range used is:
11+
exp -> {-N_j/2, ..., -1, 1, ..., N_j/2 - 1} (N_j - 1 values)
12+
cos/alg -> {1, 2, ..., N_j - 1} (N_j - 1 values)
13+
Total: prod(N - 1).
14+
"""
15+
if bandwidths.ndim != 1 or bandwidths.dtype != "int32":
16+
return "Please use an one-dimensional numpy.array with dtype 'int32' as input"
17+
if len(bandwidths) == 0:
18+
return 1
19+
return int(np.prod(bandwidths - 1))
20+
21+
22+
def nfmt_index_set_without_zeros(bandwidths: np.ndarray, bases) -> np.ndarray:
23+
"""Return the (d, prod(N-1)) integer frequency matrix.
24+
25+
Mirrors Julia's GroupedTransforms.NFMTtools.nfmt_index_set_without_zeros:
26+
exp dimension j: {-N_j/2, ..., -1, 1, ..., N_j/2 - 1}
27+
cos/alg dimension j: {1, 2, ..., N_j - 1}
28+
"""
29+
d = len(bandwidths)
30+
if d == 0:
31+
return np.array([[0]], dtype=np.int64)
32+
33+
ranges = []
34+
for n, basis in zip(bandwidths, bases):
35+
if BASES[basis] > 0:
36+
# cos or alg: {1, ..., N-1}
37+
ranges.append(list(range(1, int(n))))
38+
else:
39+
# exp: {-N/2, ..., -1, 1, ..., N/2-1}
40+
half = int(n) // 2
41+
ranges.append(list(range(-half, 0)) + list(range(1, half)))
42+
43+
if d == 1:
44+
return np.array(ranges[0], dtype=np.int64).reshape(1, -1)
45+
46+
# Cartesian product; last dimension changes fastest (row-major, matching Julia)
47+
mesh = np.array(np.meshgrid(*ranges, indexing="ij"), dtype=np.int64)
48+
return mesh.reshape(d, -1)
49+
50+
51+
def get_matrix(bandwidths, X, bases) -> np.ndarray:
52+
"""Build the evaluation matrix F with F[m, j] = phi(x_m, k_j).
53+
54+
Mirrors Julia's GroupedTransforms.NFMTtools.get_matrix / get_phi:
55+
exp: exp(-2pi i k x)
56+
cos: sqrt(2) * cos(pi * k * x) (k != 0 guaranteed by frequency set)
57+
alg: sqrt(2) * cos(k * arccos(2x - 1))
58+
59+
X is expected in (d, M) format (columns are nodes), consistent with the
60+
convention used in GroupedTransform.get_matrix.
61+
"""
62+
if X.ndim == 1 or X.shape[0] == 1:
63+
X_eval = X.flatten().reshape(-1, 1) # (M, 1)
64+
d = 1
65+
M = X_eval.shape[0]
66+
else:
67+
d, M = X.shape
68+
X_eval = X.T # (M, d)
69+
70+
if len(bandwidths) == 0:
71+
return np.ones((M, 1), dtype=np.complex128)
72+
73+
freq = nfmt_index_set_without_zeros(
74+
np.asarray(bandwidths, dtype=np.int32), list(bases)
75+
) # (d, nf)
76+
77+
nf = freq.shape[1]
78+
F = np.ones((M, nf), dtype=np.complex128)
79+
80+
for j in range(d):
81+
n_j = freq[j] # (nf,) all nonzero by construction
82+
x_j = X_eval[:, j][:, None] # (M, 1)
83+
if BASES[bases[j]] == 1:
84+
# cos basis: sqrt(2) * cos(pi * k * x)
85+
F *= np.sqrt(2.0) * np.cos(np.pi * x_j * n_j)
86+
elif BASES[bases[j]] == 2:
87+
# alg (Chebyshev) basis: sqrt(2) * cos(k * arccos(2x - 1))
88+
F *= np.sqrt(2.0) * np.cos(n_j * np.arccos(2.0 * x_j - 1.0))
89+
else:
90+
# exp basis: exp(-2pi i k x)
91+
F *= np.exp(-2.0j * np.pi * x_j * n_j)
92+
93+
return F
94+
95+
96+
def get_transform(bandwidths: np.ndarray, X: np.ndarray, bases):
97+
"""Return a DeferredLinearOperator for the mixed-basis grouped transform.
98+
99+
Uses the same matrix as get_matrix wrapped in a linear operator so that
100+
trafo and adjoint are consistent by construction.
101+
X is expected in (M, d) format (rows are nodes), consistent with the
102+
convention used in GroupedTransform.transforms.
103+
"""
104+
if bandwidths.ndim > 1 or bandwidths.dtype != "int32":
105+
return "Please use a zero or one-dimensional numpy.array with dtype 'int32' as input"
106+
107+
M = X.shape[0]
108+
109+
if len(bandwidths) == 0:
110+
return DeferredLinearOperator(
111+
dtype=np.complex128,
112+
shape=(M, 1),
113+
mfunc=lambda fhat: np.full(M, fhat[0], dtype=np.complex128),
114+
rmfunc=lambda f: np.array([np.sum(f)], dtype=np.complex128),
115+
)
116+
117+
# X is (M, d) here; get_matrix expects (d, M)
118+
mat = get_matrix(bandwidths, X.T, list(bases)) # (M, nf)
119+
N = int(np.prod(bandwidths - 1))
120+
121+
return DeferredLinearOperator(
122+
dtype=np.complex128,
123+
shape=(M, N),
124+
mfunc=lambda fhat: mat @ np.asarray(fhat, dtype=np.complex128),
125+
rmfunc=lambda f: mat.conj().T @ np.asarray(f, dtype=np.complex128),
126+
)

tests/nfmt_U.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import os
2+
import sys
3+
4+
src_aa = os.path.abspath(os.path.join(os.getcwd(), "src"))
5+
sys.path.insert(0, src_aa)
6+
7+
import numpy as np
8+
9+
from pyGroupedTransforms.GroupedTransforms import *
10+
11+
rng = np.random.default_rng(42)
12+
13+
d = 4
14+
ds = 3
15+
16+
basis_vect = ["exp", "alg", "cos", "alg"]
17+
18+
M = 1000
19+
# X in (M, d) format; exp dimension shifted to [-0.5, 0.5)
20+
X = rng.random((M, d))
21+
X[:, 0] -= 0.5
22+
23+
U = [(), (1,), (1, 2)]
24+
25+
# set up transform ###################################################
26+
27+
F = GroupedTransform(
28+
"mixed", X, U=U, N=[0, 64, 16], basis_vect=basis_vect
29+
)
30+
F_direct = F.get_matrix()
31+
32+
# compute transform with NFMT ########################################
33+
34+
fhat = GroupedCoefficients(F.settings)
35+
for i in range(len(F.settings)):
36+
u = F.settings[i].u
37+
fhat[u] = rng.random(len(fhat[u])) + 1.0j * rng.random(len(fhat[u]))
38+
39+
# arithmetic tests ###################################################
40+
41+
ghat = GroupedCoefficients(F.settings)
42+
for i in range(len(F.settings)):
43+
u = F.settings[i].u
44+
ghat[u] = rng.random(len(ghat[u])) + 1.0j * rng.random(len(ghat[u]))
45+
46+
fhat[1]
47+
fhat[1] = 1.0 + 1.0j
48+
2 * fhat
49+
fhat + ghat
50+
fhat - ghat
51+
F[(1,)]
52+
fhat.set_data(ghat.data)
53+
54+
###
55+
56+
f = F * fhat
57+
58+
# compute transform without NFMT #####################################
59+
60+
f_direct = np.matmul(F_direct, fhat.vec())
61+
62+
# compare results ####################################################
63+
64+
error = np.linalg.norm(f - f_direct)
65+
assert error < 1e-5, f"trafo error {error} >= 1e-5"
66+
67+
# generate random function values ####################################
68+
69+
y = rng.random(M) + 1.0j * rng.random(M)
70+
71+
# compute adjoint transform with NFMT ################################
72+
73+
fhat = F * y
74+
75+
# compute adjoint transform without NFMT #############################
76+
77+
fhat_direct = np.matmul(np.conj(F_direct).T, y)
78+
79+
# compare results ####################################################
80+
81+
error = np.linalg.norm(fhat.vec() - fhat_direct)
82+
assert error < 1e-5, f"adjoint error {error} >= 1e-5"

0 commit comments

Comments
 (0)