-
Notifications
You must be signed in to change notification settings - Fork 13
/
simulate_dmd.py
2350 lines (1937 loc) · 92.6 KB
/
simulate_dmd.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
"""
Tools for simulating diffraction from a digital micromirror device (DMD).
There are three important effects to consider:
(1) diffraction from the underlying DMD diffraction grating
(2) diffraction from whatever pattern of mirrors the DMD displays
(3) an efficiency envelope imposed by the diffraction from each mirror individually. This envelope is peaked
at the specular reflection condition for the mirror. When light diffracts in the same direction as the peak,
we say the blaze condition is satisfied.
The simulate_dmd_dft() function is the most useful function for computing all three effects. Given
geometry information (input direction, DMD pitch, diffraction order of interest, etc.) and a mirror pattern,
this provides the diffracted electric field at a number of angles, where the angles are related to the DFT
frequencies. In some sense, this provides the complete information about the diffraction pattern. Other angles
can be generated through exact sinc interpolation (i.e. DFT analog of the Shannon-Whittaker interpolation formula).
This interpolation can be performed using interpolate_dmd_data() for arbitrary angles. Doing this interpolation
is mostly useful for understanding Fourier broadening of diffraction peaks.
For direct simulation of arbitrary output angles, the simulate_dmd() function performs a brute force simulation
which is essentially an O(n^2) numerical discrete Fourier transform (DFT) plus the effect of the blaze envelope.
This is vastly less efficient than simulate_dmd_dft(), since the FFT algorithm is O(nlog(n)). It essentially
provides the same services as the combination of simulate_dmd_dft() and interpolate_dmd_data()
When designing a DMD system, the most important questions are how to set the input and output angles in such
a way that the blaze condition is satisfied. Many of the other tools provided here can be used to answer these
questions. For example, find_combined_condition() determines what pairs of input/output angles satisfy both
the blaze and diffraction condition. solve_1color_1d() is a wrapper which solves the same problem along the x-y
direction (i.e. for typical operation of the DMD). get_diffracted_output_uvec() computes the angles of diffraction
orders for a given input direction. etc...
When simulating a periodic pattern such as used in Structured Illumination Microscopy (SIM), the tools found in
dmd_pattern.py may be more suitable.
Coordinate systems
####################
We adopt a coordinate system with x- and y- axes along the primary axes of the DMD chip (i.e. determined
by the periodic mirror array), and z- direction is positive pointing away from the DMD face. This way the unit
vectors describing the direction of an incoming plane waves has negative z-component, and the unit vector of
an outgoing plane wave has positive z-component. We typically suppose the mirrors swivel about the axis
n = [1, 1, 0]/sqrt(2), i.e. diagonal to the DMD axes, by angle +/- gamma. This ensures that light incident in
the x-y (x minus y) plane stays in plane after diffraction (for the blazed order)
In addition to the xyz coordinate system, we also use two other convenient coordinate systems.
1. the mpz coordinate system:
This coordinate system is convenient for dealing with diffraction from the DMD, as discussed above. Note
that the mirrors swivel about the ep direction
.. math::
e_m = \\frac{e_x - e_y}{\\sqrt{2}}
e_p = \\frac{e_x + e_y}{\\sqrt{2}}
2. the 123 or "mirror" coordinate system:
This coordinate system is specialized to dealing with the blaze condition. Here the unit vector e3 is the normal to the
DMD mirror, e2 is along the (x+y)/sqrt(2) direction, and e1 is orthogonal to these two. Since e3 is normal
to the DMD mirrors this coordinate system depends on the mirror swivel angle.
In whichever coordinate system, if we want to specify directions we have the choice of working with either
unit vectors or an angular parameterization. Typically unit vectors are easier to work with, although angles
may be easier to interpret. We use different angular parameterizations for incoming and outgoing unit vectors.
For example, in the xy coordinate system we use
.. math::
a = a_z * \\left[ \\tan(t^a_x), \\tan(t^a_y), -1 \\right]
b = |b_z| * \\left[ \\tan(t^b_x), \\tan(t^b_y), 1 \\right]
If light is incident towards the DMD as a plane wave from some direction determined by a unit vector, a, then it
is then diffracted into different output directions depending on the spatial frequencies of the DMD pattern.
Call these directions b(f).
If the DMD is tilted, the DMD pattern frequencies f will not exactly match the optical system frequencies.
In particular, although the DMD pattern will have components at f and -f the optical system frequencies will
not be perfectly centered on the optical axis.
"""
from typing import Union, Optional
from collections.abc import Sequence, Callable
from warnings import warn
from pathlib import Path
import numpy as np
from dask import delayed, compute
from dask.diagnostics import ProgressBar
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.figure import Figure
from matplotlib.colors import PowerNorm
from matplotlib.patches import Circle
from localize_psf.rotation import get_rot_mat, get_rot_mat_angle_axis
try:
import cupy as cp
except ImportError:
cp = None
if cp:
array = Union[np.ndarray, cp.ndarray]
else:
array = np.ndarray
# ###########################################
# main simulation functions
# ###########################################
_dlp_1stgen_axis = (1/np.sqrt(2), 1/np.sqrt(2), 0.)
# todo: migrate functions to this class
class DMD:
def __init__(self,
dx: float,
dy: float,
wx: float,
wy: float,
gamma_on: float,
gamma_off: float,
rot_axis_on: Sequence[float] = _dlp_1stgen_axis,
rot_axis_off: Sequence[float] = _dlp_1stgen_axis,
nx: Optional[int] = None,
ny: Optional[int] = None):
"""
:param dx: spacing between DMD pixels in the x-direction. Same units as wavelength.
:param dy: spacing between DMD pixels in the y-direction
:param wx: width of mirrors in the x-direction. Must be <= dx.
:param wy: width of mirrors in y-direction
:param gamma_on: DMD mirror angle in radians
:param gamma_off:
:param rot_axis_on:
:param rot_axis_off:
:param nx:
:param ny:
"""
self.gamma_on = float(gamma_on)
self.gamma_off = float(gamma_off)
self.dx = float(dx)
self.dy = float(dy)
self.wx = float(wx)
self.wy = float(wy)
self.rot_axis_on = np.asarray(rot_axis_on)
self.rot_axis_off = np.asarray(rot_axis_off)
self.size = (ny, nx)
def simulate_pattern(self,
wavelength: float,
pattern: array,
uvec_in: array,
uvecs_out: array,
zshifts: Optional[array] = None,
phase_errs: Optional[array] = None,
efield_profile: Optional[array] = None) -> (array, array, array):
"""
Simulate plane wave diffracted from a digital mirror device (DMD) naively. In most cases this function is not
the most efficient to use! When working with SIM patterns it is much more efficient to rely on the tools
found in dmd_patterns
We assume that the body of the device is in the xy plane with the negative z-unit vector defining the plane's
normal. This means incident unit vectors have positive z-component, and outgoing unit vectors have negative
z-component. We suppose the device has rectangular pixels with sides parallel to the x- and y-axes.
We further suppose a given pixel (centered at (0,0)) swivels about the vector n = [1, 1, 0]/sqrt(2)
by angle gamma, i.e. the direction x-y is the most interesting one.
:param wavelength: choose any units as long as consistent with dx, dy, wx, and wy.
:param pattern: an NxM array. Dimensions of the DMD are determined from this. As usual, the upper left
hand corner if this array represents the smallest x- and y- values
:param uvec_in: (ax, ay, az) direction of plane wave input to DMD
:param uvecs_out: N x 3 array. Output unit vectors where diffraction should be computed.
:param zshifts: if DMD is assumed to be non-flat, give height profile here. Array of the same size as pattern
:param phase_errs: direct phase errors per mirror. This is an alternative way to provide aberration information
compared with zshifts
:param efield_profile: electric field values (amplitude and phase) across the DMD
:return efields, sinc_efield_on, sinc_efield_off:
"""
if cp and isinstance(pattern, cp.ndarray):
xp = cp
else:
xp = np
# check input arguments are sensible
if not pattern.dtype == bool:
raise TypeError('pattern must be of type bool.')
if zshifts is None:
zshifts = xp.zeros(pattern.shape)
zshifts = xp.array(zshifts)
if phase_errs is None:
phase_errs = xp.zeros(pattern.shape)
phase_errs = xp.asarray(phase_errs)
if efield_profile is None:
efield_profile = xp.ones(pattern.shape)
efield_profile = xp.array(efield_profile)
uvec_in = xp.array(uvec_in)
uvecs_out = xp.atleast_2d(uvecs_out)
# DMD pixel coordinates using origin at center FFT convention
ny, nx = pattern.shape
mxmx, mymy = xp.meshgrid(xp.arange(nx) - nx // 2,
xp.arange(ny) - ny // 2)
# function to do computation for each output unit vector
def calc_output_angle(bvec):
# incoming minus outgoing unit vectors
bma = bvec - uvec_in.squeeze()
# efield phase for each DMD pixel
efield_per_mirror = efield_profile * \
xp.exp(-1j * 2 * np.pi / wavelength * (self.dx * mxmx * bma[0] +
self.dy * mymy * bma[1] +
zshifts * bma[2]) +
1j * phase_errs)
# get envelope functions for "on" and "off" states
sinc_efield_on = self.wx * self.wy * blaze_envelope(wavelength,
self.gamma_on,
self.wx,
self.wy,
bma,
self.rot_axis_on)
sinc_efield_off = self.wx * self.wy * blaze_envelope(wavelength,
self.gamma_off,
self.wx,
self.wy,
bma,
self.rot_axis_off)
# final summation
efs = xp.sum(efield_per_mirror * (sinc_efield_on * pattern + sinc_efield_off * (1 - pattern)))
return efs, sinc_efield_on, sinc_efield_off
# get shape want output arrays to be
output_shape = uvecs_out.shape[:-1]
# reshape bvecs to iterate over
bvecs_to_iterate = xp.reshape(uvecs_out, (np.prod(output_shape), 3))
# simulate
with ProgressBar():
r = [delayed(calc_output_angle)(bvec) for bvec in bvecs_to_iterate]
results = compute(*r)
# unpack results for all output directions
efields, sinc_efield_on, sinc_efield_off = zip(*results)
efields = xp.asarray(efields).reshape(output_shape)
sinc_efield_on = xp.asarray(sinc_efield_on).reshape(output_shape)
sinc_efield_off = xp.asarray(sinc_efield_off).reshape(output_shape)
return efields, sinc_efield_on, sinc_efield_off
def simulate_pattern_dft(self,
wavelength: float,
pattern: array,
efield_profile: array,
uvec_in,
order: Sequence[int],
dn_orders: int = 0):
"""
Simulate DMD diffraction using DFT. These produces peaks at a discrete set of frequencies which are
(b-a)_x = wavelength / dx * ix / nx for ix = 0, ... nx - 1
(b-a)_y = wavelength / dy * iy / ny for iy = 0, ... ny - 1
these contain the full information of the output field. Intermediate values can be generated by (exact)
interpolation using the DFT analog of the Shannon-Whittaker interpolation formula.
:param wavelength:
:param pattern:
:param efield_profile: illumination profile, which can include intensity and phase errors
:param uvec_in:
:param order: (nx, ny)
:param dn_orders: number of orders along nx and ny to compute around the central order of interest
:return efields, sinc_efield_on, sinc_efield_off, b:
"""
if cp and isinstance(pattern, cp.ndarray):
xp = cp
else:
xp = np
efield_profile = xp.array(efield_profile)
uvec_in = xp.array(uvec_in)
ny, nx = pattern.shape
# get allowed diffraction orders
orders = np.stack(np.meshgrid(range(order[0] - dn_orders, order[0] + dn_orders + 1),
range(order[1] - dn_orders, order[1] + dn_orders + 1)), axis=-1)
order_xlims = [np.nanmin(orders[..., 0]), np.nanmax(orders[..., 0])]
nx_orders = xp.arange(order_xlims[0], order_xlims[1] + 1)
order_ylims = [np.nanmin(orders[..., 1]), np.nanmax(orders[..., 1])]
ny_orders = xp.arange(order_ylims[0], order_ylims[1] + 1)
# dft freqs
fxs = xp.fft.fftshift(xp.fft.fftfreq(nx))
fys = xp.fft.fftshift(xp.fft.fftfreq(ny))
fxfx, fyfy = xp.meshgrid(fxs, fys)
# to get effective frequencies, add diffraction orders
# b_x = (b-a)_x + a_x
uvecs_out_dft = xp.zeros((len(ny_orders) * ny, len(nx_orders) * nx, 3))
uvecs_out_dft[..., 0] = (xp.tile(fxfx, [len(ny_orders), len(nx_orders)]) +
xp.kron(nx_orders, xp.ones((ny * len(nx_orders), nx)))) * wavelength / self.dx + \
uvec_in.squeeze()[0]
# b_y = (b-a)_y + a_y
uvecs_out_dft[..., 1] = (xp.tile(fyfy, [len(ny_orders), len(nx_orders)]) +
xp.kron(np.expand_dims(ny_orders, axis=1),
xp.ones((ny, nx * len(ny_orders))))) * wavelength / self.dy + \
uvec_in.squeeze()[1]
# b_z from normalization
uvecs_out_dft[..., 2] = xp.sqrt(1 - uvecs_out_dft[..., 0] ** 2 - uvecs_out_dft[..., 1] ** 2)
# get envelope functions for "on" and "off" states
sinc_efield_on = self.wx * self.wy * blaze_envelope(wavelength,
self.gamma_on,
self.wx,
self.wy,
uvecs_out_dft - uvec_in,
self.rot_axis_on)
sinc_efield_off = self.wx * self.wy * blaze_envelope(wavelength,
self.gamma_off,
self.wx,
self.wy,
uvecs_out_dft - uvec_in,
self.rot_axis_off)
pattern_dft = xp.fft.fftshift(xp.fft.fft2(xp.fft.ifftshift(pattern * efield_profile)))
pattern_complement_dft = xp.fft.fftshift(xp.fft.fft2(xp.fft.ifftshift((1 - pattern) * efield_profile)))
# efields = pattern_dft * sinc_efield_on + pattern_complement_dft * sinc_efield_off
efields_on = xp.tile(pattern_dft, [len(nx_orders), len(ny_orders)]) * sinc_efield_on
efields_off = xp.tile(pattern_complement_dft, [len(nx_orders), len(ny_orders)]) * sinc_efield_off
efields = efields_on + efields_off
return efields, pattern_dft, pattern_complement_dft, sinc_efield_on, sinc_efield_off, uvecs_out_dft
def interpolate_pattern_data(self,
wavelength: float,
pattern: array,
efield_profile: array,
uvec_in,
order,
bvecs_interp):
"""
Exact interpolation of dmd diffraction DFT data to other output angles using
Shannon-Whittaker interpolation formula.
todo: don't expect this to be any more efficient than simulate_dmd(), but should give the same result
todo: possible way to speed up interpolation is with FT Fourier shift theorem. So approach would be to
todo: choose certain shifts (e.g. make n-times denser and compute n^2 shift theorems)
:param wavelength:
:param pattern:
:param efield_profile:
:param uvec_in:
:param order:
:param bvecs_interp:
:return efields:
"""
if cp and isinstance(pattern, cp.ndarray):
xp = cp
else:
xp = np
bvecs_interp = xp.atleast_2d(bvecs_interp)
uvec_in = xp.atleast_2d(uvec_in)
# get DFT results
_, pattern_dft, pattern_dft_complement, _, _, bvec_dft = self.simulate_pattern_dft(wavelength,
pattern,
efield_profile,
uvec_in,
order,
dn_orders=0)
ny, nx = pattern.shape
# dft freqs
fxs = xp.fft.fftshift(xp.fft.fftfreq(nx))
fys = xp.fft.fftshift(xp.fft.fftfreq(ny))
bma = bvecs_interp - uvec_in
sinc_efield_on = self.wx * self.wy * blaze_envelope(wavelength,
self.gamma_on,
self.wx,
self.wy,
bma,
self.rot_axis_on)
sinc_efield_off = self.wx * self.wy * blaze_envelope(wavelength,
self.gamma_off,
self.wx,
self.wy,
bma,
self.rot_axis_off)
def dft_interp_1d(d, v, n, frqs):
arg = frqs - d / wavelength * v
# val = 1 / n * np.sin(np.pi * arg * n) / np.sin(np.pi * arg) * np.exp(np.pi * 1j * arg * (n - 1))
if xp.mod(n, 2) == 1:
val = 1 / n * xp.sin(np.pi * arg * n) / xp.sin(np.pi * arg)
else:
val = 1 / n * xp.sin(np.pi * arg * n) / xp.sin(np.pi * arg) * xp.exp(-np.pi * 1j * arg)
val[np.mod(np.round(arg, 14), 1) == 0] = 1
return val
nvecs = np.prod(bvecs_interp.shape[:-1])
output_shape = bvecs_interp.shape[:-1]
def calc(ii):
ind = np.unravel_index(ii, output_shape)
val = xp.sum((pattern_dft * sinc_efield_on[ind] + pattern_dft_complement * sinc_efield_off[ind]) *
xp.expand_dims(dft_interp_1d(self.dx, bma[ind][0], nx, fxs), axis=0) *
xp.expand_dims(dft_interp_1d(self.dy, bma[ind][1], ny, fys), axis=1))
return val
with ProgressBar():
results = compute(*[delayed(calc)(ii) for ii in range(nvecs)])
efields = xp.array(results).reshape(output_shape)
return efields
def get_diffracted_power(self,
wavelength: float,
pattern: array,
efield_profile: array,
uvec_in):
"""
Compute input and output power.
:param wavelength:
:param pattern:
:param efield_profile:
:param uvec_in:
:return power_in, power_out:
"""
ny, nx = pattern.shape
ax, ay, az = uvec_in.ravel()
power_in = np.sum(np.abs(efield_profile) ** 2)
(_, pattern_dft,
pattern_complement_dft,
sinc_efield_on,
sinc_efield_off,
uvecs_out_dft) = self.simulate_pattern_dft(wavelength,
pattern,
efield_profile,
uvec_in,
order=(0, 0),
dn_orders=0)
# check that power is conserved here...
assert np.abs(np.sum(np.abs(pattern_dft + pattern_complement_dft) ** 2) / (nx * ny) - power_in) < 1e-12
# get FFT freqs
fxs = (uvecs_out_dft[..., 0] - ax) * self.dx / wavelength
fys = (uvecs_out_dft[..., 1] - ay) * self.dy / wavelength
# get allowed diffraction orders
ns, allowed_dc, allowed_any = self.get_physical_diff_orders(uvec_in, wavelength)
def calc_power_order(order):
ox, oy = order
bxs = ax + wavelength / self.dx * (fxs + ox)
bys = ay + wavelength / self.dy * (fys + oy)
with np.errstate(invalid="ignore"):
bzs = np.sqrt(1 - bxs ** 2 - bys ** 2)
bvecs = np.stack((bxs, bys, bzs), axis=-1)
bvecs[bxs ** 2 + bys ** 2 > 1] = np.nan
envelope_on = blaze_envelope(wavelength,
self.gamma_on,
self.wx,
self.wy,
bvecs - uvec_in,
self.rot_axis_on)
envelope_off = blaze_envelope(wavelength,
self.gamma_off,
self.wx,
self.wy,
bvecs - uvec_in,
self.rot_axis_off)
on_sum = np.nansum(envelope_on ** 2)
off_sum = np.nansum(envelope_off ** 2)
pout = np.nansum(np.abs(envelope_on * pattern_dft + envelope_off * pattern_complement_dft) ** 2) / (nx * ny)
return pout, on_sum, off_sum
orders_x = ns[allowed_any, 0]
orders_y = ns[allowed_any, 1]
with ProgressBar():
r = [delayed(calc_power_order)((orders_x[ii], orders_y[ii])) for ii in range(len(orders_x))]
results = compute(*r)
power_out_orders, on_sum_orders, off_sum_orders = zip(*results)
power_out = np.sum(power_out_orders)
return power_in, power_out
def get_physical_diff_orders(self,
uvec_in,
wavelength: float):
"""
Determine which diffraction orders are physically supported by the grating given a certain input direction
:param uvec_in:
:param wavelength:
:return ns, allowed_dc, allowed_any: n x n x 2 array, where ns[ii, jj] = np.array([nx[ii, jj], ny[ii, jj]).
allowed_dc and allowed_any are boolean arrays which indicate which ns have forbidden DC values and which ns
have all forbidden diffraction orders
"""
# todo: maybe better to remove the uvec_in argument and return values allowed for any vecs
ax, ay, az = uvec_in.ravel()
nx_max = int(np.floor(self.dx / wavelength * (1 - ax))) + 1
nx_min = int(np.ceil(self.dx / wavelength * (-1 - ax))) - 1
ny_max = int(np.floor(self.dy / wavelength * (1 - ay))) + 1
ny_min = int(np.ceil(self.dy / wavelength * (-1 - ay))) - 1
nxnx, nyny = np.meshgrid(range(nx_min, nx_max + 1), range(ny_min, ny_max + 1))
nxnx = nxnx.astype(float)
nyny = nyny.astype(float)
# check which DC orders are allowed
bx = ax + wavelength / self.dx * nxnx
by = ay + wavelength / self.dy * nyny
allowed_dc = bx ** 2 + by ** 2 <= 1
# check corner diffraction orders
bx_c1 = ax + wavelength / self.dx * (nxnx + 0.5)
by_c1 = ay + wavelength / self.dy * (nyny + 0.5)
bx_c2 = ax + wavelength / self.dx * (nxnx + 0.5)
by_c2 = ay + wavelength / self.dy * (nyny - 0.5)
bx_c3 = ax + wavelength / self.dx * (nxnx - 0.5)
by_c3 = ay + wavelength / self.dy * (nyny + 0.5)
bx_c4 = ax + wavelength / self.dx * (nxnx - 0.5)
by_c4 = ay + wavelength / self.dy * (nyny - 0.5)
allowed_any = np.logical_or.reduce((bx_c1 ** 2 + by_c1 ** 2 <= 1,
bx_c2 ** 2 + by_c2 ** 2 <= 1,
bx_c3 ** 2 + by_c3 ** 2 <= 1,
bx_c4 ** 2 + by_c4 ** 2 <= 1))
ns = np.stack((nxnx, nyny), axis=-1)
return ns, allowed_dc, allowed_any
def plot(self,
vects: Sequence[np.ndarray],
colors: Optional[Sequence[str]] = None,
labels: Optional[Sequence[str]] = None,
figh: Figure = None,
ax=None,
plot_on_mirror_normal: bool = True,
plot_off_mirror_normal: bool = False,
table: Optional[str] = None,
azimuth: float = -135.,
elevation: float = 115.,
**kwargs):
"""
Plot 3D DMD geometry
:param vects: sequence of arrays of length 3. Incoming and outgoing directions on DMD
:param colors: sequence of colors used when plotting vects
:param labels: labels for each vect
:param figh:
:param ax:
:param plot_on_mirror_normal:
:param plot_off_mirror_normal:
:param table: None, "xz" or "mz"
:param kwargs: passed through to figure
:return figh, ax:
"""
if isinstance(vects, np.ndarray):
if vects.ndim == 1:
vects = (vects,)
if figh is None:
figh = plt.figure(**kwargs)
figh.suptitle("DMD geometry")
if ax is None:
ax = figh.add_subplot(1, 1, 1, projection="3d")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_zlabel("z")
# set initial view
ax.azim = azimuth
ax.elev = elevation
# draw DMD outline
ny, nx = self.size
if ny is not None and nx is not None:
aspect = ny / nx
else:
aspect = 1
# draw table
if table == "xz":
ax.add_collection3d(Poly3DCollection([[(0, 0, 0),
(1, 0, 0),
(1, 0, 0.5),
(0, 0, 0.5)
]],
alpha=0.25,
color='k'))
elif table == "mz":
ax.add_collection3d(Poly3DCollection([[(-0.5 / np.sqrt(2), 0.5 / np.sqrt(2), 0),
(0.5 / np.sqrt(2), -0.5 / np.sqrt(2), 0),
(0.5 / np.sqrt(2), -0.5 / np.sqrt(2), 0.5),
(-0.5 / np.sqrt(2), 0.5 / np.sqrt(2), 0.5),
]],
alpha=0.25,
color='k'))
else:
pass
# draw DMD
ax.add_collection3d(Poly3DCollection([[(0, 0, 0),
(1, 0, 0),
(1, aspect, 0),
(0, aspect, 0)
]],
alpha=0.5,
color='k'))
# ax.plot([0, 1, 1, 0, 0],
# [0, 0, aspect, aspect, 0],
# [0, 0, 0, 0, 0],
# 'k',
# label="DMD")
ax.plot([0, .1],
[0, .1],
[0, 0],
'sandybrown',
label="$e_p$")
ax.plot([0, .1],
[0, -.1],
[0, 0],
'darkseagreen',
label="$e_m$")
ax.plot([0, 0],
[0, 0],
[0, np.sqrt(2) * 0.1],
'steelblue',
label="$e_z$")
if plot_on_mirror_normal:
on_normal = get_rot_mat(self.rot_axis_on, self.gamma_on).dot(np.array([[0], [0], [1]])).ravel()
ax.plot([0.5, 0.5 + on_normal[0]],
[0.5 * aspect, 0.5 * aspect + on_normal[1]],
[0, on_normal[2]],
'turquoise',
label="'+' mirror normal")
if plot_off_mirror_normal:
off_normal = get_rot_mat(self.rot_axis_off, self.gamma_off).dot(np.array([[0], [0], [1]])).ravel()
ax.plot([0.5, 0.5 + off_normal[0]],
[0.5 * aspect, 0.5 * aspect + off_normal[1]],
[0, off_normal[2]],
'mediumspringgreen',
label="'-' mirror normal")
# draw incoming/outgoing vectors
for ii, v in enumerate(vects):
if v[2] >= 0:
coords = ([0.5, 0.5 + v[0]],
[0.5 * aspect, 0.5 * aspect + v[1]],
[0, v[2]])
else:
coords = ([0.5, 0.5 + -v[0]],
[0.5 * aspect, 0.5 * aspect + -v[1]],
[0, -v[2]])
ax.plot(*coords,
c=None if colors is None else colors[ii],
label=None if labels is None else labels[ii])
ax.axis('equal')
ax.legend()
return figh, ax
class DLP7000(DMD):
def __init__(self):
super(DLP7000, self).__init__(13.68,
13.68,
13.68,
13.68,
12 * np.pi / 180,
-12 * np.pi / 180,
rot_axis_on=_dlp_1stgen_axis,
rot_axis_off=_dlp_1stgen_axis,
nx=1024,
ny=768)
class DLP6500(DMD):
def __init__(self):
super(DLP6500, self).__init__(7.56,
7.56,
7.56 * np.sqrt(0.92),
7.56 * np.sqrt(0.92),
12 * np.pi/180,
-12 * np.pi/180,
rot_axis_on=_dlp_1stgen_axis,
rot_axis_off=_dlp_1stgen_axis,
nx=1920,
ny=1080)
class DLP5500(DMD):
def __init__(self):
super(DLP5500, self).__init__(10.8,
10.8,
10.8,
10.8,
12 * np.pi / 180,
-12 * np.pi / 180,
rot_axis_on=_dlp_1stgen_axis,
rot_axis_off=_dlp_1stgen_axis,
nx=1024,
ny=768)
class DLP4710(DMD):
"""
For details, see TI document DLP47100.47 1080p DMD datasheet (Rev. C)
available at https://www.ti.com/product/DLP4710
Figure 6-15 describes the geometry
"""
def __init__(self):
r1 = get_rot_mat(np.array([1 / np.sqrt(2), -1 / np.sqrt(2), 0]), 12 * np.pi / 180)
r2 = get_rot_mat(np.array([1 / np.sqrt(2), 1 / np.sqrt(2), 0]), 12 * np.pi / 180)
# composite rotations
r_on = r2.dot(r1)
r_off = r2.transpose().dot(r1)
# convert on rotation matrix to equivalent rotation about single axis
rot_axis_on, gamma_on = get_rot_mat_angle_axis(r_on)
rot_axis_off, gamma_off = get_rot_mat_angle_axis(r_off)
super(DLP4710, self).__init__(5.4,
5.4,
5.4,
5.4,
gamma_on,
gamma_off,
rot_axis_on=rot_axis_on,
rot_axis_off=rot_axis_off,
nx=1920,
ny=1080)
# ###########################################
# misc helper functions
# ###########################################
def sinc_fn(x: array) -> array:
"""
Unnormalized sinc function, sinc(x) = sin(x) / x
:param x:
:return sinc(x):
"""
if cp and isinstance(x, cp.ndarray):
xp = cp
else:
xp = np
x = xp.atleast_1d(x)
with np.errstate(divide='ignore', invalid='ignore'):
y = xp.asarray(xp.sin(x) / x)
y[x == 0] = 1
return y
# ###########################################
# convert between coordinate systems
# ###########################################
def xyz2mirror(vx: array,
vy: array,
vz: array,
gamma: float,
rot_axis: Sequence[float, float, float]) -> (array, array, array):
"""
Convert vector with components vx, vy, vz to v1, v2, v3.
The unit vectors ex, ey, ez are defined along the axes of the DMD body,
whereas the unit vectors e1, e2, e3 are given by
e1 = (ex - ey) / sqrt(2) * cos(gamma) - ez * sin(gamma)
e2 = (ex + ey) / sqrt(2)
e3 = (ex - ey) / sqrt(2) sin(gamma) + ez * cos(gamma)
which are convenient because e1 points along the direction the micromirrors swivel and
e3 is normal to the DMD micrmirrors
:param vx:
:param vy:
:param vz:
:param gamma:
:param rot_axis:
:return: v1, v2, v3
"""
rot_mat = get_rot_mat(rot_axis, gamma)
# v_{123} = R^{-1} * v_{xyz}
# v1 = e1 \cdot v = vx * e1 \cdot ex + vy * e1 \cdot ey + vz * e1 \cdot ez
v1 = vx * rot_mat[0, 0] + vy * rot_mat[1, 0] + vz * rot_mat[2, 0]
v2 = vx * rot_mat[0, 1] + vy * rot_mat[1, 1] + vz * rot_mat[2, 1]
v3 = vx * rot_mat[0, 2] + vy * rot_mat[1, 2] + vz * rot_mat[2, 2]
return v1, v2, v3
def mirror2xyz(v1: Union[array, float],
v2: Union[array, float],
v3: Union[array, float],
gamma: float,
rot_axis: Sequence[float, float, float]) -> (array, array, array):
"""
Inverse function for xyz2mirror()
:param v1:
:param v2:
:param v3:
:param gamma:
:param rot_axis:
:return:
"""
rot_mat = get_rot_mat(rot_axis, gamma)
# v_{xyz} = R * v_{123}
# vx = ex \cdot v = v1 * ex \cdot e1 + v2 * ex \cdot e2 + v3 * ex \cdot e3
vx = v1 * rot_mat[0, 0] + v2 * rot_mat[0, 1] + v3 * rot_mat[0, 2]
vy = v1 * rot_mat[1, 0] + v2 * rot_mat[1, 1] + v3 * rot_mat[1, 2]
vz = v1 * rot_mat[2, 0] + v2 * rot_mat[2, 1] + v3 * rot_mat[2, 2]
return vx, vy, vz
def xyz2mpz(vx: array,
vy: array,
vz: array) -> (array, array, array):
"""
Convert from x, y, z coordinate system to m = (x-y)/sqrt(2), p = (x+y)/sqrt(2), z
:param vx:
:param vy:
:param vz:
:return vm, vp, vz:
"""
vp = np.array(vx + vy) / np.sqrt(2)
vm = np.array(vx - vy) / np.sqrt(2)
vz = np.array(vz, copy=True)
return vm, vp, vz
def mpz2xyz(vm: array,
vp: array,
vz: array) -> (array, array, array):
"""
Convert from m = (x-y)/sqrt(2), p = (x+y)/sqrt(2), z coordinate system to x, y, z
:param vm:
:param vp:
:param vz:
:return, vx, vy, vz:
"""
vx = np.array(vm + vp) / np.sqrt(2)
vy = np.array(vp - vm) / np.sqrt(2)
vz = np.array(vz, copy=True)
return vx, vy, vz
# ###########################################
# convert between different angular or unit vector representations of input and output directions
# ###########################################
def angle2xy(tp: Union[array, float],
tm: Union[array, float]) -> (array, array):
"""
Convert angle projections along the x- and y-axis to angle projections along the p=(x+y)/sqrt(2)
and m=(x-y)/sqrt(2) axis.
:param tp:
:param tm:
:return tx, ty:
"""
tx = np.arctan((np.tan(tp) + np.tan(tm)) / np.sqrt(2))
ty = np.arctan((np.tan(tp) - np.tan(tm)) / np.sqrt(2))
return tx, ty
def angle2pm(tx: array,
ty: array) -> (array, array):
"""
Convert angle projections along the p=(x+y)/sqrt(2) and m=(x-y)/sqrt(2) to x and y axes.
:param tx:
:param ty:
:return tp, tm:
"""
tm = np.arctan((np.tan(tx) - np.tan(ty)) / np.sqrt(2))
tp = np.arctan((np.tan(tx) + np.tan(ty)) / np.sqrt(2))
return tp, tm
def uvector2txty(vx: array,
vy: array,
vz: array) -> (array, array):
"""
Convert unit vector from components to theta_x, theta_y representation. Inverse function for get_unit_vector()
NOTE: tx and ty are defined differently depending on the sign of the z-component of the unit vector
:param vx:
:param vy:
:param vz:
:return:
"""
norm_factor = np.abs(1 / vz)
tx = np.arctan(vx * norm_factor)
ty = np.arctan(vy * norm_factor)
return tx, ty
def uvector2tmtp(vx: array,
vy: array,
vz: array) -> (array, array):
"""
Convert unit vector to angle projections along ep and em
:param vx:
:param vy:
:param vz:
:return tp, tm:
"""
tx, ty = uvector2txty(vx, vy, vz)
tp, tm = angle2pm(tx, ty)
return tp, tm
def pm2uvector(tm: array,
tp: array,
incoming: bool) -> (array, array, array):
"""
:param tm:
:param tp:
:param incoming:
:return:
"""
tx, ty = angle2xy(tp, tm)
return xy2uvector(tx, ty, incoming)
def xy2uvector(tx: array,
ty: array,
incoming: bool) -> (array, array, array):
"""
Get incoming or outgoing unit vector of light propagation parameterized by angles tx and ty
Let a represent an incoming vector, and b and outgoing one. We parameterize these by
a = az * [tan(tx_a), tan(ty_a), -1]
b = |bz| * [tan(tb_x), tan(tb_y), 1]
choosing negative z component for outgoing vectors is effectively taking a different
conventions for the angle between b and the z axis (compared with a and
the z-axis). We do this so that e.g. the law of reflection would give
theta_a = theta_b, instead of theta_a = -theta_b, which would hold if we
defined everything symmetrically.
:param tx: arbitrary size
:param ty: same size as tx
:param incoming: true if representing a vector pointing in the negative z-direction, i.e.
incident on the DMD
:return uvec: unit vectors, array of size tx.size x 3
"""
tx = np.atleast_1d(tx)
ty = np.atleast_1d(ty)