-
Notifications
You must be signed in to change notification settings - Fork 15
/
dmd_patterns.py
3006 lines (2469 loc) · 117 KB
/
dmd_patterns.py
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
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Generate SIM patterns using lattice periodicity vectors Va and Vb, and duplicating roi_size single unit cell.
See the supplemental material of https://doi.org/10.1038/nmeth.1734 for more discussion of similar approaches.
"""
from typing import Union, Optional
from collections.abc import Sequence
from warnings import warn
from time import perf_counter
import datetime
import json
from pathlib import Path
import numpy as np
from PIL import Image
from scipy.fft import fftshift, fftfreq
from scipy.signal import fftconvolve
from scipy.signal.windows import hann
import tifffile
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from matplotlib.colors import PowerNorm
from matplotlib.patches import Circle
from mcsim.analysis.sim_reconstruction import get_peak_value
from mcsim.analysis.fft import conj_transpose_fft, ft2
from mcsim.analysis.simulate_dmd import xy2uvector, blaze_envelope, _dlp_1stgen_axis
from localize_psf.affine import xform_sinusoid_params, xform_mat, params2xform
try:
import cupy as cp
except ImportError:
cp = None
if cp:
array = Union[np.ndarray, cp.ndarray]
else:
array = np.ndarray
def get_sim_pattern(dmd_size: Sequence[int, int],
vec_a: array,
vec_b: array,
nphases: int,
phase_index: int,
geometry: str = "orthogonal") -> tuple[array, array]:
"""
Convenience function for generating SIM patterns from the tile_patterns() function.
This function can run either on the CPU or the GPU. If vec_a is a NumPy array, it will
run on the CPU. If vec_a is a CuPy array it will run on the GPU.
:param dmd_size: [nx, ny]
:param vec_a: first lattice vector, [dxa, dya]
:param vec_b: second lattice vector, [dxb, dyb]
:param nphases: number of phase shifts required. This effects the filling of the pattern
:param phase_index: integer in range(nphases)
:param geometry: "orthogonal" for pixel addressing geometries similar to the DLP6500, or "diamond" for pixel
geometries like the DLP4500.
:return pattern, cell: 'pattern' is an array giving the desired pattern and 'cell' is an array giving
a single unit cell of the pattern
"""
xp = cp if cp and isinstance(vec_a, cp.ndarray) else np
vec_a = xp.asarray(vec_a)
vec_b = xp.asarray(vec_b)
if not _verify_int(vec_b / nphases):
raise ValueError("At least one component of vec_b was not divisible by nphases")
cell, x_cell, y_cell = get_sim_unit_cell(vec_a, vec_b, nphases, phase_index)
nx, ny = dmd_size
# get indices of pixels
jj, ii = np.meshgrid(xp.arange(nx), xp.arange(ny))
# convert indices to geometry
if geometry == "orthogonal":
x = jj
y = ii
elif geometry == "diamond":
p = jj * xp.sqrt(2) + xp.mod(ii, 2) / xp.sqrt(2)
m = ii / xp.sqrt(2)
x = (p + m) / xp.sqrt(2)
y = (p - m) / xp.sqrt(2)
else:
raise ValueError(f"Geometry `{geometry:s}` is not supported.")
pts = xp.stack((x, y), axis=-1)
pts_cell, na, nb = reduce2cell(pts, vec_a, vec_b)
pts_cell = xp.round(pts_cell, 12).astype(int)
# # generate pattern by choosing coordinate in unit cell
xinds = pts_cell[..., 0] - x_cell[0]
yinds = pts_cell[..., 1] - y_cell[0]
pattern = cell[(yinds, xinds)].astype(bool)
return pattern, cell
# tool for manipulating unit cells
def tile_pattern(dmd_size: Sequence[int, int],
vec_a: array,
vec_b: array,
start_coord: Sequence[int, int],
cell: array,
x_cell: array,
y_cell: array,
do_cell_reduction: bool = True) -> array:
"""
Generate SIM patterns using lattice vectors vec_a = [dxa, dya] and vec_b = [dxb, 0],
and duplicating roi_size single unit cell. See the supplemental material of
https://doi.org/10.1038/nmeth.1734 for more information.
Note: we interpret the pattern
params(x, y) = M[i_y, i_x], where M is the matrix representing the pattern. Matlab will display the matrix
with i_y = 0 on top, so the pattern we really want is the matrix flipped along the first dimension.
:param dmd_size: [nx, ny]
:param vec_a: [dxa, dya]
:param vec_b: [dxb, dyb]
:param start_coord: [x, y]. DMD coordinate where the origin of the unit cell will be situated. These coordinates
are relative to the image corner
:param cell:
:param x_cell:
:param y_cell:
:param do_cell_reduction: whether to call get_minimal_cell() before tiling
:return pattern:
"""
xp = cp if cp and isinstance(vec_a, cp.ndarray) else np
# todo: much slower than the old function because looping and doing pixel assignment instead of concatenating
vec_a = xp.array(vec_a, copy=True)
vec_b = xp.array(vec_b, copy=True)
start_coord = xp.array(start_coord, copy=True)
nx, ny = dmd_size
if do_cell_reduction:
# this will typically make the vectors shorter and more orthogonal, so tiling is easier
cell, x_cell, y_cell, vec_a, vec_b = get_minimal_cell(cell, x_cell, y_cell, vec_a, vec_b)
pattern = xp.zeros((ny, nx)) * xp.nan
dy, dx = cell.shape
# find maximum integer multiples of the periodicity vectors that we need
# n * vec_a + m * vec_b + start_coord = corners
# [[dxa, dxb], [dya, dyb]] * [[n], [m]] = [[cx], [cy]] - [[sx], [sy]]
sx, sy = start_coord
mat = xp.linalg.inv(xp.array([[vec_a[0], vec_b[0]], [vec_a[1], vec_b[1]]]))
n1, m1 = mat.dot(xp.array([[pattern.shape[1] - sx], [pattern.shape[0] - sy]], dtype=float))
n2, m2 = mat.dot(xp.array([[0. - sx], [pattern.shape[0] - sy]], dtype=float))
n3, m3 = mat.dot(xp.array([[pattern.shape[1] - sx], [0. - sy]], dtype=float))
n4, m4 = mat.dot(xp.array([[0. - sx], [0. - sy]], dtype=float))
na_min = int(xp.floor(xp.array([n1, n2, n3, n4]).min()))
na_max = int(xp.ceil(xp.array([n1, n2, n3, n4]).max()))
nb_min = int(xp.floor(xp.array([m1, m2, m3, m4]).min()))
nb_max = int(xp.ceil(xp.array([m1, m2, m3, m4]).max()))
niterations = (na_max - na_min) * (nb_max - nb_min)
if niterations > 1e3:
# if number of iterations is large, reduce number of tilings required by doubling unit cell
# todo: could probably find the optimal number of doublings/tilings. This is important to get this to be fast
# todo: in a general case
# todo: right now pretty fast for 'reasonable' patterns, but still seems to be slow for some sets of patterns.
na_max_doublings = int(xp.floor(xp.log2((na_max - na_min))))
na_doublings = int(xp.array([int(xp.round(na_max_doublings / 2)), 1]).max())
nb_max_doublings = int(xp.floor(xp.log2((nb_max - nb_min))))
nb_doublings = int(xp.array([int(xp.round(nb_max_doublings / 2)), 1]).max())
large_pattern, xpatt, ypatt = double_cell(cell,
x_cell,
y_cell,
vec_a,
vec_b,
na=na_doublings,
nb=nb_doublings)
# finish by tiling
pattern = tile_pattern(dmd_size,
2 ** na_doublings * vec_a,
2 ** nb_doublings * vec_b,
start_coord,
large_pattern,
xpatt,
ypatt,
do_cell_reduction=False)
else:
# for smaller iteration number, tile directly
for n in range(na_min, na_max + 1):
for m in range(nb_min, nb_max + 1):
# account for fact the origin of the cell may not be at the lower left corner.
# (0, 0) position of the cell should be at vec_a * n + vec_b * m + start_coord
xzero, yzero = vec_a * n + vec_b * m + start_coord
xstart = int(xzero + xp.min(x_cell))
ystart = int(yzero + xp.min(y_cell))
xend = xstart + int(dx)
yend = ystart + int(dy)
if xend < 0 or yend < 0 or xstart > pattern.shape[1] or ystart > pattern.shape[0]:
continue
if xstart < 0:
xstart_cell = -xstart
xstart = 0
else:
xstart_cell = 0
if xend > pattern.shape[1]:
xend = pattern.shape[1]
xend_cell = xstart_cell + (xend - xstart)
if ystart < 0:
ystart_cell = -ystart
ystart = 0
else:
ystart_cell = 0
if yend > pattern.shape[0]:
yend = pattern.shape[0]
yend_cell = ystart_cell + (yend - ystart)
pattern[ystart:yend, xstart:xend] = xp.nansum(
np.concatenate((pattern[ystart:yend, xstart:xend, None],
cell[ystart_cell:yend_cell, xstart_cell:xend_cell, None]), axis=2), axis=2)
assert not xp.any(xp.isnan(pattern))
pattern = xp.asarray(pattern, dtype=bool)
return pattern
def double_cell(cell: array,
x: array,
y: array,
vec_a: array,
vec_b: array,
na: int = 1,
nb: int = 0) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
"""
Create new unit cell by doubling the original one by a factor of na along vec_a and nb along vec_b
:param cell: initial cell
:param x: x-coordinates of cell
:param y: y-coordinates of cell
:param vec_a: periodicity vector a
:param vec_b: periodicity vector b
:param na: number of times to double unit cell along vec_a
:param nb: number of times to double cell along vec_b
:return big_cell, xs, ys: doubled cell and x- and y-coordinates of double cell
"""
xp = cp if cp and isinstance(cell, cp.ndarray) else np
x = xp.asarray(x)
y = xp.asarray(y)
vec_a = xp.array(vec_a, copy=True)
vec_b = xp.array(vec_b, copy=True)
if not (na == 1 and nb == 0):
big_cell = cell
xs = x
ys = y
for ii in range(na):
big_cell, xs, ys = double_cell(big_cell, xs, ys, 2**ii * vec_a, vec_b, na=1, nb=0)
for jj in range(nb):
big_cell, xs, ys = double_cell(big_cell, xs, ys, 2**jj * vec_b, 2**na * vec_a, na=1, nb=0)
else:
dyc, dxc = cell.shape
v1 = xp.array([0, 0])
v2 = 2*vec_a
v3 = vec_b
v4 = 2*vec_a + vec_b
xs = xp.arange(int(xp.stack([v1[0], v2[0], v3[0], v4[0]]).min()),
int(xp.stack([v1[0], v2[0], v3[0], v4[0]]).max()) + 1)
ys = xp.arange(int(xp.stack([v1[1], v2[1], v3[1], v4[1]]).min()),
int(xp.stack([v1[1], v2[1], v3[1], v4[1]]).max()) + 1)
big_cell = xp.zeros((len(ys), len(xs))) * xp.nan
for n in [0, 1]:
xzero, yzero = vec_a * n
# todo: should round or ceil/floor?
istart_x = int(xzero - xp.min(xs) + xp.min(x))
istart_y = int(yzero - xp.min(ys) + xp.min(y))
big_cell[istart_y:istart_y+dyc, istart_x:istart_x+dxc][np.logical_not(np.isnan(cell))] = \
cell[np.logical_not(np.isnan(cell))]
return big_cell, xs, ys
def get_sim_unit_cell(vec_a: array,
vec_b: array,
nphases: int,
phase_index: int = 0) -> tuple[array, array, array]:
"""
Get unit cell, which can be repeated to form SIM pattern.
:param vec_a: first lattice vector
:param vec_b: second lattice vector
:param nphases: number of phase shifts. Required to determine the on and off pixels in cell.
:param phase_index:
:return cell, x, y: square array representing cell. Ones and zeroes give on and off points, and nans are
points that are not part of the unit cell, but are necessary to pad the array to make it squares
"""
xp = cp if cp is not None and isinstance(vec_a, cp.ndarray) else np
vec_a = xp.asarray(vec_a)
vec_b = xp.asarray(vec_b)
# ensure both vec_b components are divisible by nphases
if not _verify_int(vec_b / nphases):
raise ValueError("At least one component of vec_b was not divisible by nphases")
# get full unit cell
cell, x_cell, y_cell = get_unit_cell(vec_a, vec_b)
# get reduced unit cell from vec_a, vec_b/nphases. If we set all of these positions to 1,
# then we get perfect tiling.
vec_b_sub = vec_b // nphases
cell_sub, x_cell_sub, y_cell_sub = get_unit_cell(vec_a, vec_b_sub)
to_use = xp.logical_not(np.isnan(cell_sub))
# get coordinates in main cell
xx_cs, yy_cs = np.meshgrid(x_cell_sub, y_cell_sub)
xx_cs += vec_b_sub[0] * phase_index
yy_cs += vec_b_sub[1] * phase_index
# ensure using coordinates in unit cell
pts_cell, _, _ = reduce2cell(xp.stack((xx_cs, yy_cs), axis=-1), vec_a, vec_b)
pts_cell = xp.round(pts_cell, 12).astype(int)
# convert to indices and set pattern
xinds = (pts_cell[..., 0] - x_cell[0])[to_use]
yinds = (pts_cell[..., 1] - y_cell[0])[to_use]
cell[(yinds, xinds)] = 1
with np.errstate(invalid='ignore'):
if xp.nansum(cell) != xp.sum(cell >= 0) / nphases:
raise ValueError("Cell does not have appropriate number of 'on' pixels")
return cell, x_cell, y_cell
def get_unit_cell(vec_a: array,
vec_b: array) -> tuple[array, array, array]:
"""
Generate a mask which represents one unit cell of a pattern for given vectors.
This mask is a square array with NaNs at positions outside the unit cell, and
zeros at points in the cell.
The unit cell is the area enclosed by [0, vec_a, vec_b, vec_a + vec_b]. For pixels, we say that
an entire pixel is within the cell if its center is. For a pixel with center exactly on one of the
edges of the cell, we say it is inside if it lies on the lines from [0, vec_b] or
[0, vec_a] and outside if its lies on the lines from [vec_a, vec_a + vec_b] or [vec_b, vec_a + vec_b].
This choice avoids including pixels twice.
:param vec_a: [dxa, dya]
:param vec_b: [dxb, dyb]
:return cell, x, y: cell values are 0 for valid points and NaN for invalid points. x- and y- coordinates
"""
xp = cp if cp and isinstance(vec_a, cp.ndarray) else np
# test that vec_a and vec_b components are integers
if not _verify_int(vec_a) or not _verify_int(vec_b):
raise ValueError(f"At least one component of lattice vectors={vec_a}, {vec_b} cannot "
f"be interpreted as an integer")
# copy vector data, so don't affect inputs
vec_a = xp.array(vec_a, copy=True, dtype=int)
vec_b = xp.array(vec_b, copy=True, dtype=int)
# check vectors are linearly independent
if (vec_a[0] * vec_b[1] - vec_a[1] * vec_b[0]) == 0:
raise ValueError("vec_a and vec_b are linearly dependent.")
# square array containing unit cell, with points not in unit cell nans
dy = xp.abs(vec_a[1]) + xp.abs(vec_b[1])
dx = xp.abs(vec_a[0]) + xp.abs(vec_b[0])
# x-coordinates massaged so that origin is at x=0
x = xp.arange(dx)
if vec_a[0] < 0 and vec_b[0] >= 0:
x = x + vec_a[0] + 1
elif vec_a[0] >= 0 and vec_b[0] < 0:
x = x + vec_b[0] + 1
elif vec_a[0] < 0 and vec_b[0] < 0:
x = x + vec_a[0] + vec_b[0] + 1
# y-coordinates massaged so that origin is at y=0
y = xp.arange(dy)
if vec_a[1] < 0 and vec_b[1] >= 0:
y = y + vec_a[1] + 1
elif vec_a[1] >= 0 and vec_b[1] < 0:
y = y + vec_b[1] + 1
elif vec_a[1] < 0 and vec_b[1] < 0:
y = y + vec_a[1] + vec_b[1] + 1
xx, yy = xp.meshgrid(x, y)
# generate cell
pts = xp.stack((xx, yy), axis=-1)
cell = xp.array(test_in_cell(pts, vec_a, vec_b), dtype=float)
cell[cell == False] = np.nan
cell[cell == True] = 0
# check unit cell has correct volume
cell_volume = xp.abs(vec_a[0] * vec_b[1] - vec_a[1] * vec_b[0])
assert xp.nansum(xp.logical_not(xp.isnan(cell))) == cell_volume
return cell, x, y
def test_in_cell(points: array,
va: array,
vb: array) -> array:
"""
Test if points (x, y) are in the unit cell for a given pair of unit vectors. We suppose the
unit cell is the region enclosed by 0, va, vb, and va + vb. Point on the boundary are considered
inside if they are on the lines 0 -> va or 0 ->vb, and outside if they are on the lines va -> va+vb
or vb -> va + vb
:param points: array of size n0 x n1 x ... x nm x 2, where points[..., 0] are the
x-coordinates and points[..., 1] are the y-coordinates
:param va: first lattice vector
:param vb: second lattice vector
:return in_cell: boolean array of size n0 x n1 x ... x nm indicating if coordinate is in the unit cell or not
"""
xp = cp if cp and isinstance(va, cp.ndarray) else np
va = xp.array(va, copy=True).ravel()
vb = xp.array(vb, copy=True).ravel()
x = points[..., 0]
y = points[..., 1]
def line(x, p1, p2): return ((p2[1] - p1[1]) * x + p1[1] * p2[0] - p1[0] * p2[1]) / (p2[0] - p1[0])
precision = 10
# strategy: consider parellel lines from line1 = [0,0] -> va and line2 = vb -> va + vb
# if point is on opposite sides of line1 and line2, or exactly on line1 then it is inside the cell
# if it is one of the same sides of line1 and line2, or exactly on line2, it is outside
if va[0] != 0:
gthan_a1 = xp.round(line(x, [0, 0], va), precision) > xp.round(y, precision)
eq_a1 = xp.round(line(x, [0, 0], va), precision) == xp.round(y, precision)
gthan_a2 = xp.round(line(x, vb, va + vb), precision) > xp.round(y, precision)
eq_a2 = xp.round(line(x, vb, va + vb), precision) == xp.round(y, precision)
else:
# if x-component of va = 0. Then x-component of vb cannot be zero, else linearly dependent
gthan_a1 = xp.round(x, precision) > 0
eq_a1 = xp.round(x, precision) == 0
gthan_a2 = xp.round(x, precision) > xp.round(vb[0], precision)
eq_a2 = xp.round(x, precision) == xp.round(vb[0], precision)
# same strategy for vb
if vb[0] != 0:
gthan_b1 = xp.round(line(x, [0, 0], vb), precision) > xp.round(y, precision)
eq_b1 = xp.round(line(x, [0, 0], vb), precision) == xp.round(y, precision)
gthan_b2 = xp.round(line(x, va, va + vb), precision) > xp.round(y, precision)
eq_b2 = xp.round(line(x, va, va + vb), precision) == xp.round(y, precision)
else:
# if x-component of vb = 0. Then x-component of va cannot be zero, else linearly dependent
gthan_b1 = xp.round(x, precision) > 0
eq_b1 = xp.round(x, precision) == 0
gthan_b2 = xp.round(x, precision) > xp.round(va[0], precision)
eq_b2 = xp.round(x, precision) == xp.round(va[0], precision)
in_cell_a = xp.logical_and(np.logical_or(gthan_a1 != gthan_a2, eq_a1), xp.logical_not(eq_a2))
in_cell_b = xp.logical_and(np.logical_or(gthan_b1 != gthan_b2, eq_b1), xp.logical_not(eq_b2))
in_cell = xp.logical_and(in_cell_a, in_cell_b)
return in_cell
def reduce2cell(point: array,
va: array,
vb: array) -> tuple[array, array, array]:
"""
Given a vector, reduce it to coordinates within the unit cell.
To ensure all reduced points actually lie in the unit cell, run
>>> assert np.all(test_in_cell(point, va, vb)[0], va, vb)
:param point: of size n0 x n1 x ... x nm x 2
:param va: first lattice vector, of size 2 x 1
:param vb: second lattice vector, of size 2 x 1
:return point_red, na_out, nb_out:
"""
xp = cp if cp is not None and isinstance(point, cp.ndarray) else np
if not _verify_int(va) or not _verify_int(vb):
raise ValueError(f"At least one component of lattice vectors={va}, {vb} cannot "
f"be interpreted as an integer")
point = xp.asarray(point)
va = xp.asarray(va)
vb = xp.asarray(vb)
ra, rb = get_reciprocal_vects(va, vb)
# need to round to avoid problems with machine precision
na_out = xp.floor(xp.round(xp.dot(point, ra), 10)).astype(int)
nb_out = xp.floor(xp.round(xp.dot(point, rb), 10)).astype(int)
point_red = point - (na_out * va + nb_out * vb)
return point_red, na_out, nb_out
def convert_cell(cell1: array,
x1: array,
y1: array,
va1: array,
vb1: array,
va2: array,
vb2: array) -> tuple[array, array, array]:
"""
Given a unit cell described by vectors va1 and vb2, convert to equivalent description
in terms of va2, vb2
:param cell1: initial cell
:param x1: initial coordinates
:param y1:
:param va1: initial lattice vectors
:param vb1:
:param va2: new lattice vectors
:param vb2:
:return cell2, x2, y2:
"""
xp = cp if cp and isinstance(cell1, cp.ndarray) else np
x1 = xp.asarray(x1)
y1 = xp.asarray(y1)
va1 = xp.asarray(va1)
vb1 = xp.asarray(vb1)
va2 = xp.asarray(va2)
vb2 = xp.asarray(vb2)
# todo: add check that va1/vb1 and va2/vb2 describe same lattice
for vec in [va1, vb1, va2, vb2]:
if not _verify_int(vec):
raise ValueError(f"At least one component of lattice {vec} cannot "
f"be interpreted as an integer")
cell2, x2, y2 = get_unit_cell(va2, vb2)
xx2, yy2 = xp.meshgrid(x2, y2)
p1, _, _ = reduce2cell(xp.stack((xx2.ravel(), yy2.ravel()), axis=1),
va1,
vb1)
xind = p1[:, 0] - x1.min()
yind = p1[:, 1] - y1.min()
cell2 += cell1[(yind, xind)].reshape(xx2.shape) # preserver NaNs
return cell2, x2, y2
def get_minimal_cell(cell: array,
x: array,
y: array,
va: array,
vb: array) -> tuple[array, array, array, array, array]:
"""
Convert to cell using the smallest lattice vectors
:param cell:
:param x:
:param y:
:param va: first lattice vector
:param vb: second lattice vector
:return cell_m, x_m, y_m, va_m, vb_m:
"""
va_m, vb_m = reduce_basis(va, vb)
cell_m, x_m, y_m = convert_cell(cell, x, y, va, vb, va_m, vb_m)
return cell_m, x_m, y_m, va_m, vb_m
def show_cell(v1: np.ndarray,
v2: np.ndarray,
cell: np.ndarray,
x: np.ndarray,
y: np.ndarray,
aspect_equal: bool = True,
ax=None,
**kwargs):
"""
Plot unit cell and periodicity vectors
:param v1: first lattice vector
:param v2: second lattice vector
:param cell: array representing values of pattern in unit cell
:param x: x-coordinates of cell
:param y: y-coordinates of cell
:param aspect_equal:
:param ax: axes on which to plot cell. If not provided, generate a new figure
:return fig, ax: handle to resulting figure and axes
"""
if ax is None:
fig = plt.figure(**kwargs)
ax = fig.add_subplot(1, 1, 1)
else:
fig = ax.get_figure()
ax.set_title("Unit cell")
# plot cell
if aspect_equal:
aspect = "equal"
else:
aspect = "auto"
ax.imshow(np.abs(cell),
origin='lower',
extent=[x[0] - 0.5, x[-1] + 0.5,
y[0] - 0.5, y[-1] + 0.5],
aspect=aspect,
interpolation="none")
# plot lattice vectors
if v1 is not None and v2 is not None:
v1 = np.asarray(v1).ravel()
v2 = np.asarray(v2).ravel()
ax.plot([0, v1[0]],
[0, v1[1]],
'r',
label=f"$v_1$=({v1[0]:d}, {v1[1]:d})")
ax.plot([0, v2[0]],
[0, v2[1]],
'g',
label=f"$v_2$=({v2[0]:d}, {v2[1]:d})")
ax.plot([v2[0], v2[0] + v1[0]],
[v2[1], v2[1] + v1[1]],
'r')
ax.plot([v1[0], v2[0] + v1[0]],
[v1[1], v2[1] + v1[1]],
'g')
ax.legend()
ax.set_xlabel("x")
ax.set_ylabel("y")
return fig, ax
# determine parameters of SIM patterns
def get_reciprocal_vects(vec_a: array,
vec_b: array) -> tuple[array, array]:
"""
Compute the reciprocal vectors for (real-space) lattice vectors vec_a and vec_b.
If we call the lattice vectors a_i and the reciprocal vectors b_j, then these should be defined such that
dot(a_i, b_j) = delta_{ij}, i.e. exp[ i 2*pi*ai * bj] = 1.
This means the b_j are frequency like (i.e. equivalent of Hz). Note that they are instead sometimes defined
dot(a_i, b_j) = 2*pi * delta_{ij}, which would make them angular-frequency like (i.e. in radians).
:param vec_a: first lattice vector, [vx, vy]
:param vec_b: second lattice vector
:return rv1, rv2: frequency-like reciprocal lattice vectors
"""
xp = cp if cp and isinstance(vec_a, cp.ndarray) else np
vec_a = xp.asarray(vec_a)
vec_b = xp.asarray(vec_b)
# check this directly, as sometimes due to numerical precision np.linalg.inv() will not throw error
err_msg = "vec_a and vec_b are linearly dependent, so their reciprocal vectors could not be computed."
if (vec_a[0] * vec_b[1] - vec_a[1] * vec_b[0]) == 0:
raise ValueError(err_msg)
a_mat = xp.stack((vec_a, vec_b), axis=0)
try:
inv_a = xp.linalg.inv(a_mat)
except xp.linalg.LinAlgError:
raise ValueError(err_msg)
rv1 = inv_a[:, 0][:, None]
rv2 = inv_a[:, 1][:, None]
# todo: return 1D vectors instead of 2D vectors
# rv1 = inv_a[:, 0]
# rv2 = inv_a[:, 1]
return rv1, rv2
def get_sim_angle(vec_a: array,
vec_b: array) -> float:
"""
Get angle of SIM pattern in radians
:param vec_a: first lattice vector, [vx, vy]
:param vec_b: second lattice vector
:return angle: angle in radians
"""
xp = cp if cp and isinstance(vec_a, cp.ndarray) else np
recp_va, recp_vb = get_reciprocal_vects(vec_a, vec_b)
angle = xp.angle(recp_vb[0, 0] + 1j * recp_vb[1, 0])
return float(xp.mod(angle, 2*np.pi))
def get_sim_period(vec_a: array,
vec_b: array) -> float:
"""
Get period of SIM pattern constructed from periodicity vectors.
The period is the distance between parallel lines pointing in the direction of vec_a passing through the
points 0 and vec_b_temp respectively. We construct this by taking the projection of vec_b along the perpendicular to
vec_a. NOTE: to say this another way, the period is given by the reciprocal lattice vector orthogonal to vec_a.
:param vec_a: first lattice vector, [vx, vy]
:param vec_b: second lattice vector
:return period: pattern period
"""
xp = cp if cp and isinstance(vec_a, cp.ndarray) else np
vec_b = xp.asarray(vec_b)
uvec_perp_a = xp.array([vec_a[1], -vec_a[0]]) / np.sqrt(vec_a[0]**2 + vec_a[1]**2)
period = xp.abs(uvec_perp_a.dot(vec_b))
return float(period)
def get_sim_frqs(vec_a: array,
vec_b: array) -> tuple[float, float]:
"""
Get spatial frequency of SIM pattern constructed from periodicity vectors.
:param vec_a: first lattice vector, [vx, vy]
:param vec_b: second lattice vector
:return fx, fy:
"""
recp_va, recp_vb = get_reciprocal_vects(vec_a, vec_b)
fx = recp_vb[0, 0]
fy = recp_vb[1, 0]
return fx, fy
def get_sim_phase(vec_a: array,
vec_b: array,
nphases: int,
phase_index: int,
pattern_size: Sequence[int],
use_fft_origin: bool = True):
"""
Get phase of dominant frequency component in the SIM pattern.
.. math::
P(x, y) = 0.5 \\left(1 + \\cos(2 \\pi f_x x + 2\\pi f_y y + \\phi \\right)
:param vec_a: first lattice vector, [vx, vy]
:param vec_b: second lattive vector
:param nphases: number of equal phase shifts for SIM pattern
:param phase_index: 0, ..., nphases-1
:param pattern_size: [nx, ny]
:param use_fft_origin: origin to use for computing the phase. If True, will assume the coordinates are the same
as used in an FFT (i.e. before performing an ifftshift, with the 0 near the center). If False, will
suppose the origin is at pattern[0, 0].
:return phase: phase of the SIM pattern at the dominant frequency component (which is recp_vec_b)
"""
xp = cp if cp and isinstance(vec_a, cp.ndarray) else np
vec_b = xp.asarray(vec_b)
cell, xs, ys = get_sim_unit_cell(vec_a, vec_b, nphases)
fourier_component, _ = get_pattern_fourier_component(cell,
xs,
ys,
vec_a,
vec_b,
0,
1,
nphases,
phase_index,
use_fft_origin=use_fft_origin,
dmd_size=pattern_size)
phase = xp.angle(fourier_component)
return xp.mod(phase, 2*np.pi)
def ldftfreq(vec_a: np.ndarray,
vec_b: np.ndarray) -> tuple[array, array, array]:
"""
Get the Fourier frequencies for the lattice fourier transform (LDFT). See ldft2() for more details.
This function is an analog of fftfreq().
:param vec_a: first lattice vector
:param vec_b: second lattice vector
:return fvecs, f1, f2: fvecs are the frequencies. Return NaNs for values outside the unit cell.
f1 and f2 are integers which define the frequencies as multiplies of the reciprocal
lattice vectors b1 and b2. fvecs = f1 * b1 + f2 * b2.
"""
xp = cp if cp and isinstance(vec_a, cp.ndarray) else np
b1, b2 = get_reciprocal_vects(vec_a, vec_b)
det = _convert_int(xp.linalg.det(xp.stack((vec_a, vec_b), axis=1)))
b1_int_approx = b1.ravel() * det
b2_int_approx = b2.ravel() * det
b1_int = _convert_int(b1_int_approx)
b2_int = _convert_int(b2_int_approx)
# todo: to match DFT, want this to produce one more negative frequency than positive frequency
# todo: don't think this works that way at the moment?
bcell, f1, f2 = get_unit_cell(b1_int, b2_int)
fvecs = xp.expand_dims(f1, axis=(0, 2)) * xp.expand_dims(b1.ravel(), axis=(0, 1)) + \
xp.expand_dims(f2, axis=(1, 2)) * xp.expand_dims(b2.ravel(), axis=(0, 1))
fvecs[xp.isnan(bcell), :] = np.nan
return fvecs, f1, f2
def ldft2(unit_cell: array,
x: array,
y: array,
vec_a: array,
vec_b: array) -> array:
"""
Compute the 2D lattice DFT (LDFT) of a given pattern defined on a unit cell. Here the indices of
the pattern are given by the pixel locations in the unit cell. The frequencies are also defined on a unit cell,
but in this case generated by the unit vectors b1 * det(A) and b2 * det(A), where det(A) is the determinant of a
matrix with rows or columns given by the periodicity vectors a1, a2. The unit cell and coordinates can be
obtained from get_sim_unit_cell(). Alternatively, the skeleton of the unit cell can be obtained from
get_unit_cell() and then the values on the unit cell may be chosen by the user.
Use ldftfreq() to get the corresponding frequencies.
:math: `LDFT[g](f_1, f_2) = \\sum_{nx, ny} \\exp[-2 \\pi i [f_1 b_1 (n_x, n_y) + f_2 b_2 (n_x, n_y)]] g(n_x, n_y)
Instead of (nx, ny), one can also work with coefficients d1, d2 for the vectors a1 and a2.
i.e. (nx, ny) = d1(nx, ny) * a1 + d2(nx, ny) * a2
:param unit_cell: array giving values of the unit cell at each coordinate, or NaN's if the position is not
in the unit cell
:param x: x-coordinates of the unit cell. Same shape as unit_cell
:param y: y-coordinates of the unit cell.
:param vec_a: first lattice vector
:param vec_b: second lattice vector
:return ldft: points which are not in the frequency unit cell are returned as NaNs
"""
# todo: what exactly is the relationship between this and get_pattern_fourier_component()?
# todo: presumably this computes all Fourier components rather than just one
# todo: what is relationship between this and get_efield_fourier_components()?
# todo: should combine these two functions ...
xp = cp if cp and isinstance(vec_a, cp.ndarray) else np
fvecs, f1, f2 = ldftfreq(vec_a, vec_b)
xx, yy = np.meshgrid(x, y)
# todo: can I rewrite to do this with an fft on a larger rectangle?
ldft = xp.nansum(unit_cell * xp.exp(-1j*2*np.pi * (fvecs[..., 0][..., None, None] * xx +
fvecs[..., 1][..., None, None] * yy)),
axis=(-1, -2))
ldft[xp.isnan(fvecs[..., 0])] = xp.nan + 1j * xp.nan
return ldft
def get_pattern_fourier_component(unit_cell: array,
x: array,
y: array,
vec_a: array,
vec_b: array,
na: int,
nb: int,
nphases: int = 3,
phase_index: int = 0,
use_fft_origin: bool = True,
dmd_size: Optional[Sequence[int]] = None) -> tuple[np.ndarray, np.ndarray]:
"""
Get fourier component at f = n * recp_vec_a + m * recp_vec_b.
:math: `\\mathcal{F}(f) = \\sum_r f(r) * \\exp(-1j 2 \\pi f r)`
:param np.array unit_cell: unit cell, as produced by get_sim_unit_cell()
:param x: x-coordinates of unit cell
:param y: y-coordinates of unit cell
:param vec_a: first lattice vector
:param vec_b: second lattice vector
:param na: integer multiples of recp_vec_a
:param nb: integer multiples of recp_vec_b
:param nphases: only relevant for calculating phase
:param phase_index: only relevant for calculating phase
:param use_fft_origin: Specifies where the origin of the array is, which affects the phase.
if not using the fft_origin, using the corner
:param dmd_size: [nx, ny], only required if origin is "fft"
:return fcomponent, frq_vector: fourier component of pattern at frq_vector = recp_vec_a * n + recp_vec_b * m
"""
# todo: vectorize in na/nb
# todo: change this to accept frequency instead of index? Then can use this as helper function for ldft2()
# todo: accept any pattern, not just SIM pattern
xp = cp if cp and isinstance(unit_cell, cp.ndarray) else np
x = xp.asarray(x)
y = xp.asarray(y)
vec_a = xp.asarray(vec_a)
vec_b = xp.asarray(vec_b)
recp_vect_a, recp_vect_b = get_reciprocal_vects(vec_a, vec_b)
frq_vector = na * recp_vect_a + nb * recp_vect_b
# fourier component is integral over unit cell
xxs, yys = xp.meshgrid(x, y)
fcomponent = xp.nansum(unit_cell * xp.exp(-1j*2*np.pi * (frq_vector[0] * xxs + frq_vector[1] * yys)))
# correct phase for start coord of pattern "on" mirrors
# todo: better to include this shift in definition fo unit_cell, and make
# get_sim_unit_cell() accept a phase argument
start_coord = xp.array(vec_b) / nphases * phase_index
phase = xp.angle(fcomponent) - 2 * np.pi * start_coord.dot(frq_vector)
if use_fft_origin:
if dmd_size is None:
raise ValueError("dmd_size was None, but must be specified when use_fft_origin is True")
# now correct for DMD size
# todo: relies on assumption that the cell zero coordinate is placed at DMD[0, 0]
nx, ny = dmd_size
x_pattern = np.arange(nx) - (nx // 2)
y_pattern = np.arange(ny) - (ny // 2)
# center coordinate in the edge coordinate system
center_coord = xp.array([-x_pattern[0], -y_pattern[0]])
phase = phase + 2 * np.pi * center_coord.dot(frq_vector)
fcomponent = xp.abs(fcomponent) * xp.exp(1j * phase)
return fcomponent[0], frq_vector
def get_efield_fourier_components(unit_cell: np.ndarray,
x: np.ndarray,
y: np.ndarray,
vec_a: np.ndarray,
vec_b: np.ndarray,
nphases: int,
phase_index: int,
dmd_size: Sequence[int],
nmax: int = 20,
use_fft_origin: bool = True,
ctf: Optional[np.ndarray] = None):
"""
Generate many Fourier components of pattern
:param unit_cell:
:param x:
:param y:
:param vec_a:
:param vec_b:
:param nphases:
:param phase_index:
:param dmd_size:
:param nmax:
:param use_fft_origin:
:param ctf: coherent transfer function to apply
:return efield, ns, ms, vecs: evaluated at the frequencyes vecs = ns * recp_va + ms * recp_vb
"""
warn("get_efield_fourier_components() is deprecated in favor of ldft2()")
if ctf is None:
def ctf(fx, fy): return 1
rva, rvb = get_reciprocal_vects(vec_a, vec_b)
# first, get electric field fourier components
ns = np.arange(-nmax, nmax + 1)
ms = np.arange(-nmax, nmax + 1)
ninds = 2 * nmax + 1
vecs = np.zeros((ninds, ninds, 2))
efield_fc = np.zeros((ninds, ninds), dtype=complex)
# calculate half of values, as can get other half with E(-f) = E^*(f)
for ii in range(nmax, len(ns)):
for jj in range(len(ms)):
# maximum pattern size is f = 0.5 1/mirrors, after this Fourier transform repeats information
v = rva * ns[ii] + rvb * ms[jj]
# if np.linalg.norm(v) > 1:
# if np.linalg.norm(v) > 0.5:
if np.abs(v[0]) > 0.5 or np.abs(v[1]) > 0.5:
efield_fc[ii, jj] = 0
vecs[ii, jj] = v[:, 0]
else:
efield_fc[ii, jj], v = get_pattern_fourier_component(unit_cell,
x,
y,
vec_a,
vec_b,
ns[ii],
ms[jj],
nphases,
phase_index,
use_fft_origin=use_fft_origin,
dmd_size=dmd_size)
vecs[ii, jj] = v[:, 0]
# E(-f) = E^*(f)
efield_fc[:nmax] = np.flip(efield_fc[nmax + 1:], axis=(0, 1)).conj()
vecs[:nmax] = -np.flip(vecs[nmax + 1:], axis=(0, 1))
# apply OTF
efield_fc = efield_fc * ctf(vecs[:, :, 0], vecs[:, :, 1])
# divide by volume of unit cell (i.e. maximum possible Fourier component)