structured-optics 0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- structured_optics/__init__.py +3 -0
- structured_optics/algebra_utils.py +90 -0
- structured_optics/hologram.py +70 -0
- structured_optics/modes.py +234 -0
- structured_optics/prop_methods.py +12 -0
- structured_optics/slmdisplay.py +85 -0
- structured_optics/struct_opt.py +357 -0
- structured_optics/utils.py +306 -0
- structured_optics-0.1.dist-info/METADATA +10 -0
- structured_optics-0.1.dist-info/RECORD +12 -0
- structured_optics-0.1.dist-info/WHEEL +5 -0
- structured_optics-0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from structured_optics.utils import overlap
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def hg_proj(Beam,N, completeness = False):
|
|
7
|
+
overlaps = np.zeros((N+1, N+1), dtype='complex')
|
|
8
|
+
auxiliary_beam = Beam.copy_clean()
|
|
9
|
+
for n in range(N+1):
|
|
10
|
+
for m in range(N+1):
|
|
11
|
+
auxiliary_beam.hg(n,m)
|
|
12
|
+
overlaps[n,m] = overlap(Beam, auxiliary_beam)
|
|
13
|
+
n, m = np.meshgrid(np.arange(0, N+1, 1), np.arange(0, N+1, 1))
|
|
14
|
+
comp = np.sum(np.abs(overlaps)**2)
|
|
15
|
+
if completeness == True:
|
|
16
|
+
return n, m, overlaps, comp
|
|
17
|
+
else:
|
|
18
|
+
return n, m, overlaps
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def lg_proj(Beam, N, completeness = False):
|
|
22
|
+
overlaps = np.zeros((int(2*N)+1, int(N/2)+1), dtype='complex')
|
|
23
|
+
auxiliary_beam = Beam.copy_clean()
|
|
24
|
+
for l in np.arange(-N, N+1, 1):
|
|
25
|
+
for p in range(int(N/2)+1):
|
|
26
|
+
auxiliary_beam.lg(l,p)
|
|
27
|
+
overlaps[l+N,p] = overlap(Beam, auxiliary_beam)
|
|
28
|
+
p, l = np.meshgrid(np.arange(0, int(N/2)+1, 1), np.arange(-N, N+1, 1))
|
|
29
|
+
comp = np.sum(np.abs(overlaps)**2)
|
|
30
|
+
if completeness == True:
|
|
31
|
+
return l, p, overlaps, comp
|
|
32
|
+
else:
|
|
33
|
+
return l, p, overlaps
|
|
34
|
+
|
|
35
|
+
def hg_basis(Beam, N, waist = None, norm1 = False):
|
|
36
|
+
aux = Beam.copy_clean()
|
|
37
|
+
if waist != None:
|
|
38
|
+
aux.waist = waist
|
|
39
|
+
basis = np.empty((N+1, N+1, Beam.Dy, Beam.Dx), dtype = 'complex')
|
|
40
|
+
for n in range(N+1):
|
|
41
|
+
for m in range(N+1):
|
|
42
|
+
aux.hg(n, m)
|
|
43
|
+
basis[n, m] = aux.field
|
|
44
|
+
if norm1 == True:
|
|
45
|
+
basis = basis/np.max(basis)
|
|
46
|
+
return basis
|
|
47
|
+
|
|
48
|
+
def lg_basis(Beam, N, waist=None, norm1=False):
|
|
49
|
+
aux = Beam.copy_clean()
|
|
50
|
+
if waist != None:
|
|
51
|
+
aux.waist = waist
|
|
52
|
+
basis = np.empty((N+1, N+1, Beam.Dy, Beam.Dx), dtype = 'complex')
|
|
53
|
+
for n in range(N+1):
|
|
54
|
+
for m in range(N+1):
|
|
55
|
+
l = m-n
|
|
56
|
+
p = min(n,m)
|
|
57
|
+
aux.lg(l, p)
|
|
58
|
+
basis[n, m] = aux.field
|
|
59
|
+
if norm1 == True:
|
|
60
|
+
basis = basis/np.max(basis)
|
|
61
|
+
return basis
|
|
62
|
+
|
|
63
|
+
def bessel_basis(Beam, Nmax, waist=None, norm1=False):
|
|
64
|
+
aux = Beam.copy_clean()
|
|
65
|
+
if waist is not None:
|
|
66
|
+
aux.waist = waist
|
|
67
|
+
# basis[n] = Bessel beam of order n
|
|
68
|
+
basis = np.empty((2*Nmax+1, aux.Dy, aux.Dx), dtype='complex')
|
|
69
|
+
for n in range(2*Nmax+1):
|
|
70
|
+
aux.bessel(n-Nmax) # or aux.bessel(n, theta=...)
|
|
71
|
+
basis[n] = aux.field
|
|
72
|
+
if norm1:
|
|
73
|
+
basis = basis / np.max(np.abs(basis))
|
|
74
|
+
return basis
|
|
75
|
+
|
|
76
|
+
def build_from_coefs_and_basis(Beam, coefs, basis):
|
|
77
|
+
N = len(coefs)
|
|
78
|
+
aux = Beam.copy_clean()
|
|
79
|
+
for i in range(N):
|
|
80
|
+
for j in range(N):
|
|
81
|
+
aux.field = aux.field + coefs[i,j] * basis[i,j]
|
|
82
|
+
Beam.field = aux.field
|
|
83
|
+
return Beam
|
|
84
|
+
|
|
85
|
+
def astigmatic_mode_converter(Beam, N, theta):
|
|
86
|
+
n, m, overlaps = Beam.hg_projector(N)
|
|
87
|
+
converter = np.exp(1j*(m-n)*theta)
|
|
88
|
+
overlaps = overlaps*converter
|
|
89
|
+
Beam.build_from_coefs_and_basis(overlaps, Beam.hg_basis(N))
|
|
90
|
+
return Beam
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from structured_optics.struct_opt import *
|
|
2
|
+
import numpy as np
|
|
3
|
+
from structured_optics.utils import *
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def slm_hologram(Beam, x_grating, y_grating, method, input_beam, eps, max_range):
|
|
8
|
+
#generate a hologram for a slm
|
|
9
|
+
if Beam.pol_dim != 1:
|
|
10
|
+
print('Beam must be scalar (pol_dim=1) to generate holograms')
|
|
11
|
+
return None
|
|
12
|
+
|
|
13
|
+
dx = (Beam.x[0,1] - Beam.x[0,0])
|
|
14
|
+
dy = (Beam.y[1,0] - Beam.y[0,0])
|
|
15
|
+
if input_beam is None:
|
|
16
|
+
input_field = np.exp(-((Beam.x**2 + Beam.y**2)/(2*np.max(Beam.x))**2))
|
|
17
|
+
else:
|
|
18
|
+
input_field = input_beam.field
|
|
19
|
+
desired = Beam.field/np.max(np.abs(Beam.field))
|
|
20
|
+
lamb_x = dx*x_grating
|
|
21
|
+
lamb_y = dy*y_grating
|
|
22
|
+
Ain = np.abs(input_field)
|
|
23
|
+
Ain = Ain / Ain.max()
|
|
24
|
+
Ades = np.abs(desired)
|
|
25
|
+
s = np.min(Ain / (Ades + eps))
|
|
26
|
+
Ades = s * Ades
|
|
27
|
+
A_rel = Ades / Ain
|
|
28
|
+
phi_g = np.mod(2*np.pi*(Beam.x/lamb_x + Beam.y/lamb_y), 2*np.pi)
|
|
29
|
+
phi_relg = np.angle(desired) - np.angle(input_field) + phi_g
|
|
30
|
+
if method == 'simple':
|
|
31
|
+
H = A_rel*phi_relg
|
|
32
|
+
elif method == 'g_holo':
|
|
33
|
+
edg = desired*np.exp(1j*phi_g)
|
|
34
|
+
N = np.min(np.abs(input_field)/(np.abs(desired) + 1e-9))
|
|
35
|
+
H = np.angle(N*edg + input_field)
|
|
36
|
+
elif method == 'davis':
|
|
37
|
+
H = (1-inv_sinc(A_rel)/np.pi)*phi_relg
|
|
38
|
+
elif method == 'bolduc':
|
|
39
|
+
M = 1+inv_sinc(A_rel)/np.pi
|
|
40
|
+
H = M*(phi_relg - np.pi*M)
|
|
41
|
+
elif method == 'bessel0':
|
|
42
|
+
H = phi_relg + inv_J0(A_rel)*np.sin(phi_relg)
|
|
43
|
+
elif method == 'bessel1':
|
|
44
|
+
H = inv_J1(A_rel)*np.sin(phi_relg)
|
|
45
|
+
else:
|
|
46
|
+
print('Error! method must be simple, g_holo, davis, bolduc, bessel0 or bessel1.')
|
|
47
|
+
H = max_range * (H - H.min()) / (H.max() - H.min())
|
|
48
|
+
return H.astype(np.uint8)
|
|
49
|
+
|
|
50
|
+
def dmd_hologram(Beam, cx, cy, sign):
|
|
51
|
+
#generates a hologram for a dmd
|
|
52
|
+
if Beam.pol_dim != 1:
|
|
53
|
+
print('Beam must be scalar (pol_dim=1) to generate holograms')
|
|
54
|
+
return None
|
|
55
|
+
U = np.abs(Beam.field)
|
|
56
|
+
U = U/np.amax(U)
|
|
57
|
+
phi = Beam.phase()
|
|
58
|
+
A = np.arcsin(U)
|
|
59
|
+
|
|
60
|
+
fx = cx*Beam.Dx/(2*Beam.nix)
|
|
61
|
+
fy = cy*Beam.Dy/(2*Beam.niy)
|
|
62
|
+
g = [fx, fy]
|
|
63
|
+
G = g[0]*Beam.x + g[1]*Beam.y
|
|
64
|
+
|
|
65
|
+
H = 0.5 + sign*0.5*np.sign(np.cos(phi + 2*np.pi*G) - np.cos(A))
|
|
66
|
+
return H
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
from structured_optics.utils import *
|
|
2
|
+
import numpy as np
|
|
3
|
+
from scipy import special
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
#modes implementations
|
|
7
|
+
|
|
8
|
+
def hg_mode(Beam, N, M, z, pol_index=None):
|
|
9
|
+
#get a HG mode at distance z of order N+M
|
|
10
|
+
zr = Beam.zr()
|
|
11
|
+
q0 = 1j*zr
|
|
12
|
+
q = -z + 1j*zr
|
|
13
|
+
k = 2*np.pi/Beam.lamb
|
|
14
|
+
w = Beam.waist*np.sqrt(1 + (z/zr)**2)
|
|
15
|
+
Cn = np.sqrt(np.sqrt(2/np.pi)*q0/(2**N * special.factorial(N) * q * Beam.waist))
|
|
16
|
+
un = Cn*(-np.conjugate(q)/q)**(N/2)*hermite(np.sqrt(2)*(Beam.x-Beam.x0)/w, N)*np.exp(-1j*k*(Beam.x-Beam.x0)**2/(2*q))
|
|
17
|
+
Cm = np.sqrt(np.sqrt(2/np.pi)*q0/(2**M * special.factorial(M) * q * Beam.waist))
|
|
18
|
+
um = Cm*(-np.conjugate(q)/q)**(M/2)*hermite(np.sqrt(2)*(Beam.y-Beam.y0)/w, M)*np.exp(-1j*k*(Beam.y-Beam.y0)**2/(2*q))
|
|
19
|
+
F = un*um*np.exp(1j*k*z)
|
|
20
|
+
if pol_index == None:
|
|
21
|
+
if Beam.pol_dim == 1:
|
|
22
|
+
Beam.field = F
|
|
23
|
+
elif Beam.pol_dim == 2:
|
|
24
|
+
Beam.field = np.array([F, F])/np.sqrt(2)
|
|
25
|
+
elif Beam.pol_dim == 3:
|
|
26
|
+
Beam.field = np.array([F, F, F])/np.sqrt(3)
|
|
27
|
+
else:
|
|
28
|
+
Beam.field[pol_index] = F
|
|
29
|
+
return Beam
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def lg_mode(Beam,l,p, z, pol_index=None):
|
|
33
|
+
#get a LG mode at distance z of order abs(N) + 2M
|
|
34
|
+
zr = Beam.zr()
|
|
35
|
+
k = 2*np.pi/Beam.lamb
|
|
36
|
+
w = Beam.waist*np.sqrt(1 + (z/zr)**2)
|
|
37
|
+
R = (z**2 + zr**2)
|
|
38
|
+
gouy = (np.abs(l) + 2*p + 1)*np.arctan(z/zr)
|
|
39
|
+
r = np.sqrt((Beam.x-Beam.x0)**2 + (Beam.y-Beam.y0)**2)
|
|
40
|
+
Cte = np.sqrt(2*special.factorial(p) / (np.pi*special.factorial(p+np.abs(l))))
|
|
41
|
+
F = Cte*(1/w)*(np.sqrt(2)*r/w)**(np.abs(l)) * np.exp(-(r/w)**2) * laguerre(2*(r/w)**2, np.abs(l), p) *\
|
|
42
|
+
np.exp(1j*(k*z + k*z*(r**2)/(2*R) + l*np.arctan2(Beam.y-Beam.y0,Beam.x-Beam.x0) - gouy))
|
|
43
|
+
if pol_index == None:
|
|
44
|
+
if Beam.pol_dim == 1:
|
|
45
|
+
Beam.field = F
|
|
46
|
+
elif Beam.pol_dim == 2:
|
|
47
|
+
Beam.field = np.array([F, F])/np.sqrt(2)
|
|
48
|
+
elif Beam.pol_dim == 3:
|
|
49
|
+
Beam.field = np.array([F, F, F])/np.sqrt(3)
|
|
50
|
+
else:
|
|
51
|
+
Beam.field[pol_index] = F
|
|
52
|
+
return Beam
|
|
53
|
+
|
|
54
|
+
def bessel_mode(Beam, N, z, pol_index=None):
|
|
55
|
+
#get a Bessel mode of order N at distance z
|
|
56
|
+
k = 2*np.pi / Beam.lamb # wavenumber
|
|
57
|
+
j01 = special.jn_zeros(abs(N), 1)[0] # first zero of J_N
|
|
58
|
+
# enforce the first zero at r = self.waist
|
|
59
|
+
kr = j01 / Beam.waist
|
|
60
|
+
kz = np.sqrt(k**2 - kr**2)
|
|
61
|
+
r = np.sqrt((Beam.x-Beam.x0)**2 + (Beam.y-Beam.y0)**2)
|
|
62
|
+
phi = np.arctan2(Beam.y-Beam.y0, Beam.x-Beam.x0)
|
|
63
|
+
F = (np.exp(1j*kz*z) * special.jv(abs(N), kr*r) * np.exp(-1j*N*phi))
|
|
64
|
+
if pol_index == None:
|
|
65
|
+
if Beam.pol_dim == 1:
|
|
66
|
+
Beam.field = F
|
|
67
|
+
elif Beam.pol_dim == 2:
|
|
68
|
+
Beam.field = np.array([F, F])
|
|
69
|
+
elif Beam.pol_dim == 3:
|
|
70
|
+
Beam.field = np.array([F, F, F])
|
|
71
|
+
else:
|
|
72
|
+
Beam.field[pol_index] = F
|
|
73
|
+
Beam.norm_beam()
|
|
74
|
+
return Beam
|
|
75
|
+
|
|
76
|
+
def gbessel_mode(Beam, N, r0, pol_index):
|
|
77
|
+
#get a gaussian bessel beam of order N at z=0
|
|
78
|
+
rad = 2*np.pi*Beam.waist**2/r0
|
|
79
|
+
r = np.sqrt((Beam.x-Beam.x0)**2 + (Beam.y-Beam.y0)**2)
|
|
80
|
+
F = special.jv(np.abs(N),rad*r/Beam.waist**2)*\
|
|
81
|
+
np.exp(-1j*N*np.arctan2(Beam.y-Beam.y0, Beam.x-Beam.x0))*np.exp(-(r/Beam.waist)**2)
|
|
82
|
+
if pol_index == None:
|
|
83
|
+
if Beam.pol_dim == 1:
|
|
84
|
+
Beam.field = F
|
|
85
|
+
elif Beam.pol_dim == 2:
|
|
86
|
+
Beam.field = np.array([F, F])
|
|
87
|
+
elif Beam.pol_dim == 3:
|
|
88
|
+
Beam.field = np.array([F, F, F])
|
|
89
|
+
else:
|
|
90
|
+
Beam.field[pol_index] = F
|
|
91
|
+
Beam.norm_beam()
|
|
92
|
+
return Beam
|
|
93
|
+
|
|
94
|
+
def lg_prod_mode(Beam, N, ls, centers, pol_index):
|
|
95
|
+
if centers is None:
|
|
96
|
+
centers = np.zeros((N, 2))
|
|
97
|
+
centers = np.asarray(centers)
|
|
98
|
+
if ls is None:
|
|
99
|
+
ls = np.ones(N, dtype=int)
|
|
100
|
+
ls = np.asarray(ls)
|
|
101
|
+
|
|
102
|
+
F = np.ones((Beam.Dy, Beam.Dx), dtype='complex128')
|
|
103
|
+
for i in range(N):
|
|
104
|
+
x0 = centers[i, 0]
|
|
105
|
+
y0 = centers[i, 1]
|
|
106
|
+
l = ls[i]
|
|
107
|
+
sign = -1 if l < 0 else 1
|
|
108
|
+
exp_pow = int(np.abs(l))
|
|
109
|
+
arg = (Beam.x - x0) + 1j * sign * (Beam.y - y0)
|
|
110
|
+
r2 = (Beam.x - x0)**2 + (Beam.y - y0)**2
|
|
111
|
+
lg = arg**exp_pow * np.exp(-r2/N / ((Beam.waist)**2))
|
|
112
|
+
F *= lg
|
|
113
|
+
if pol_index == None:
|
|
114
|
+
if Beam.pol_dim == 1:
|
|
115
|
+
Beam.field = F
|
|
116
|
+
elif Beam.pol_dim == 2:
|
|
117
|
+
Beam.field = np.array([F, F])
|
|
118
|
+
elif Beam.pol_dim == 3:
|
|
119
|
+
Beam.field = np.array([F, F, F])
|
|
120
|
+
else:
|
|
121
|
+
Beam.field[pol_index] = F
|
|
122
|
+
Beam.norm_beam()
|
|
123
|
+
return Beam
|
|
124
|
+
|
|
125
|
+
def frac_oam_mode(Beam, Ma, n_modes, beta, theta_0, z, pol_index):
|
|
126
|
+
#get a fractional OAM beam, with OAM Ma (!= integer), by the method of LG supperpositions.
|
|
127
|
+
mu = Ma%1
|
|
128
|
+
m = Ma-mu
|
|
129
|
+
if mu >=0.5:
|
|
130
|
+
n_min = np.ceil(Ma - n_modes / 2)
|
|
131
|
+
else:
|
|
132
|
+
n_min = np.floor(Ma - n_modes / 2)
|
|
133
|
+
n_max = n_min + n_modes - 1
|
|
134
|
+
F = np.zeros((Beam.Dy, Beam.Dx), dtype='complex128')
|
|
135
|
+
for l in np.arange(int(n_min), int(n_max)+1):
|
|
136
|
+
coef = np.exp(-1j*mu*beta)*1j*np.exp(1j*(Ma-l)*theta_0)/(2*np.pi*(Ma-l))*np.exp(1j*(m-l)*beta)*(1-np.exp(1j*mu*2*np.pi))
|
|
137
|
+
if Beam.pol_dim == 1:
|
|
138
|
+
F += coef*(Beam.lg(l, 0, z = z).field)
|
|
139
|
+
else:
|
|
140
|
+
F += coef*(Beam.lg(l, 0, z = z, pol_index=0).field[0])
|
|
141
|
+
if pol_index == None:
|
|
142
|
+
if Beam.pol_dim == 1:
|
|
143
|
+
Beam.field = F
|
|
144
|
+
elif Beam.pol_dim == 2:
|
|
145
|
+
Beam.field = np.array([F, F])
|
|
146
|
+
elif Beam.pol_dim == 3:
|
|
147
|
+
Beam.field = np.array([F, F, F])
|
|
148
|
+
else:
|
|
149
|
+
Beam.field[pol_index] = F
|
|
150
|
+
Beam.norm_beam()
|
|
151
|
+
return Beam
|
|
152
|
+
|
|
153
|
+
def IG_even_mode(Beam, p, m, q, z, pol_index):
|
|
154
|
+
#Even Ince-Gaussian mode IG_p,m^e(x,y)
|
|
155
|
+
|
|
156
|
+
xi, eta = cartesian_to_elliptic(Beam.x-Beam.x0, Beam.y-Beam.y0, q, Beam.waist, z, Beam.lamb)
|
|
157
|
+
|
|
158
|
+
Ce_xi = C_ince(1j*xi, p, m, q)
|
|
159
|
+
Ce_eta = C_ince(eta, p, m, q)
|
|
160
|
+
|
|
161
|
+
zr = np.pi * Beam.waist**2 / Beam.lamb
|
|
162
|
+
k = 2*np.pi/Beam.lamb
|
|
163
|
+
wz = Beam.waist * np.sqrt(1 + (z * Beam.lamb / (np.pi * Beam.waist**2))**2)
|
|
164
|
+
Rz = (z**2 + zr**2)
|
|
165
|
+
r2 = (Beam.x-Beam.x0)**2 + (Beam.y-Beam.y0)**2
|
|
166
|
+
gouy = (p+1)*np.arctan(z / zr)
|
|
167
|
+
F = Ce_xi * Ce_eta * np.exp(-r2 / (wz**2))*np.exp(1j*(k*z + k*z*(r2)/(2*Rz) - gouy))
|
|
168
|
+
if pol_index == None:
|
|
169
|
+
if Beam.pol_dim == 1:
|
|
170
|
+
Beam.field = F
|
|
171
|
+
elif Beam.pol_dim == 2:
|
|
172
|
+
Beam.field = np.array([F, F])
|
|
173
|
+
elif Beam.pol_dim == 3:
|
|
174
|
+
Beam.field = np.array([F, F, F])
|
|
175
|
+
else:
|
|
176
|
+
Beam.field[pol_index] = F
|
|
177
|
+
Beam.norm_beam()
|
|
178
|
+
return Beam
|
|
179
|
+
|
|
180
|
+
def IG_odd_mode(Beam, p, m, q, z, pol_index):
|
|
181
|
+
#Odd Ince-Gaussian mode IG_p,m^o(x,y)
|
|
182
|
+
|
|
183
|
+
xi, eta = cartesian_to_elliptic(Beam.x-Beam.x0, Beam.y-Beam.y0, q, Beam.waist, z, Beam.lamb)
|
|
184
|
+
So_xi = S_ince(1j*xi, p, m, q)
|
|
185
|
+
So_eta = S_ince(eta, p, m, q)
|
|
186
|
+
|
|
187
|
+
zr = np.pi * Beam.waist**2 / Beam.lamb
|
|
188
|
+
k = 2*np.pi/Beam.lamb
|
|
189
|
+
wz = Beam.waist * np.sqrt(1 + (z * Beam.lamb / (np.pi * Beam.waist**2))**2)
|
|
190
|
+
Rz = (z**2 + zr**2)
|
|
191
|
+
r2 = (Beam.x-Beam.x0)**2 + (Beam.y-Beam.y0)**2
|
|
192
|
+
gouy = (p+1)*np.arctan(z / zr)
|
|
193
|
+
F = So_xi * So_eta * np.exp(-r2 / (wz**2))*np.exp(1j*(k*z + k*z*(r2)/(2*Rz) - gouy))
|
|
194
|
+
if pol_index == None:
|
|
195
|
+
if Beam.pol_dim == 1:
|
|
196
|
+
Beam.field = F
|
|
197
|
+
elif Beam.pol_dim == 2:
|
|
198
|
+
Beam.field = np.array([F, F])
|
|
199
|
+
elif Beam.pol_dim == 3:
|
|
200
|
+
Beam.field = np.array([F, F, F])
|
|
201
|
+
else:
|
|
202
|
+
Beam.field[pol_index] = F
|
|
203
|
+
Beam.norm_beam()
|
|
204
|
+
return Beam
|
|
205
|
+
|
|
206
|
+
def HInceG_mode(Beam, p, m, q, z, helicity, pol_index):
|
|
207
|
+
#Hermite-Ince-Gaussian mode HIG_p,m^e(x,y), combining even and odd Ince-Gaussian modes with given helicity.
|
|
208
|
+
#For some reason IG_odd has a 3*pi/2 phase shift wrt IG_even, so the helicity sign is inverted here.
|
|
209
|
+
xi, eta = cartesian_to_elliptic(Beam.x-Beam.x0, Beam.y-Beam.y0, q, Beam.waist, z, Beam.lamb)
|
|
210
|
+
|
|
211
|
+
Ce_xi = C_ince(1j*xi, p, m, q)
|
|
212
|
+
Ce_eta = C_ince(eta, p, m, q)
|
|
213
|
+
|
|
214
|
+
So_xi = S_ince(1j*xi, p, m, q)
|
|
215
|
+
So_eta = S_ince(eta, p, m, q)
|
|
216
|
+
|
|
217
|
+
zr = np.pi * Beam.waist**2 / Beam.lamb
|
|
218
|
+
k = 2*np.pi/Beam.lamb
|
|
219
|
+
wz = Beam.waist * np.sqrt(1 + (z * Beam.lamb / (np.pi * Beam.waist**2))**2)
|
|
220
|
+
Rz = (z**2 + zr**2)
|
|
221
|
+
r2 = (Beam.x-Beam.x0)**2 + (Beam.y-Beam.y0)**2
|
|
222
|
+
gouy = (p+1)*np.arctan(z / zr)
|
|
223
|
+
F = (Ce_xi * Ce_eta - helicity*So_xi * So_eta) * np.exp(-r2 / (wz**2))*np.exp(1j*(k*z + k*z*(r2)/(2*Rz) - gouy))
|
|
224
|
+
if pol_index == None:
|
|
225
|
+
if Beam.pol_dim == 1:
|
|
226
|
+
Beam.field = F
|
|
227
|
+
elif Beam.pol_dim == 2:
|
|
228
|
+
Beam.field = np.array([F, F])
|
|
229
|
+
elif Beam.pol_dim == 3:
|
|
230
|
+
Beam.field = np.array([F, F, F])
|
|
231
|
+
else:
|
|
232
|
+
Beam.field[pol_index] = F
|
|
233
|
+
Beam.norm_beam()
|
|
234
|
+
return Beam
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy import fft
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def propagate_conv(space, z):
|
|
6
|
+
#propagate the field by a distance z using fresnel integral. convolution method
|
|
7
|
+
k = 2*np.pi/space.lamb
|
|
8
|
+
prop = np.exp(-1j * z * (space.kx ** 2 + space.ky ** 2) / (2 * k) - 1j*k*z)
|
|
9
|
+
space.fourier_field = fft.fft2(space.field)
|
|
10
|
+
space.field = fft.ifft2(prop*space.fourier_field)
|
|
11
|
+
return space
|
|
12
|
+
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import numpy as np
|
|
3
|
+
from PyQt6.QtWidgets import QApplication, QLabel
|
|
4
|
+
from PyQt6.QtGui import QImage, QPixmap
|
|
5
|
+
from PyQt6.QtCore import Qt
|
|
6
|
+
|
|
7
|
+
class My_SLM(QLabel):
|
|
8
|
+
def __init__(self, screen_index=0):
|
|
9
|
+
self.app = QApplication.instance() or QApplication(sys.argv)
|
|
10
|
+
super().__init__()
|
|
11
|
+
|
|
12
|
+
screens = self.app.screens()
|
|
13
|
+
if screen_index >= len(screens):
|
|
14
|
+
raise ValueError("Invalid screen index")
|
|
15
|
+
|
|
16
|
+
screen = screens[screen_index]
|
|
17
|
+
|
|
18
|
+
# Force native window creation (CRITICAL on Linux)
|
|
19
|
+
self.setAttribute(Qt.WidgetAttribute.WA_NativeWindow)
|
|
20
|
+
|
|
21
|
+
self.setWindowFlags(
|
|
22
|
+
Qt.WindowType.FramelessWindowHint
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
self.show() # must come before windowHandle()
|
|
26
|
+
|
|
27
|
+
self.windowHandle().setScreen(screen)
|
|
28
|
+
geo = screen.geometry()
|
|
29
|
+
|
|
30
|
+
# Store logical screen size
|
|
31
|
+
self.width = geo.width()
|
|
32
|
+
self.height = geo.height()
|
|
33
|
+
self.setGeometry(geo)
|
|
34
|
+
|
|
35
|
+
self.showFullScreen()
|
|
36
|
+
|
|
37
|
+
self._img_ref = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def update_image(self, img: np.ndarray):
|
|
41
|
+
"""
|
|
42
|
+
img:
|
|
43
|
+
- (H, W) uint8 → grayscale
|
|
44
|
+
- (H, W, 3) uint8 → RGB
|
|
45
|
+
"""
|
|
46
|
+
if img.dtype != np.uint8:
|
|
47
|
+
raise TypeError("Image must be uint8")
|
|
48
|
+
|
|
49
|
+
self._img_ref = img # keep reference alive
|
|
50
|
+
|
|
51
|
+
if img.ndim == 2:
|
|
52
|
+
h, w = img.shape
|
|
53
|
+
stride = img.strides[0]
|
|
54
|
+
|
|
55
|
+
qimg = QImage(
|
|
56
|
+
img.data,
|
|
57
|
+
w,
|
|
58
|
+
h,
|
|
59
|
+
stride,
|
|
60
|
+
QImage.Format.Format_Grayscale8
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
elif img.ndim == 3 and img.shape[2] == 3:
|
|
64
|
+
h, w, _ = img.shape
|
|
65
|
+
stride = img.strides[0]
|
|
66
|
+
|
|
67
|
+
qimg = QImage(
|
|
68
|
+
img.data,
|
|
69
|
+
w,
|
|
70
|
+
h,
|
|
71
|
+
stride,
|
|
72
|
+
QImage.Format.Format_RGB888
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
raise ValueError("Image must be (H,W) or (H,W,3)")
|
|
76
|
+
|
|
77
|
+
self.setPixmap(QPixmap.fromImage(qimg))
|
|
78
|
+
self.app.processEvents()
|
|
79
|
+
|
|
80
|
+
def close(self):
|
|
81
|
+
if not self.isVisible():
|
|
82
|
+
return
|
|
83
|
+
self.hide()
|
|
84
|
+
self.setParent(None) # detach from Qt ownership
|
|
85
|
+
self.deleteLater()
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from scipy import fft, ndimage
|
|
5
|
+
from structured_optics.prop_methods import *
|
|
6
|
+
import copy
|
|
7
|
+
from structured_optics.utils import *
|
|
8
|
+
from structured_optics.modes import *
|
|
9
|
+
from structured_optics.algebra_utils import *
|
|
10
|
+
from structured_optics.hologram import *
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Beam():
|
|
15
|
+
def __init__(self, nix: float, #Region of interest in x (from -nix to +nix)
|
|
16
|
+
Dx: int, #Number of points in x
|
|
17
|
+
niy: float=None, #Region of interest in y (from -niy to +niy), equals nix if None
|
|
18
|
+
Dy: int=None, #Number of points in y, equals Dx if None
|
|
19
|
+
sparse:bool =True, #Whether to use sparse meshgrid for x and y
|
|
20
|
+
pol_dim:int=1, #Number of polarization components, Ex, Ey, Ez
|
|
21
|
+
waist:float=1e-3, #Beam waist. Standard value 1mm
|
|
22
|
+
lamb:float = 1064e-9, #Wavelength. Standard value 1064nm
|
|
23
|
+
x0:float = 0, #Beam center x position
|
|
24
|
+
y0:float = 0) -> object: #Beam center y position
|
|
25
|
+
# Initiate an object with the necessary parameters for calculating transverse fields
|
|
26
|
+
|
|
27
|
+
self.sparse = sparse
|
|
28
|
+
self.nix = nix
|
|
29
|
+
if niy == None:
|
|
30
|
+
self.niy = nix
|
|
31
|
+
else:
|
|
32
|
+
self.niy = niy
|
|
33
|
+
self.Dx = Dx
|
|
34
|
+
if Dy == None:
|
|
35
|
+
self.Dy = Dx
|
|
36
|
+
else:
|
|
37
|
+
self.Dy = Dy
|
|
38
|
+
self.pol_dim = pol_dim
|
|
39
|
+
self.x, self.y = np.meshgrid(np.linspace(-self.nix, self.nix, self.Dx), np.linspace(-self.niy, self.niy, self.Dy), sparse=sparse)
|
|
40
|
+
self.field = np.zeros((pol_dim, self.Dy, self.Dx), dtype='complex128')
|
|
41
|
+
self.kx, self.ky = np.meshgrid(2*np.pi*fft.fftfreq(self.Dx, 2*self.nix/self.Dx), 2*np.pi*fft.fftfreq(self.Dy, 2*self.niy/self.Dy), sparse=sparse)
|
|
42
|
+
self.lamb = lamb
|
|
43
|
+
self.waist = waist
|
|
44
|
+
self.x0 = x0
|
|
45
|
+
self.y0 = y0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
#Auxialiary
|
|
51
|
+
|
|
52
|
+
def copy(self):
|
|
53
|
+
#Perform a deep copy of the Beam object
|
|
54
|
+
return copy.deepcopy(self)
|
|
55
|
+
|
|
56
|
+
def copy_clean(self):
|
|
57
|
+
#Perform a deep copy of the Beam object, but with field initialized to zero
|
|
58
|
+
new = self.copy()
|
|
59
|
+
new.field = np.zeros((self.pol_dim, self.Dy, self.Dx), dtype='complex128')
|
|
60
|
+
return new
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
#Operator overloading
|
|
66
|
+
|
|
67
|
+
def __mul__(self, other):
|
|
68
|
+
#Multiply the transverse fields point by point if multiplied by another field, or globally if multiplied by number.
|
|
69
|
+
if isinstance(other, (type(self))):
|
|
70
|
+
new = self.copy()
|
|
71
|
+
new.field = self.field*other.field
|
|
72
|
+
return new
|
|
73
|
+
if isinstance(other, (int, float, complex)):
|
|
74
|
+
new = self.copy()
|
|
75
|
+
new.field = other*self.field
|
|
76
|
+
return new
|
|
77
|
+
|
|
78
|
+
else:
|
|
79
|
+
raise TypeError(f"sorry, don't know how to multiply by {type(other).__name__}")
|
|
80
|
+
|
|
81
|
+
__rmul__ = __mul__
|
|
82
|
+
|
|
83
|
+
def __add__(self, other:object):
|
|
84
|
+
#Add the transverse fields point by point
|
|
85
|
+
if isinstance(other, (Beam, type(self))):
|
|
86
|
+
new = self.copy()
|
|
87
|
+
new.field = other.field + self.field
|
|
88
|
+
return new
|
|
89
|
+
else:
|
|
90
|
+
raise TypeError(f"sorry, don't know how to add by {type(other).__name__}")
|
|
91
|
+
|
|
92
|
+
__radd__ = __add__
|
|
93
|
+
|
|
94
|
+
def __sub__(self, other:object):
|
|
95
|
+
#Subtract the transverse fields point by point
|
|
96
|
+
if isinstance(other, Beam):
|
|
97
|
+
new = self.copy()
|
|
98
|
+
new.field = self.field - other.field
|
|
99
|
+
return new
|
|
100
|
+
else:
|
|
101
|
+
raise TypeError(f"sorry, don't know how to subtract by {type(other).__name__}")
|
|
102
|
+
|
|
103
|
+
__rsub__ = __sub__
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
#modes
|
|
109
|
+
#pol_index: polarization index to store the mode, if None uses all polarizations
|
|
110
|
+
#returns the Beam object with the mode stored in field
|
|
111
|
+
def hg(self, n:int, m:int, z:float=0, pol_index:int=None) -> object:
|
|
112
|
+
#get a HG mode at distance z of order n+m
|
|
113
|
+
return hg_mode(self, n, m, z, pol_index)
|
|
114
|
+
|
|
115
|
+
def lg(self,l:int,p:int, z:float=0, pol_index:int=None) -> object:
|
|
116
|
+
#get a LG mode at distance z of order abs(N) + 2M
|
|
117
|
+
return lg_mode(self, l, p, z, pol_index)
|
|
118
|
+
|
|
119
|
+
def bessel(self, N:int, z:float=0, pol_index:int=None) -> object:
|
|
120
|
+
#get a Bessel mode of order N at distance z
|
|
121
|
+
return bessel_mode(self, N, z, pol_index)
|
|
122
|
+
|
|
123
|
+
def gbessel(self, N:int, r0:int, pol_index:int=None) -> object:
|
|
124
|
+
#get a gaussian bessel beam of order N at z=0, r0 is the radius of the first intensity null
|
|
125
|
+
return gbessel_mode(self, N, r0, pol_index)
|
|
126
|
+
|
|
127
|
+
def lg_prod(self, N:int, ls:tuple=None, centers:tuple=None, pol_index:int=None) -> object:
|
|
128
|
+
#get a product superposition of N LG modes, with list of OAMs ls and list of centers
|
|
129
|
+
return lg_prod_mode(self, N, ls, centers, pol_index)
|
|
130
|
+
|
|
131
|
+
def frac_oam(self, Ma:float, n_modes:int, beta:float = 0, theta_0:float=0, z:float = 0, pol_index=None) -> object:
|
|
132
|
+
#get a fractional OAM beam, with OAM Ma (!= integer).
|
|
133
|
+
return frac_oam_mode(self, Ma, n_modes, beta, theta_0, z, pol_index)
|
|
134
|
+
|
|
135
|
+
def IG_even(self, p:int, m:int, q:float, z:float=0, pol_index:int=None) -> object:
|
|
136
|
+
#get an even Ince-Gaussian beam IG_p,m^e at distance z with ellipticity q
|
|
137
|
+
return IG_even_mode(self, p, m, q, z, pol_index)
|
|
138
|
+
|
|
139
|
+
def IG_odd(self, p:int, m:int, q:float, z:float=0, pol_index:int=None) -> object:
|
|
140
|
+
#get an odd Ince-Gaussian beam IG_p,m^o at distance z with ellipticity q
|
|
141
|
+
return IG_odd_mode(self, p, m, q, z, pol_index)
|
|
142
|
+
|
|
143
|
+
def HelIG(self, p:int, m:int, q:float, z:float=0, helicity:int=1, pol_index:int=None) -> object:
|
|
144
|
+
#get a Hermite-Ince-Gaussian beam HIG_p,m at distance z with ellipticity q and given helicity (+1 or -1)
|
|
145
|
+
return HInceG_mode(self, p, m, q, z, helicity, pol_index)
|
|
146
|
+
|
|
147
|
+
#Holograms put dmd and slm hologram here
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
#Linear Algebra with modes utils
|
|
152
|
+
|
|
153
|
+
def hg_projector(self,N:int, completeness:bool = False) -> tuple:
|
|
154
|
+
#Project the beam into HG basis up to order N
|
|
155
|
+
#returns tuple with n, m index and overlaps array, if completeness=True also returns completeness value
|
|
156
|
+
return hg_proj(self, N, completeness)
|
|
157
|
+
|
|
158
|
+
def lg_projector(self, N:int, completeness:bool = False) -> tuple:
|
|
159
|
+
#Project the beam into LG basis up to order N
|
|
160
|
+
#returns tuple with l, p index and overlaps array, if completeness=True also returns completeness value
|
|
161
|
+
return lg_proj(self, N, completeness)
|
|
162
|
+
|
|
163
|
+
def hg_basis(self, N:int, waist:float=None, norm1:bool = False) -> np.ndarray:
|
|
164
|
+
#Create a HG basis up to order N
|
|
165
|
+
return hg_basis(self, N, waist, norm1)
|
|
166
|
+
|
|
167
|
+
def lg_basis(self, N:int, waist:float=None, norm1:bool=False) -> np.ndarray:
|
|
168
|
+
#Create a LG basis up to order N
|
|
169
|
+
return lg_basis(self, N, waist, norm1)
|
|
170
|
+
|
|
171
|
+
def bessel_basis(self, Nmax:int, waist:float=None, norm1:bool=False) -> np.ndarray:
|
|
172
|
+
#Create a Bessel basis up to Nmax
|
|
173
|
+
return bessel_basis(self, Nmax, waist, norm1)
|
|
174
|
+
|
|
175
|
+
def build_from_coefs_and_basis(self, coefs:np.ndarray, basis:np.ndarray) -> object:
|
|
176
|
+
#Build beam from given coefficients and basis
|
|
177
|
+
return build_from_coefs_and_basis(self, coefs, basis)
|
|
178
|
+
|
|
179
|
+
def mode_converter(self, N:int, theta:float=np.pi/4) -> object:
|
|
180
|
+
#Astigmatic mode converter that converts HG to LG modes and viceversa
|
|
181
|
+
return astigmatic_mode_converter(self, N, theta)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
#Beam physical atributes and its utilities
|
|
187
|
+
|
|
188
|
+
def Power(self, pol_index:int=None) -> float:
|
|
189
|
+
#Get the total Power of a field within the region of interest
|
|
190
|
+
if pol_index == None:
|
|
191
|
+
return np.sum(self.int_profile())*(4*self.nix/self.Dx)*(self.niy/self.Dy)
|
|
192
|
+
else:
|
|
193
|
+
return np.sum(self.int_profile(pol_index))*(4*self.nix/self.Dx)*(self.niy/self.Dy)
|
|
194
|
+
|
|
195
|
+
def zr(self) -> float:
|
|
196
|
+
#Get the Rayleigh range of the beam
|
|
197
|
+
return np.pi*self.waist**2/self.lamb
|
|
198
|
+
|
|
199
|
+
def int_profile(self, pol_index:int=None) -> np.ndarray:
|
|
200
|
+
#Get intensity profile of the field within Beam
|
|
201
|
+
I = np.abs(self.field)**2
|
|
202
|
+
if pol_index == None:
|
|
203
|
+
return I
|
|
204
|
+
else:
|
|
205
|
+
return I[pol_index]
|
|
206
|
+
|
|
207
|
+
def phase(self, twopi:bool=False) -> np.ndarray:
|
|
208
|
+
#Get phase profile of the field. If twopi=True, phase is given in [0, 2pi], else in [-pi, pi]
|
|
209
|
+
if twopi == False:
|
|
210
|
+
return np.angle(self.field)
|
|
211
|
+
if twopi == True:
|
|
212
|
+
p = np.angle(self.field)
|
|
213
|
+
neg = p<0
|
|
214
|
+
p = p + neg*2*np.pi
|
|
215
|
+
return p
|
|
216
|
+
|
|
217
|
+
def center_mass(self, pol_index:int=0) -> tuple:
|
|
218
|
+
#Calculates the center of mass of intensities of a given field in given polarization
|
|
219
|
+
#pol_index: polarization index to use for the calculation, default 0
|
|
220
|
+
if self.pol_dim >1:
|
|
221
|
+
c = ndimage.center_of_mass(self.int_profile(pol_index))
|
|
222
|
+
else:
|
|
223
|
+
c = ndimage.center_of_mass(self.int_profile())
|
|
224
|
+
return c
|
|
225
|
+
|
|
226
|
+
def std(self, pol_index:int=0) -> float:
|
|
227
|
+
#Calculate the standard deviation of intensities
|
|
228
|
+
#pol_index: polarization index to use for the calculation, default 0
|
|
229
|
+
if self.pol_dim >1:
|
|
230
|
+
c = self.center_mass(pol_index)
|
|
231
|
+
return np.sqrt(np.average((self.x-self.x[0,int(c[1])])**2+(self.y-self.y[int(c[0]),0])**2, weights=self.int_profile(pol_index)))
|
|
232
|
+
else:
|
|
233
|
+
c = self.center_mass()
|
|
234
|
+
return np.sqrt(np.average((self.x-self.x[0,int(c[1])])**2+(self.y-self.y[int(c[0]),0])**2, weights=self.int_profile()))
|
|
235
|
+
|
|
236
|
+
def section(self, ang_min:float, ang_max:float) -> np.ndarray:
|
|
237
|
+
#Return field distribution of given section, defined by minimum angle and maximum angle
|
|
238
|
+
return get_section(self, ang_min, ang_max)
|
|
239
|
+
|
|
240
|
+
def int_section(self, ang_min:float, ang_max:float)-> np.ndarray:
|
|
241
|
+
#Get intensity profile of given section, defined by minimum angle and maximum angle
|
|
242
|
+
return np.abs(self.section(ang_min, ang_max))**2
|
|
243
|
+
|
|
244
|
+
def Power_section(self, ang_min:float, ang_max:float)-> float:
|
|
245
|
+
#Get power of given section
|
|
246
|
+
return np.sum(np.abs(self.section(ang_min, ang_max))**2*(4*self.nix/self.Dx)*(self.niy/self.Dy))
|
|
247
|
+
|
|
248
|
+
def norm_beam(self)-> object:
|
|
249
|
+
#normalize beam so that Total Power = 1 in region of interest
|
|
250
|
+
self.field = self.field/np.sqrt(self.Power())
|
|
251
|
+
return self
|
|
252
|
+
|
|
253
|
+
def Max_int1(self)-> object:
|
|
254
|
+
#rescale the field such that the maximum Intensity point is equal to one.
|
|
255
|
+
self.field = self.field/np.sqrt(np.amax(self.int_profile()))
|
|
256
|
+
return self
|
|
257
|
+
|
|
258
|
+
def crop(self, center:tuple=None, std:float=None, window:float=2, pol_index:int=0)-> object:
|
|
259
|
+
#Crops a field by its std*window arround the center of mass
|
|
260
|
+
#Here pol_index is the field taken as reference for calcutale std and center
|
|
261
|
+
return get_crop(self, center, std, window, pol_index)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
#masks
|
|
271
|
+
def lens(self, f:float, f0:tuple=(0,0))-> object:
|
|
272
|
+
#apply a lens operator to the field, with lens center at f0 and focus lenght equal to f
|
|
273
|
+
k = 2*np.pi/self.lamb
|
|
274
|
+
self.field = self.field*np.exp(-1j*k*(((self.x-f0[0])**2 + (self.y-f0[1])**2)/(2*f)))
|
|
275
|
+
return self
|
|
276
|
+
|
|
277
|
+
def astigmatic_lens(self, fx:float, fy:float, f0:tuple=(0,0))-> object:
|
|
278
|
+
#apply a astigmatic lens with two focal axis, with focus fx and fy.
|
|
279
|
+
k = 2*np.pi/self.lamb
|
|
280
|
+
self.field = self.field*np.exp(-1j*k*(((self.x-f0[0])**2)/fx + ((self.y-f0[1])**2)/fy)/2)
|
|
281
|
+
return self
|
|
282
|
+
|
|
283
|
+
def tilted_lens(self, f:float, phi:float, f0:tuple=(0,0))-> object:
|
|
284
|
+
#apply a astigmatic lens with two focal axis, with each focus given by a tilt phi.
|
|
285
|
+
fx = f*np.cos(phi)**3
|
|
286
|
+
fy = f*np.cos(phi)
|
|
287
|
+
return self.astigmatic_lens(fx,fy, f0)
|
|
288
|
+
|
|
289
|
+
def tilted_lens_y(self, f:float, phi:float, f0:tuple=(0,0))-> object:
|
|
290
|
+
#apply a astigmatic lens with two focal axis, with each focus given by a tilt phi, this time the tilt is in y direction.
|
|
291
|
+
fx = f*np.cos(phi)
|
|
292
|
+
fy = f*np.cos(phi)**3
|
|
293
|
+
return self.astigmatic_lens(fx,fy, f0)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def stripe_v(self, size, move=0):
|
|
297
|
+
"""Def vertical stripe"""
|
|
298
|
+
d = move*self.Dx/(2*self.nix)
|
|
299
|
+
s = size*self.Dx/(2*self.nix)
|
|
300
|
+
self.field[:,int(self.Dx/2 - s + d):int(self.Dx/2+s+d)] = 0
|
|
301
|
+
return self
|
|
302
|
+
|
|
303
|
+
def stripe_h(self, size):
|
|
304
|
+
"""Def horizontal stripe"""
|
|
305
|
+
s = size*self.Dy/(2*self.niy)
|
|
306
|
+
self.field[int(self.Dy/2 - s):int(self.Dy/2+s),:] = 0
|
|
307
|
+
return self
|
|
308
|
+
|
|
309
|
+
def stripe_cross(self, size, hsize=None):
|
|
310
|
+
"""Def cross stripe"""
|
|
311
|
+
if hsize == None:
|
|
312
|
+
hsize = size
|
|
313
|
+
s = size*self.Dx/(2*self.nix)
|
|
314
|
+
sh = hsize*self.Dy/(2*self.niy)
|
|
315
|
+
self.field[int(self.Dy/2 - sh):int(self.Dy/2+sh),:] = 0
|
|
316
|
+
self.field[:,int(self.Dx/2 - s):int(self.Dx/2+s)] = 0
|
|
317
|
+
return self
|
|
318
|
+
|
|
319
|
+
def slit(self, size):
|
|
320
|
+
"""Def 1 slit"""
|
|
321
|
+
s = size*self.Dx/(2*self.nix)
|
|
322
|
+
self.field[:,:int(self.Dx/2-s)] = 0
|
|
323
|
+
self.field[:,int(self.Dx/2 + s):] = 0
|
|
324
|
+
return self
|
|
325
|
+
|
|
326
|
+
def double_slit(self, size, dis):
|
|
327
|
+
"""Def double slit"""
|
|
328
|
+
s = size*self.Dx/(2*self.nix)
|
|
329
|
+
d = dis*self.Dx/(2*self.nix)
|
|
330
|
+
self.field[:,0:int((self.Dx-d-s)/2)] = 0
|
|
331
|
+
self.field[:,int((self.Dx-d+s)/2):int((self.Dx+d-s)/2)] = 0
|
|
332
|
+
self.field[:,int((self.Dx+d+s)/2):] = 0
|
|
333
|
+
return self
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
#Propagation
|
|
340
|
+
def propagate(self, z, method='conv', renorm=False): #propagate the field by a distance z using fresnel integral with exp(-ikz)
|
|
341
|
+
if method == 'conv':
|
|
342
|
+
self = propagate_conv(self, z)
|
|
343
|
+
if renorm == True:
|
|
344
|
+
self.norm_beam()
|
|
345
|
+
return self
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
#Holograms
|
|
349
|
+
|
|
350
|
+
def slm_holo(self, x_grating:int, y_grating:int, method:str = 'bessel1', input_beam:object= None,
|
|
351
|
+
eps:float = 1e-12, max_range:int = 255)-> np.ndarray:
|
|
352
|
+
#generates hologram for slm
|
|
353
|
+
return slm_hologram(self, x_grating, y_grating, method = method, input_beam = input_beam, eps = eps, max_range = max_range)
|
|
354
|
+
|
|
355
|
+
def dmd_holo(self, cx:float, cy:float, sign:int=1)-> np.ndarray:
|
|
356
|
+
#generates hologram for dmd
|
|
357
|
+
return dmd_hologram(self, cx, cy, sign)
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy import special
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
#utils for modes
|
|
7
|
+
|
|
8
|
+
def hermite(X, N): #hermite polynomial
|
|
9
|
+
HER = special.hermite(N)
|
|
10
|
+
sn = HER(X)
|
|
11
|
+
return sn
|
|
12
|
+
|
|
13
|
+
def laguerre(X, L, P): #laguerre polynomial
|
|
14
|
+
LAG = special.genlaguerre(P, L)
|
|
15
|
+
sn = LAG(X)
|
|
16
|
+
return sn
|
|
17
|
+
|
|
18
|
+
def even_coeffs(p, q, kind):
|
|
19
|
+
#Compute coefficients A_r of even Ince polynomials C_p^m.
|
|
20
|
+
if p % 2 != 0:
|
|
21
|
+
raise ValueError("p must be even for even coefs")
|
|
22
|
+
|
|
23
|
+
N = p // 2
|
|
24
|
+
|
|
25
|
+
if kind =='C':
|
|
26
|
+
M = np.zeros((N+1, N+1), dtype=float)
|
|
27
|
+
# First equation
|
|
28
|
+
M[0,0] = 0
|
|
29
|
+
if N >= 1:
|
|
30
|
+
M[0, 1] = (p/2 + 1) * q
|
|
31
|
+
|
|
32
|
+
# Second equation
|
|
33
|
+
if N >= 1:
|
|
34
|
+
M[1, 0] = p * q
|
|
35
|
+
M[1, 1] = 4
|
|
36
|
+
if N >= 2:
|
|
37
|
+
M[1, 2] = (p/2 + 2) * q
|
|
38
|
+
|
|
39
|
+
# General recurrence
|
|
40
|
+
for r in range(2,N):
|
|
41
|
+
|
|
42
|
+
M[r, r-1] = -(r -1 - p/2) * q
|
|
43
|
+
M[r, r] = 4 * (r)**2
|
|
44
|
+
M[r, r+1] = (p/2 + r + 1) * q
|
|
45
|
+
# Last row
|
|
46
|
+
if N >=2:
|
|
47
|
+
M[N, N-1] = q
|
|
48
|
+
M[N, N] = 4 * (N)**2
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
elif kind =='S':
|
|
52
|
+
M = np.zeros((N+1, N+1), dtype=float)
|
|
53
|
+
|
|
54
|
+
# First equation (r=1)
|
|
55
|
+
if N >= 1:
|
|
56
|
+
M[1, 1] = 4
|
|
57
|
+
if N >= 2:
|
|
58
|
+
M[1, 2] = (p/2 + 2) * q
|
|
59
|
+
|
|
60
|
+
# General recurrence
|
|
61
|
+
for r in range(2, N):
|
|
62
|
+
M[r, r-1] = -(r - 1 - p/2) * q
|
|
63
|
+
M[r, r] = 4 * (r)**2
|
|
64
|
+
M[r, r+1] = (p/2 + r + 1) * q
|
|
65
|
+
|
|
66
|
+
# Last row
|
|
67
|
+
if N >= 2:
|
|
68
|
+
M[N, N-1] = -(N - 1 - p/2) * q
|
|
69
|
+
M[N, N] = 4 * (N)**2
|
|
70
|
+
|
|
71
|
+
eigvals, eigvecs = np.linalg.eig(M)
|
|
72
|
+
|
|
73
|
+
# Move the eigenvector with zero eigenvalue to the front (only for 'S' kind) and sort the rest.
|
|
74
|
+
if kind == 'S':
|
|
75
|
+
#find zero position
|
|
76
|
+
zero_idx = np.argmin(np.abs(eigvals))
|
|
77
|
+
|
|
78
|
+
mask = np.arange(len(eigvals)) != zero_idx
|
|
79
|
+
|
|
80
|
+
non_zero_eigvals = eigvals[mask]
|
|
81
|
+
non_zero_eigvecs = eigvecs[:, mask]
|
|
82
|
+
|
|
83
|
+
#sort non-zero eigenvalues and eigenvectors
|
|
84
|
+
sorted_idx = np.argsort(non_zero_eigvals)
|
|
85
|
+
non_zero_eigvals_sorted = non_zero_eigvals[sorted_idx]
|
|
86
|
+
non_zero_eigvecs_sorted = non_zero_eigvecs[:, sorted_idx]
|
|
87
|
+
|
|
88
|
+
#combine zero eigenvalue and sorted non-zero ones
|
|
89
|
+
eigvals_sorted = np.concatenate([[eigvals[zero_idx]], non_zero_eigvals_sorted])
|
|
90
|
+
eigvecs_sorted = np.column_stack([eigvecs[:, zero_idx:zero_idx+1], non_zero_eigvecs_sorted])
|
|
91
|
+
else:
|
|
92
|
+
# For 'C' kind, just sort normally
|
|
93
|
+
idx = np.argsort(eigvals)
|
|
94
|
+
eigvals_sorted = eigvals[idx]
|
|
95
|
+
eigvecs_sorted = eigvecs[:, idx]
|
|
96
|
+
|
|
97
|
+
# Sign convention: ensure sum of coefficients is positive
|
|
98
|
+
for i in range(eigvecs_sorted.shape[1]):
|
|
99
|
+
if np.sum(eigvecs_sorted[:, i]) < 0:
|
|
100
|
+
eigvecs_sorted[:, i] *= -1
|
|
101
|
+
|
|
102
|
+
return eigvals_sorted, eigvecs_sorted
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def odd_coeffs(p, q, kind):
|
|
106
|
+
|
|
107
|
+
if p % 2 != 1:
|
|
108
|
+
raise ValueError("p must be odd for odd coefs")
|
|
109
|
+
|
|
110
|
+
N = p // 2
|
|
111
|
+
M = np.zeros((N+1, N+1), dtype=float)
|
|
112
|
+
|
|
113
|
+
if kind == 'C':
|
|
114
|
+
# First equation
|
|
115
|
+
M[0, 0] = q/2 * (p + 1) + 1
|
|
116
|
+
if N >= 1:
|
|
117
|
+
M[0, 1] = q/2 * (p + 3)
|
|
118
|
+
|
|
119
|
+
# General recurrence (r >= 1)
|
|
120
|
+
for r in range(1, N):
|
|
121
|
+
M[r, r-1] = -q/2 * (2*r - p - 1)
|
|
122
|
+
M[r, r] = (2*r + 1)**2
|
|
123
|
+
M[r, r+1] = q/2 * (p + 2*r + 3)
|
|
124
|
+
# Last row
|
|
125
|
+
if N >=1:
|
|
126
|
+
M[N, N-1] = q
|
|
127
|
+
M[N, N] = (2*N + 1)**2
|
|
128
|
+
|
|
129
|
+
if kind == 'S':
|
|
130
|
+
# First equation
|
|
131
|
+
M[0, 0] = 1 - q/2 * (p + 1)
|
|
132
|
+
if N >= 1:
|
|
133
|
+
M[0, 1] = q/2 * (p + 3)
|
|
134
|
+
|
|
135
|
+
# General recurrence (r >= 1)
|
|
136
|
+
for r in range(1, N):
|
|
137
|
+
M[r, r-1] = -q/2 * (2*r - p - 1)
|
|
138
|
+
M[r, r] = (2*r + 1)**2
|
|
139
|
+
M[r, r+1] = q/2 * (p + 2*r + 3)
|
|
140
|
+
# Last row
|
|
141
|
+
if N >=1:
|
|
142
|
+
M[N, N-1] = q
|
|
143
|
+
M[N, N] = (2*N + 1)**2
|
|
144
|
+
|
|
145
|
+
eigvals, eigvecs = np.linalg.eig(M)
|
|
146
|
+
|
|
147
|
+
# Sort eigenvalues and eigenvectors
|
|
148
|
+
idx = np.argsort(eigvals)
|
|
149
|
+
eigvals_sorted = eigvals[idx]
|
|
150
|
+
eigvecs_sorted = eigvecs[:, idx]
|
|
151
|
+
|
|
152
|
+
# Sign convention: ensure sum of coefficients is positive
|
|
153
|
+
for i in range(eigvecs_sorted.shape[1]):
|
|
154
|
+
if np.sum(eigvecs_sorted[:, i]) < 0:
|
|
155
|
+
eigvecs_sorted[:, i] *= -1
|
|
156
|
+
|
|
157
|
+
return eigvals_sorted, eigvecs_sorted
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def C_ince(xi, p, m, q):
|
|
161
|
+
# Compute even Ince polynomial C_p^m(xi, q)
|
|
162
|
+
if p<0 or m < 0 or m > p:
|
|
163
|
+
raise ValueError("invalid p,m")
|
|
164
|
+
if p % 2 == 0 and m % 2 == 0:
|
|
165
|
+
eigvals, eigvecs = even_coeffs(p, q, kind='C')
|
|
166
|
+
idx = m // 2
|
|
167
|
+
A = eigvecs[:, idx]
|
|
168
|
+
s = sum(A[r] * np.cos(2*r*xi) for r in range(len(A)))
|
|
169
|
+
return s
|
|
170
|
+
if p % 2 == 1 and m % 2 == 1:
|
|
171
|
+
eigvals, eigvecs = odd_coeffs(p, q, kind='C')
|
|
172
|
+
idx = m // 2
|
|
173
|
+
B = eigvecs[:, idx]
|
|
174
|
+
s = sum(B[r] * np.cos((2*r+1)*xi) for r in range(len(B)))
|
|
175
|
+
return s
|
|
176
|
+
else:
|
|
177
|
+
raise ValueError("C_ince is only defined for even (p-m)")
|
|
178
|
+
|
|
179
|
+
def S_ince(xi, p, m, q):
|
|
180
|
+
# Compute odd Ince polynomial S_p^m(xi, q)
|
|
181
|
+
if p<0 or m < 0 or m > p:
|
|
182
|
+
raise ValueError("invalid p,m")
|
|
183
|
+
if p % 2 == 0 and m % 2 == 0:
|
|
184
|
+
eigvals, eigvecs = even_coeffs(p, q, kind='S')
|
|
185
|
+
idx = m // 2
|
|
186
|
+
A = eigvecs[:, idx]
|
|
187
|
+
s = sum(A[r] * np.sin(2*r*xi) for r in range(1,len(A)))
|
|
188
|
+
return s
|
|
189
|
+
if p % 2 == 1 and m % 2 == 1:
|
|
190
|
+
eigvals, eigvecs = odd_coeffs(p, q, kind='S')
|
|
191
|
+
idx = (m-1) // 2
|
|
192
|
+
B = eigvecs[:, idx]
|
|
193
|
+
s = sum(B[r] * np.sin((2*r+1)*xi) for r in range(len(B)))
|
|
194
|
+
return s
|
|
195
|
+
else:
|
|
196
|
+
raise ValueError("S_ince is only defined for even (p-m)")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def cartesian_to_elliptic(x, y, q, w0, z, lamb):
|
|
200
|
+
#Convert Cartesian (x,y) to elliptic coordinates (xi, eta) with elipticity q.
|
|
201
|
+
|
|
202
|
+
f0 = w0*np.sqrt(q/2)
|
|
203
|
+
w = w0 * np.sqrt(1 + (z * lamb / (np.pi * w0**2))**2)
|
|
204
|
+
f = f0*w/w0
|
|
205
|
+
r_plus = np.sqrt((x + f)**2 + y**2)
|
|
206
|
+
r_minus = np.sqrt((x - f)**2 + y**2)
|
|
207
|
+
|
|
208
|
+
xi = np.arccosh((r_plus + r_minus) / (2*f))
|
|
209
|
+
eta = np.sign(y)*np.arccos((r_plus - r_minus) / (2*f))
|
|
210
|
+
|
|
211
|
+
return xi, eta
|
|
212
|
+
|
|
213
|
+
def elliptic_to_cartesian(xi, eta, q, w0, z, lamb):
|
|
214
|
+
#Convert elliptic coordinates (xi, eta) to Cartesian (x,y) with elipticity q.
|
|
215
|
+
f0 = w0*np.sqrt(q/2)
|
|
216
|
+
w = w0 * np.sqrt(1 + (z * lamb / (np.pi * w0**2))**2)
|
|
217
|
+
f = f0*w/w0
|
|
218
|
+
x = f * np.cosh(xi) * np.cos(eta)
|
|
219
|
+
y = f * np.sinh(xi) * np.sin(eta)
|
|
220
|
+
return x, y
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
#utils for Beam class parameters calculation
|
|
225
|
+
|
|
226
|
+
def overlap(first_beam:object, second_beam:object)->complex:
|
|
227
|
+
# calculate overlap between two beams, only properly works if ni and D of beams are equal.
|
|
228
|
+
return np.sum(first_beam.field*np.conjugate(second_beam.field))*4*(first_beam.nix/first_beam.Dx) \
|
|
229
|
+
*(first_beam.niy/first_beam.Dy)/np.sqrt(first_beam.Power()*second_beam.Power())
|
|
230
|
+
|
|
231
|
+
def int_overlap(first_beam:object, second_beam:object)->float:
|
|
232
|
+
return np.sum(first_beam.int_profile()*second_beam.int_profile())*4*(first_beam.nix/first_beam.Dx) \
|
|
233
|
+
*(first_beam.niy/first_beam.Dy)/(first_beam.Power()*second_beam.Power())
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def get_section(Beam, ang_min, ang_max):
|
|
237
|
+
#Return field distribution of given section, defined by minimum angle and maximum angle
|
|
238
|
+
if ang_min > ang_max:
|
|
239
|
+
ang_min, ang_max = ang_max, ang_min
|
|
240
|
+
if ang_max <= np.pi and ang_min <= np.pi:
|
|
241
|
+
sec = (np.arctan2(Beam.y,Beam.x)>=ang_min)*(np.arctan2(Beam.y,Beam.x)<ang_max)
|
|
242
|
+
if ang_max> np.pi and ang_min <= np.pi:
|
|
243
|
+
sec1 = (np.arctan2(Beam.y,Beam.x)>=ang_min)
|
|
244
|
+
ang_max = ang_max-2*np.pi
|
|
245
|
+
sec2 = (np.arctan2(Beam.y,Beam.x)<ang_max)
|
|
246
|
+
sec = sec1 + sec2
|
|
247
|
+
if ang_max>np.pi and ang_min > np.pi:
|
|
248
|
+
ang_max = ang_max - 2*np.pi
|
|
249
|
+
ang_min = ang_min - 2*np.pi
|
|
250
|
+
sec = (np.arctan2(Beam.y,Beam.x)>=ang_min)*(np.arctan2(Beam.y,Beam.x)<ang_max)
|
|
251
|
+
return Beam.field*sec
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def get_crop(Beam, center=None, std=None, window=2, pol_index:int=0):
|
|
255
|
+
#Crops a field by its std*window arround the center of mass
|
|
256
|
+
if center == None:
|
|
257
|
+
center = Beam.center_mass(pol_index)
|
|
258
|
+
if std == None:
|
|
259
|
+
std = Beam.std(pol_index)
|
|
260
|
+
xmin = int(center[1] - window*std*Beam.Dx/Beam.nix/2)
|
|
261
|
+
xmax = int(center[1] + window*std*Beam.Dx/Beam.nix/2)
|
|
262
|
+
ymin = int(center[0] - window*std*Beam.Dy/Beam.niy/2)
|
|
263
|
+
ymax = int(center[0] + window*std*Beam.Dy/Beam.niy/2)
|
|
264
|
+
Beam.x = Beam.x[:,xmin:xmax]
|
|
265
|
+
Beam.y = Beam.y[ymin:ymax, :]
|
|
266
|
+
if Beam.pol_dim >1:
|
|
267
|
+
Beam.field = Beam.field[:,ymin:ymax, xmin:xmax]
|
|
268
|
+
else:
|
|
269
|
+
Beam.field = Beam.field[ymin:ymax, xmin:xmax]
|
|
270
|
+
Beam.Dx = len(Beam.x[0,:])
|
|
271
|
+
Beam.Dy = len(Beam.y[:,0])
|
|
272
|
+
Beam.nix = (Beam.x[0,-1] - Beam.x[0,0])/2
|
|
273
|
+
Beam.niy = (Beam.y[-1,0] - Beam.y[0,0])/2
|
|
274
|
+
Beam.x0 = center[1]
|
|
275
|
+
Beam.y0 = center[0]
|
|
276
|
+
return Beam
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
#utils for holograms
|
|
280
|
+
|
|
281
|
+
def inv_sinc(A, n=10000):
|
|
282
|
+
#invert function sinc
|
|
283
|
+
x = np.linspace(0, np.pi, n)
|
|
284
|
+
y = np.sinc(x/np.pi)
|
|
285
|
+
|
|
286
|
+
return np.interp(A, y[::-1], x[::-1])
|
|
287
|
+
|
|
288
|
+
def inv_J0(A, n=10000):
|
|
289
|
+
#invert bessel function J0
|
|
290
|
+
j01 = 2.404825557695773
|
|
291
|
+
x = np.linspace(0.0, j01, n)
|
|
292
|
+
y = special.j0(x)
|
|
293
|
+
|
|
294
|
+
return np.interp(A, y[::-1], x[::-1])
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def inv_J1(A, a=None, n=10000):
|
|
298
|
+
#invert bessel function J1
|
|
299
|
+
x1_max = 1.8411837813406593
|
|
300
|
+
if a == None:
|
|
301
|
+
a = special.j1(x1_max)
|
|
302
|
+
A = np.clip(A, 0.0, 1.0)
|
|
303
|
+
x = np.linspace(0.0, x1_max, n)
|
|
304
|
+
y = special.j1(x)
|
|
305
|
+
|
|
306
|
+
return np.interp(a * A, y, x)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
structured_optics/__init__.py,sha256=fOIda-DvXD8aSlijBC2bYj-MHFUlBBeNTdHf_ejhOz4,86
|
|
2
|
+
structured_optics/algebra_utils.py,sha256=kECxFBjza074AbBlM40CjUdoltNQT-_kGmAUQTAkgrs,2861
|
|
3
|
+
structured_optics/hologram.py,sha256=nm93Ma4u4X88KJQAOt7JYlM0X5SU1dTVzcPeMuWNa-c,2216
|
|
4
|
+
structured_optics/modes.py,sha256=nemaiAKPqHAwimObKgbUXItU3DSv2TTDYixnqf4U9xc,8668
|
|
5
|
+
structured_optics/prop_methods.py,sha256=NZcwjHdhkF3coZ-XcI1TbZGWI-2Hk9h4d53Ap-btwEY,421
|
|
6
|
+
structured_optics/slmdisplay.py,sha256=sMeixkJxSeqaXXyieWTpX3U7oqNWPQqDxb_qZGFUQog,2215
|
|
7
|
+
structured_optics/struct_opt.py,sha256=CW1MYL_fy8FvE9abPJCMkJSO6_2curAUfwgeXbSAWQg,14678
|
|
8
|
+
structured_optics/utils.py,sha256=Ml2bGIBalOSTQPv-7CYkQPY8D0gT3nk_079hnolj5WY,9414
|
|
9
|
+
structured_optics-0.1.dist-info/METADATA,sha256=So3HcKl_05c-0Asxbd2kmtgpLU1Rg_6dN8whwH9iKrQ,238
|
|
10
|
+
structured_optics-0.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
11
|
+
structured_optics-0.1.dist-info/top_level.txt,sha256=3YZZDRH--fgZIAQh3FIjP6BuDbC6XDj9OTEVwSiqDt4,18
|
|
12
|
+
structured_optics-0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
structured_optics
|