Skip to content

Commit 64a78fb

Browse files
committed
feat: incorporate 2D-DFT to obtain 2D-Frank image
1 parent b654a2a commit 64a78fb

File tree

5 files changed

+337
-50
lines changed

5 files changed

+337
-50
lines changed
File renamed without changes.

frank/Examples/DFT_2D_test.ipynb

Lines changed: 203 additions & 0 deletions
Large diffs are not rendered by default.

frank/fourier2d.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
2+
import numpy as np
3+
4+
class DiscreteFourierTransform2D(object):
5+
def __init__(self, Rmax, N, nu=0):
6+
# Remember that now N is to create N**2 points in image plane.
7+
self.Xmax = 2*Rmax # [Rmax] = rad.
8+
self.Ymax = 2*Rmax
9+
self.Nx = N
10+
self.Ny = N
11+
self.N = N**2 # Number of points we want to use in the 2D-DFT.
12+
13+
# Real space collocation points
14+
x1n = np.linspace(0, self.Xmax, self.Nx) # rad
15+
x2n = np.linspace(0, self.Ymax, self.Ny) # rad
16+
x1n, x2n = np.meshgrid(x1n, x1n, indexing='ij')
17+
# x1n.shape = N**2 X 1, so now, we have N**2 collocation points in the image plane.
18+
x1n, x2n = x1n.reshape(-1), x2n.reshape(-1) # x1n = x_array and x2n = y_array
19+
dx = 2*self.Xmax/self.N
20+
dy = 2*self.Ymax/self.N
21+
22+
# Frequency space collocation points.
23+
# The [1:] is because to not consider the 0 baseline. But we're missing points.
24+
q1n = np.fft.fftfreq(self.Nx+1, d = dx)[1:]
25+
q2n = np.fft.fftfreq(self.Ny+1, d = dy)[1:]
26+
q1n, q2n = np.meshgrid(q1n, q2n, indexing='ij')
27+
# q1n.shape = N**2 X 1, so now, we have N**2 collocation points.
28+
q1n, q2n = q1n.reshape(-1), q2n.reshape(-1) # q1n = u_array and q2n = v_array
29+
30+
self.Xn = x1n
31+
self.Yn = x2n
32+
self.Un = q1n
33+
self.Vn = q2n
34+
35+
def get_collocation_points(self):
36+
return np.array([self.Xn, self.Yn]), np.array([self.Un, self.Vn])
37+
38+
def coefficients(self, u = None, v = None, direction="forward"):
39+
if direction == 'forward':
40+
norm = 1
41+
factor = -2j*np.pi/self.Nx
42+
elif direction == 'backward':
43+
norm = 1 / self.N
44+
factor = 2j*np.pi/self.Nx
45+
else:
46+
raise AttributeError("direction must be one of {}"
47+
"".format(['forward', 'backward']))
48+
if u is None:
49+
u = self.Un
50+
v = self.Vn
51+
52+
if direction == "forward":
53+
H = norm * np.exp(factor*(np.outer(u, self.Xn) + np.outer(v, self.Yn)))
54+
else:
55+
H = norm * np.exp(factor*(np.outer(u, self.Xn) + np.outer(v, self.Yn)))
56+
return H
57+
58+
59+
def transform(self, f, u, v, direction="forward"):
60+
Y = self.coefficients(u, v, direction=direction)
61+
return np.dot(Y, f)
62+
63+
64+
@property
65+
def size(self):
66+
"""Number of points used in the 2D-DFT"""
67+
return self.N
68+
69+
@property
70+
def uv_points(self):
71+
"""u and v collocation points"""
72+
return self.Un, self.Vn
73+
74+
@property
75+
def q(self):
76+
"""Frequency points"""
77+
return np.hypot(self.Un, self.Vn)

frank/radial_fitters.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from frank.constants import rad_to_arcsec
2929
from frank.filter import CriticalFilter
3030
from frank.hankel import DiscreteHankelTransform
31+
from frank.fourier2d import DiscreteFourierTransform2D
3132
from frank.statistical_models import (
3233
GaussianModel, LogNormalMAPModel, VisibilityMapping
3334
)
@@ -443,6 +444,7 @@ def __init__(self, Rmax, N, geometry, nu=0, block_data=True,
443444
self._geometry = geometry
444445

445446
self._DHT = DiscreteHankelTransform(Rmax, N, nu)
447+
self._DFT = DiscreteFourierTransform2D(Rmax, N)
446448

447449
if assume_optically_thick:
448450
if scale_height is not None:
@@ -457,7 +459,8 @@ def __init__(self, Rmax, N, geometry, nu=0, block_data=True,
457459
self._vis_map = VisibilityMapping(self._DHT, geometry,
458460
model, scale_height=scale_height,
459461
block_data=block_data, block_size=block_size,
460-
check_qbounds=False, verbose=verbose)
462+
check_qbounds=False, verbose=verbose,
463+
DFT = self._DFT)
461464

462465
self._info = {'Rmax' : self._DHT.Rmax * rad_to_arcsec,
463466
'N' : self._DHT.size
@@ -577,7 +580,7 @@ def _fit(self):
577580
"""Fit step. Computes the best fit given the pre-processed data"""
578581
fit = GaussianModel(self._DHT, self._M, self._j,
579582
noise_likelihood=self._H0,
580-
Wvalues= self._Wvalues, V = self._V)
583+
Wvalues= self._Wvalues, V = self._V, DFT = self._DFT)
581584

582585
self._sol = FrankGaussianFit(self._vis_map, fit, self._info,
583586
geometry=self._geometry.clone())

frank/statistical_models.py

Lines changed: 52 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ class VisibilityMapping:
6666
"""
6767
def __init__(self, DHT, geometry,
6868
vis_model='opt_thick', scale_height=None, block_data=True,
69-
block_size=10 ** 5, check_qbounds=True, verbose=True):
69+
block_size=10 ** 5, check_qbounds=True, verbose=True,
70+
DFT = None):
7071

7172
_vis_models = ['opt_thick', 'opt_thin', 'debris']
7273
if vis_model not in _vis_models:
@@ -81,6 +82,7 @@ def __init__(self, DHT, geometry,
8182
self._chunk_size = block_size
8283

8384
self._DHT = DHT
85+
self._DFT = DFT
8486
self._geometry = geometry
8587

8688
# Check for consistency and report the model choice.
@@ -178,8 +180,8 @@ def map_visibilities(self, u, v, V, weights, frequencies=None, geometry=None):
178180
frequencies = np.ones_like(V)
179181

180182
channels = np.unique(frequencies)
181-
Ms = np.zeros([len(channels), self.size, self.size], dtype='f8')
182-
js = np.zeros([len(channels), self.size], dtype='f8')
183+
Ms = np.zeros([len(channels), self.size, self.size], dtype='c8')
184+
js = np.zeros([len(channels), self.size], dtype='c8')
183185
for i, f in enumerate(channels):
184186
idx = frequencies == f
185187

@@ -199,11 +201,13 @@ def map_visibilities(self, u, v, V, weights, frequencies=None, geometry=None):
199201
Ndata = len(Vi)
200202
while start < Ndata:
201203
qs = qi[start:end]
204+
us = u[start:end]
205+
vs = v[start:end]
202206
ks = ki[start:end]
203207
ws = wi[start:end]
204208
Vs = Vi[start:end]
205209

206-
X = self._get_mapping_coefficients(qs, ks)
210+
X = self._get_mapping_coefficients(qs, ks, us, vs)
207211

208212
wXT = np.array(X.T * ws, order='C')
209213

@@ -462,7 +466,7 @@ def interpolate(self, f, r, space='Real'):
462466
return self._DHT.interpolate(f, r, space)
463467

464468

465-
def _get_mapping_coefficients(self, qs, ks, geometry=None, inverse=False):
469+
def _get_mapping_coefficients(self, qs, ks, u, v, geometry=None, inverse=False):
466470
"""Get :math:`H(q)`, such that :math:`V(q) = H(q) I_\nu`"""
467471

468472
if self._vis_model == 'opt_thick':
@@ -486,7 +490,8 @@ def _get_mapping_coefficients(self, qs, ks, geometry=None, inverse=False):
486490
else:
487491
direction='forward'
488492

489-
H = self._DHT.coefficients(qs, direction=direction) * scale
493+
#H = self._DHT.coefficients(qs, direction=direction) * scale
494+
H = self._DFT.coefficients(u, v, direction=direction) * scale
490495

491496
return H
492497

@@ -529,7 +534,8 @@ def Rmax(self):
529534
@property
530535
def q(self):
531536
r"""Frequency points, unit = :math:`\lambda`"""
532-
return self._DHT.q
537+
#return self._DHT.q
538+
return self._DFT.q
533539

534540
@property
535541
def Qmax(self):
@@ -539,7 +545,8 @@ def Qmax(self):
539545
@property
540546
def size(self):
541547
"""Number of points in reconstruction"""
542-
return self._DHT.size
548+
#return self._DHT.size
549+
return self._DFT.size
543550

544551
@property
545552
def scale_height(self):
@@ -631,9 +638,10 @@ class GaussianModel:
631638

632639
def __init__(self, DHT, M, j, p=None, scale=None, guess=None,
633640
Nfields=None, noise_likelihood=0,
634-
Wvalues = None, V = None):
641+
Wvalues = None, V = None, DFT = None):
635642

636643
self._DHT = DHT
644+
self._DFT = DFT
637645
self._Wvalues = Wvalues
638646
self._V = V
639647

@@ -691,24 +699,13 @@ def __init__(self, DHT, M, j, p=None, scale=None, guess=None,
691699
en = (n+1)*Nr
692700

693701
self._Sinv[sn:en, sn:en] += Sj[n]
694-
else:
702+
else: # p is None, so we are interested in this case.
703+
" New GP Kernel below"
695704
#self._Sinv = None
696-
q_array = self._DHT.q
697-
698-
def true_squared_exponential_kernel(q, p, l):
699-
700-
q1, q2 = np.meshgrid(q, q)
701-
p1, p2 = np.meshgrid(p, p)
702-
SE_Kernel = np.sqrt(p1 * p2) * np.exp(-0.5*(q1-q2)**2 / l**2)
703-
return SE_Kernel
704-
705-
Ykm = self._DHT.coefficients(direction="backward")
706-
# We continue after set M matrix because is needed to calculate
707-
# best parameters for S matrix.
708705

709706
# Compute the design matrix
710-
self._M = np.zeros([Nr*Nfields, Nr*Nfields], dtype='f8')
711-
self._j = np.zeros(Nr*Nfields, dtype='f8')
707+
self._M = np.zeros([Nr*Nfields, Nr*Nfields], dtype='c8')
708+
self._j = np.zeros(Nr*Nfields, dtype='c8')
712709
for si, Mi, ji in zip(self._scale, M, j):
713710

714711
for n in range(0, Nfields):
@@ -724,35 +721,42 @@ def true_squared_exponential_kernel(q, p, l):
724721

725722
self._like_noise = noise_likelihood
726723

727-
# M is already defined, so we find best parameters for S matrix and use it.
728-
m, c , l = self.minimizeS()
729-
pI = np.exp(m*np.log(q_array) + c)
730-
S_fspace = true_squared_exponential_kernel(q_array, pI, l)
731-
S_real = np.dot(np.transpose(Ykm), np.dot(S_fspace, Ykm))
724+
" New GP "
725+
self.u, self.v = self._DFT.uv_points
726+
self.Ykm = self._DFT.coefficients(direction="backward")
727+
self.Ykm_f = self._DFT.coefficients(direction="forward")
728+
m, c , l = -4.8, 59.21, 1.21e5
729+
#m, c, l = self.minimizeS() # Finding best parameters to S matrix.
730+
S_real = self.calculate_S(self.u, self.v, l, m, c)
732731
S_real_inv = np.linalg.inv(S_real)
733732
self._Sinv = S_real_inv
734733

735734
self._fit()
736735

736+
def true_squared_exponential_kernel(self, u, v, l, m, c):
737+
u1, u2 = np.meshgrid(u, u)
738+
v1, v2 = np.meshgrid(v, v)
739+
q1 = np.hypot(u1, v1)
740+
q2 = np.hypot(u2, v2)
741+
742+
def power_spectrum(q, m, c):
743+
return np.exp(m*np.log(q) + c)
744+
p1 = power_spectrum(q1, m, c)
745+
p2 = power_spectrum(q2, m, c)
746+
747+
SE_Kernel = np.sqrt(p1 * p2) * np.exp(-0.5*((u1-u2)**2 + (v1-v2)**2)/ l**2)
748+
return SE_Kernel
749+
750+
def calculate_S(self, u, v, l, m, c):
751+
S_fspace = self.true_squared_exponential_kernel(u, v, l, m, c)
752+
S_real = np.matmul(self.Ykm, np.matmul(S_fspace, self.Ykm_f))
753+
return S_real
754+
737755
def minimizeS(self):
738756
from scipy.optimize import minimize
739757
from scipy.special import gamma
740758
V = self._V
741759

742-
def calculate_S(m, c, l):
743-
q_array = self._DHT.q
744-
p_array = c*(q_array**m)
745-
def true_squared_exponential_kernel(q, p, l):
746-
q1, q2 = np.meshgrid(q, q)
747-
p1, p2 = np.meshgrid(p, p)
748-
SE_Kernel = np.sqrt(p1 * p2) * np.exp(-0.5*(q1-q2)**2 / l**2)
749-
return SE_Kernel
750-
751-
Ykm = self._DHT.coefficients(direction="backward")
752-
S_fspace = true_squared_exponential_kernel(q_array, p_array, l)
753-
S_real = np.dot(np.transpose(Ykm), np.dot(S_fspace, Ykm))
754-
return S_real
755-
756760
def calculate_D(S):
757761
S_real_inv = np.linalg.inv(S)
758762
Dinv = self._M + S_real_inv
@@ -782,7 +786,7 @@ def likelihood(param, data):
782786
def inv_gamma_function(l, alpha, beta):
783787
return ((gamma(alpha)*beta)**(-1))*((beta/l)**(alpha + 1))*np.exp(-beta/l)
784788

785-
S = calculate_S(m,c, l)
789+
S = self.calculate_S(self.u, self.v, l, m, c)
786790
[Dinv, D] = calculate_D(S)
787791
mu = calculate_mu(Dinv)
788792
logdetS = np.linalg.slogdet(S)[1]
@@ -796,18 +800,17 @@ def inv_gamma_function(l, alpha, beta):
796800
- 0.5*(factor + logdetS) \
797801
+ 0.5*(factor + logdetD) \
798802
+ 0.5*np.dot(np.transpose(self._j), mu) \
799-
- 0.5*np.dot(np.transpose(data), np.dot(np.diag(Wvalues), data))
803+
- 0.5*np.dot(np.transpose(np.conjugate(data)), np.dot(np.diag(Wvalues), data))
800804
return -log_likelihood
801805

802806
result = minimize(likelihood, x0=np.array([-5, 60, 1e5]), args=(V,),
803807
bounds=[(-6, 6), (1, 70), (1e4, 1e6)],
804808
method="Nelder-Mead", tol=1e-7,
805809
)
806810
m, c, l = result.x
807-
print("Result: ", "m: ", m, "c: ", c, "l: ", "{:e}".format(l))
811+
print("Best parameters: ", "m: ", m, "c: ", c, "l: ", "{:e}".format(l))
808812
return [m, c, l]
809813

810-
811814
def _fit(self):
812815
"""Compute the mean and variance"""
813816
# Compute the inverse prior covariance, S(p)^-1
@@ -980,7 +983,8 @@ def num_fields(self):
980983
@property
981984
def size(self):
982985
"""Number of points in reconstruction"""
983-
return self._DHT.size
986+
#return self._DHT.size
987+
return self._DFT.size
984988

985989

986990
class LogNormalMAPModel:

0 commit comments

Comments
 (0)