-
Notifications
You must be signed in to change notification settings - Fork 51
Expand file tree
/
Copy pathkgrid.py
More file actions
701 lines (581 loc) · 22.7 KB
/
kgrid.py
File metadata and controls
701 lines (581 loc) · 22.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
import math
import sys
from dataclasses import dataclass
import numpy as np
from kwave.data import FlexibleVector, Vector
from kwave.enums import DiscreteCosine, DiscreteSine
from kwave.utils import matlab
from kwave.utils.math import largest_prime_factor
@dataclass
class kWaveGrid(object):
"""
kWaveGrid is the grid class used across the k-Wave Toolbox. An object
of the kWaveGrid class contains the grid coordinates and wavenumber
matrices used within the simulation and reconstruction functions in
k-Wave. The grid matrices are indexed as: (x, 1) in 1D; (x, y) in
2D; and (x, y, z) in 3D. The grid is assumed to be a regularly spaced
Cartesian grid, with grid spacing given by dx, dy, dz (typically the
grid spacing in each direction is constant).
"""
# default CFL number
CFL_DEFAULT = 0.3
# machine precision
MACHINE_PRECISION = 100 * sys.float_info.epsilon
def __init__(self, N, spacing):
"""
Args:
N: grid size in each dimension [grid points]
spacing: grid point spacing in each direction [m]
"""
N, spacing = np.atleast_1d(N), np.atleast_1d(spacing) # if inputs are lists
assert N.ndim == 1 and spacing.ndim == 1 # ensure no multidimensional lists
assert (1 <= N.size <= 3) and (1 <= spacing.size <= 3) # ensure valid dimensionality
assert N.size == spacing.size, "Size list N and spacing list do not have the same size."
self.N = N.astype(int) #: grid size in each dimension [grid points]
self.spacing = spacing #: grid point spacing in each direction [m]
self.dim = self.N.size #: Number of dimensions (1, 2 or 3)
self.nonuniform = False #: flag that indicates grid non-uniformity
self.dt = "auto" #: size of time step [s]
self.Nt = "auto" #: number of time steps [s]
# originally there was [xn_vec, yn_vec, zn_vec]
self.n_vec = FlexibleVector([0] * self.dim) #: position vectors for the grid points in [0, 1]
# originally there was [xn_vec_sgx, yn_vec_sgy, zn_vec_sgz]
self.n_vec_sg = FlexibleVector([0] * self.dim) #: position vectors for the staggered grid points in [0, 1]
# originally there was [dxudxn, dyudyn, dzudzn]
self.dudn = FlexibleVector([0] * self.dim) #: transformation gradients between uniform and staggered grids
# originally there was [dxudxn_sgx, dyudyn_sgy, dzudzn_sgz]
self.dudn_sg = FlexibleVector([0] * self.dim) #: transformation gradients between uniform and staggered grids
# assign the grid parameters for the x spatial direction
# originally kx_vec
self.k_vec = FlexibleVector([self.makeDim(self.Nx, self.dx)]) #: Nx x 1 vector of wavenumber components in the x-direction [rad/m]
if self.dim == 1:
# define the scalar wavenumber based on the wavenumber components
self.k = abs(self.k_vec.x) #: scalar wavenumber
if self.dim >= 2:
# assign the grid parameters for the x and y spatial directions
# Ny x 1 vector of wavenumber components in the y-direction [rad/m]
self.k_vec = self.k_vec.append(self.makeDim(self.Ny, self.dy))
if self.dim == 2:
# define the wavenumber based on the wavenumber components
self.k = np.zeros((self.Nx, self.Ny))
self.k = np.reshape(self.k_vec.x, (-1, 1)) ** 2 + self.k
self.k = np.reshape(self.k_vec.y, (1, -1)) ** 2 + self.k
self.k = np.sqrt(self.k) #: scalar wavenumber
if self.dim == 3:
# assign the grid parameters for the x, y, and z spatial directions
# Nz x 1 vector of wavenumber components in the z-direction [rad/m]
self.k_vec = self.k_vec.append(self.makeDim(self.Nz, self.dz))
# define the wavenumber based on the wavenumber components
self.k = np.zeros((self.Nx, self.Ny, self.Nz))
self.k = np.reshape(self.k_vec.x, (-1, 1, 1)) ** 2 + self.k
self.k = np.reshape(self.k_vec.y, (1, -1, 1)) ** 2 + self.k
self.k = np.reshape(self.k_vec.z, (1, 1, -1)) ** 2 + self.k
self.k = np.sqrt(self.k) #: scalar wavenumber
@property
def t_array(self):
"""
time array [s]
"""
# TODO (walter): I would change this functionality to return a time array even if Nt or dt are not yet set
# (e.g. if they are still 'auto')
if self.Nt == "auto" or self.dt == "auto":
return "auto"
else:
t_array = np.arange(0, self.Nt) * self.dt
# TODO: adding this extra dimension seems unnecessary
# This leads to an extra squeeze when plotting e.g. in example "array as sensor" on lines 110 and 111
return np.expand_dims(t_array, axis=0)
@t_array.setter
def t_array(self, t_array):
# check for 'auto' input
if isinstance(t_array, str) and t_array == "auto":
# set values to auto
self.Nt = "auto"
self.dt = "auto"
else:
# extract property values
Nt_temp = t_array.size
dt_temp = t_array[1] - t_array[0]
# check the time array begins at zero
assert t_array[0] == 0, "t_array must begin at zero."
# check the time array is evenly spaced
assert (t_array[1:] - t_array[0:-1] - dt_temp).max() < self.MACHINE_PRECISION, "t_array must be evenly spaced."
# check the time steps are increasing
assert dt_temp > 0, "t_array must be monotonically increasing."
# assign values
self.Nt = Nt_temp
self.dt = dt_temp
def setTime(self, Nt, dt) -> None:
"""
Set Nt and dt based on user input
Args:
Nt:
dt:
Returns: None
"""
# check the value for Nt
assert (
isinstance(Nt, int) or np.issubdtype(Nt, np.int64) or np.issubdtype(Nt, np.int32)
) and Nt > 0, "Nt must be a positive integer."
# check the value for dt
assert dt > 0, "dt must be positive."
# assign values
self.Nt = Nt
self.dt = dt
@property
def Nx(self):
"""
grid size in x-direction [grid points]
"""
return self.N[0]
@property
def Ny(self):
"""
grid size in y-direction [grid points]
"""
return self.N[1] if self.N.size >= 2 else 0
@property
def Nz(self):
"""
grid size in z-direction [grid points]
"""
return self.N[2] if self.N.size == 3 else 0
@property
def dx(self):
"""
grid point spacing in x-direction [m]
"""
return self.spacing[0]
@property
def dy(self):
"""
grid point spacing in y-direction [m]
"""
return self.spacing[1] if self.spacing.size >= 2 else 0
@property
def dz(self):
"""
grid point spacing in z-direction [m]
"""
return self.spacing[2] if self.spacing.size == 3 else 0
@property
def x_vec(self):
"""
Nx x 1 vector of the grid coordinates in the x-direction [m]
"""
# calculate x_vec based on kx_vec
return self.size[0] * self.k_vec.x * self.dx / (2 * np.pi)
@property
def y_vec(self):
"""
Ny x 1 vector of the grid coordinates in the y-direction [m]
"""
# calculate y_vec based on ky_vec
if self.dim < 2:
return np.nan
return self.size[1] * self.k_vec.y * self.dy / (2 * np.pi)
@property
def z_vec(self):
"""
Nz x 1 vector of the grid coordinates in the z-direction [m]
"""
# calculate z_vec based on kz_vec
if self.dim < 3:
return np.nan
return self.size[2] * self.k_vec.z * self.dz / (2 * np.pi)
@property
def x(self):
"""
Nx x Ny x Nz grid containing repeated copies of the grid coordinates in the x-direction [m]
"""
return self.size[0] * self.kx * self.dx / (2 * math.pi)
@property
def y(self):
"""
Nx x Ny x Nz grid containing repeated copies of the grid coordinates in the y-direction [m]
"""
if self.dim < 2:
return 0
return self.size[1] * self.ky * self.dy / (2 * math.pi)
@property
def z(self):
"""
Nx x Ny x Nz grid containing repeated copies of the grid coordinates in the z-direction [m]
"""
if self.dim < 3:
return 0
return self.size[2] * self.kz * self.dz / (2 * math.pi)
@property
def xn(self):
"""
3D plaid non-uniform spatial grids
Returns:
plaid xn matrix
"""
if self.dim == 1:
return self.n_vec.x if self.nonuniform else 0
elif self.dim == 2:
return np.tile(self.n_vec.x, (1, self.Ny)) if self.nonuniform else 0
else:
return np.tile(self.n_vec.x, (1, self.Ny, self.Nz)) if self.nonuniform else 0
@property
def yn(self):
"""
3D plaid non-uniform spatial grids
Returns:
plaid yn matrix
"""
if self.dim < 2:
return np.nan
n_vec_y = np.array(self.n_vec.y).T
if self.dim == 2:
return np.tile(n_vec_y, (self.Nx, 1)) if self.nonuniform else 0
else:
return np.tile(n_vec_y, (self.Nx, 1, self.Nz)) if self.nonuniform else 0
@property
def zn(self):
"""
3D plaid non-uniform spatial grids
Returns:
plaid zn matrix
"""
if self.dim < 3:
return np.nan
n_vec_z = np.atleast_1d(np.squeeze(self.n_vec.z))[None, None, :]
return np.tile(n_vec_z, (self.Nx, self.Ny, 1)) if self.nonuniform else 0
@property
def size(self):
"""
Size of grid in the all directions [m]
"""
return Vector(self.N * self.spacing)
@property
def total_grid_points(self) -> np.ndarray:
"""
Total number of grid points (equal to Nx * Ny * Nz)
"""
return np.prod(self.N)
@property
def kx(self):
"""
Nx x Ny x Nz grid containing repeated copies of the wavenumber components in the x-direction [rad/m]
Returns:
plaid xn matrix
"""
if self.dim == 1:
return self.k_vec.x
elif self.dim == 2:
return np.tile(self.k_vec.x, (1, self.Ny))
else:
return np.tile(self.k_vec.x[:, :, None], (1, self.Ny, self.Nz))
@property
def ky(self):
"""
Nx x Ny x Nz grid containing repeated copies of the wavenumber components in the y-direction [rad/m]
Returns:
plaid yn matrix
"""
if self.dim == 2:
return np.tile(self.k_vec.y.T, (self.Nx, 1))
elif self.dim == 3:
return np.tile(self.k_vec.y[None, :, :], (self.Nx, 1, self.Nz))
return np.nan
@property
def kz(self):
"""
Nx x Ny x Nz grid containing repeated copies of the wavenumber components in the z-direction [rad/m]
Returns:
plaid zn matrix
"""
if self.dim == 3:
return np.tile(self.k_vec.z.T[None, :, :], (self.Nx, self.Ny, 1))
else:
return np.nan
@property
def x_size(self):
"""
Size of grid in the x-direction [m]
"""
return self.Nx * self.dx
@property
def y_size(self):
"""
Size of grid in the y-direction [m]
"""
return self.Ny * self.dy
@property
def z_size(self):
"""
Size of grid in the z-direction [m]
"""
return self.Nz * self.dz
@property
def k_max(self): # added by us, not the same as kWave k_max (see k_max_all for KwaveGrid.k_max)
"""
Maximum supported spatial frequency in the 3 directions [rad/m]
Returns:
Vector of 3 elements each in [rad/m]. Value for higher dimensions set to NaN
"""
#
kx_max = np.abs(self.k_vec.x).max()
ky_max = np.abs(self.k_vec.y).max() if self.dim >= 2 else np.nan
kz_max = np.abs(self.k_vec.z).max() if self.dim == 3 else np.nan
return Vector([kx_max, ky_max, kz_max])
@property
def k_max_all(self):
"""
Maximum supported spatial frequency in all directions [rad/m]
Originally k_max in kWave.kWaveGrid!
Returns:
Scalar in [rad/m]
"""
#
return np.nanmin(self.k_max)
########################################
# functions that can only be accessed by class members
########################################
@staticmethod
# TODO (walter): convert this name to snake case
def makeDim(num_points, spacing):
"""
Create the grid parameters for a single spatial direction
Args:
num_points:
spacing:
Returns:
"""
# define the discretisation of the spatial dimension such that there is always a DC component
if num_points % 2 == 0:
# grid dimension has an even number of points
nx = np.arange(-num_points / 2, num_points / 2) / num_points
else:
# grid dimension has an odd number of points
nx = np.arange(-(num_points - 1) / 2, (num_points - 1) / 2 + 1) / num_points
nx = np.array(nx).T
# force middle value to be zero in case 1/Nx is a recurring
# number and the series doesn't give exactly zero
nx[int(num_points // 2)] = 0
# define the wavenumber vector components
res = (2 * math.pi / spacing) * nx
return res[:, None]
def highest_prime_factors(self, axisymmetric=None) -> np.ndarray:
"""
calculate the highest prime factors
Args:
axisymmetric: Axisymmetric code or None
Returns:
Vector of three elements
"""
# import statement place here in order to avoid circular dependencies
if axisymmetric is not None:
if axisymmetric == "WSWA":
prime_facs = [largest_prime_factor(self.Nx), largest_prime_factor(self.Ny * 4), largest_prime_factor(self.Nz)]
elif axisymmetric == "WSWS":
prime_facs = [largest_prime_factor(self.Nx), largest_prime_factor(self.Ny * 2 - 2), largest_prime_factor(self.Nz)]
else:
raise ValueError("Unknown axisymmetric symmetry.")
else:
prime_facs = [largest_prime_factor(self.Nx), largest_prime_factor(self.Ny), largest_prime_factor(self.Nz)]
return np.array(prime_facs)
# TODO (walter): convert this name to snake case
def makeTime(self, c, cfl=CFL_DEFAULT, t_end=None):
"""
Compute Nt and dt based on the cfl number and grid size, where
the number of time-steps is chosen based on the time it takes to
travel from one corner of the grid to the geometrically opposite
corner. Note, if c is given as a matrix, the calculation for dt
is based on the maximum value, and the calculation for t_end
based on the minimum value.
Args:
c: sound speed
cfl: convergence condition by Courant–Friedrichs–Lewy
t_end: final time step
Returns:
Nothing
"""
# if c is a matrix, find the minimum and maximum values
c = np.array(c)
c_min, c_max = np.min(c), np.max(c)
# check for user define t_end, otherwise set the simulation
# length based on the size of the grid diagonal and the maximum
# sound speed in the medium
if t_end is None:
t_end = np.linalg.norm(self.size, ord=2) / c_min
# extract the smallest grid spacing
min_grid_dim = np.min(self.spacing)
# assign time step based on CFL stability criterion
self.dt = cfl * min_grid_dim / c_max
# assign number of time steps based on t_end
self.Nt = int(np.floor(t_end / self.dt) + 1)
# catch case where dt is a recurring number
if (np.floor(t_end / self.dt) != np.ceil(t_end / self.dt)) and (matlab.rem(t_end, self.dt) == 0):
self.Nt = self.Nt + 1
return self.t_array, self.dt
##################################################
####
#### FUNCTIONS BELOW WERE NOT TESTED FOR CORRECTNESS!
####
##################################################
def kx_vec_dtt(self, dtt_type):
"""
Compute the DTT wavenumber vector in the x-direction
Args:
dtt_type:
Returns:
"""
kx_vec_dtt, M = self.makeDTTDim(self.Nx, self.dx, dtt_type)
return kx_vec_dtt, M
def ky_vec_dtt(self, dtt_type):
"""
Compute the DTT wavenumber vector in the y-direction
Args:
dtt_type:
Returns:
"""
ky_vec_dtt, M = self.makeDTTDim(self.Ny, self.dy, dtt_type)
return ky_vec_dtt, M
def kz_vec_dtt(self, dtt_type):
"""
Compute the DTT wavenumber vector in the z-direction
Args:
dtt_type:
Returns:
"""
kz_vec_dtt, M = self.makeDTTDim(self.Nz, self.dz, dtt_type)
return kz_vec_dtt, M
@staticmethod
# TODO (walter): convert this name to snake case
def makeDTTDim(Nx, dx, dtt_type):
"""
Create the DTT grid parameters for a single spatial direction
Args:
Nx:
dx:
dtt_type:
Returns:
"""
# compute the implied period of the input function
if dtt_type == DiscreteCosine.TYPE_1:
M = 2 * (Nx - 1)
elif dtt_type == DiscreteSine.TYPE_1:
M = 2 * (Nx + 1)
else:
M = 2 * Nx
# calculate the wavenumbers
if dtt_type == DiscreteCosine.TYPE_1:
# whole-wavenumber DTT
# WSWS / DCT-I
n = np.arange(0, M // 2 + 1).T
kx_vec = 2 * math.pi * n / (M * dx)
elif dtt_type == DiscreteCosine.TYPE_2:
# whole-wavenumber DTT
# HSHS / DCT-II
n = np.arange(0, M // 2).T
kx_vec = 2 * math.pi * n / (M * dx)
elif dtt_type == DiscreteSine.TYPE_1:
# whole-wavenumber DTT
# WAWA / DST-I
n = np.arange(1, M // 2).T
kx_vec = 2 * math.pi * n / (M * dx)
elif dtt_type == DiscreteSine.TYPE_2:
# whole-wavenumber DTT
# HAHA / DST-II
n = np.arange(1, M // 2 + 1).T
kx_vec = 2 * math.pi * n / (M * dx)
elif dtt_type in [DiscreteCosine.TYPE_3, DiscreteCosine.TYPE_4, DiscreteSine.TYPE_3, DiscreteSine.TYPE_4]:
# half-wavenumber DTTs
# WSWA / DCT-III
# HSHA / DCT-IV
# WAWS / DST-III
# HAHS / DST-IV
n = np.arange(0, M // 2).T
kx_vec = 2 * math.pi * (n + 0.5) / (M * dx)
else:
raise ValueError
return kx_vec, M
########################################
# functions for non-uniform grids
########################################
# TODO (walter): convert this name to snake case
def setNUGrid(self, dim, n_vec, dudn, n_vec_sg, dudn_sg):
"""
Function to set non-uniform grid parameters in specified dimension
Args:
dim:
n_vec:
dudn:
n_vec_sg:
dudn_sg:
Returns:
"""
# check the dimension to set the nonuniform grid is appropriate
assert dim <= self.dim, f"Cannot set nonuniform parameters for dimension {dim} of {self.dim}-dimensional grid."
# force non-uniform grid spacing to be column vectors, and the
# gradients to be in the correct direction for use with bsxfun
n_vec = np.reshape(n_vec, (-1, 1), order="F")
n_vec_sg = np.reshape(n_vec_sg, (-1, 1), order="F")
if dim == 1:
dudn = np.reshape(dudn, (-1, 1), order="F")
dudn_sg = np.reshape(dudn_sg, (-1, 1), order="F")
elif dim == 2:
dudn = np.reshape(dudn, (1, -1), order="F")
dudn_sg = np.reshape(dudn_sg, (1, -1), order="F")
elif dim == 3:
dudn = np.reshape(dudn, (1, 1, -1), order="F")
dudn_sg = np.reshape(dudn_sg, (1, 1, -1), order="F")
self.n_vec.assign_dim(self.dim, n_vec)
self.n_vec_sg.assign_dim(self.dim, n_vec_sg)
self.dudn.assign_dim(self.dim, dudn)
self.dudn_sg.assign_dim(self.dim, dudn_sg)
# set non-uniform flag
self.nonuniform = True
def k_dtt(self, dtt_type): # Not tested for correctness!
"""
compute the individual wavenumber vectors, where dtt_type is the
type of discrete trigonometric transform, which corresponds to
the assumed input symmetry of the input function, where:
1. DCT-I WSWS
2. DCT-II HSHS
3. DCT-III WSWA
4. DCT-IV HSHA
5. DST-I WAWA
6. DST-II HAHA
7. DST-III WAWS
8. DST-IV HAHS
Args:
dtt_type:
Returns:
"""
# check dtt_type is a scalar or a vector the same size self.dim
dtt_type = np.array(dtt_type)
assert dtt_type.size in [1, self.dim], f"dtt_type must be a scalar, or {self.dim}D vector"
if self.dim == 1:
k, M = self.kx_vec_dtt(dtt_type[0])
return k, M
elif self.dim == 2:
# assign the grid parameters for the x and y spatial directions
kx_vec_dtt, Mx = self.kx_vec_dtt(dtt_type[0])
ky_vec_dtt, My = self.ky_vec_dtt(dtt_type[-1])
# define the wavenumber based on the wavenumber components
k = np.zeros((self.Nx, self.Ny))
# assert len(kx_vec_dtt.shape) == 3
k += np.reshape(kx_vec_dtt, (-1, 1)) ** 2
k += np.reshape(ky_vec_dtt, (1, -1)) ** 2
k = np.sqrt(k)
# define product of implied period
M = Mx * My
return k, M
elif self.dim == 3:
# assign the grid parameters for the x, y, and z spatial directions
kx_vec_dtt, Mx = self.kx_vec_dtt(dtt_type[0])
ky_vec_dtt, My = self.ky_vec_dtt(dtt_type[len(dtt_type) // 2])
kz_vec_dtt, Mz = self.kz_vec_dtt(dtt_type[-1])
# define the wavenumber based on the wavenumber components
k = np.zeros((self.Nx, self.Ny, self.Nz))
k = np.reshape(kx_vec_dtt, (-1, 1, 1)) ** 2 + k
k = np.reshape(ky_vec_dtt, (1, -1, 1)) ** 2 + k
k = np.reshape(kz_vec_dtt, (1, 1, -1)) ** 2 + k
k = np.sqrt(k)
# define product of implied period
M = Mx * My * Mz
return k, M