structured-optics 0.4__tar.gz → 0.5__tar.gz
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-0.4 → structured_optics-0.5}/PKG-INFO +3 -3
- {structured_optics-0.4 → structured_optics-0.5}/README.md +1 -1
- {structured_optics-0.4 → structured_optics-0.5}/pyproject.toml +2 -2
- structured_optics-0.5/structured_optics/bluestein.py +118 -0
- structured_optics-0.5/structured_optics/prop_methods.py +421 -0
- structured_optics-0.5/structured_optics/slmdisplay.py +160 -0
- {structured_optics-0.4 → structured_optics-0.5}/structured_optics/struct_opt.py +134 -32
- {structured_optics-0.4 → structured_optics-0.5}/structured_optics/utils.py +27 -0
- {structured_optics-0.4 → structured_optics-0.5}/structured_optics.egg-info/PKG-INFO +3 -3
- {structured_optics-0.4 → structured_optics-0.5}/structured_optics.egg-info/SOURCES.txt +1 -1
- {structured_optics-0.4 → structured_optics-0.5}/structured_optics.egg-info/requires.txt +1 -1
- structured_optics-0.4/setup.py +0 -8
- structured_optics-0.4/structured_optics/prop_methods.py +0 -46
- structured_optics-0.4/structured_optics/slmdisplay.py +0 -85
- {structured_optics-0.4 → structured_optics-0.5}/LICENSE +0 -0
- {structured_optics-0.4 → structured_optics-0.5}/setup.cfg +0 -0
- {structured_optics-0.4 → structured_optics-0.5}/structured_optics/__init__.py +0 -0
- {structured_optics-0.4 → structured_optics-0.5}/structured_optics/algebra_utils.py +0 -0
- {structured_optics-0.4 → structured_optics-0.5}/structured_optics/hologram.py +0 -0
- {structured_optics-0.4 → structured_optics-0.5}/structured_optics/modes.py +0 -0
- {structured_optics-0.4 → structured_optics-0.5}/structured_optics/polarization.py +0 -0
- {structured_optics-0.4 → structured_optics-0.5}/structured_optics.egg-info/dependency_links.txt +0 -0
- {structured_optics-0.4 → structured_optics-0.5}/structured_optics.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structured_optics
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5
|
|
4
4
|
Summary: Package for Structured Light simulation
|
|
5
5
|
Author: Altilano C. Barbosa
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -9,7 +9,7 @@ Description-Content-Type: text/markdown
|
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Dist: numpy
|
|
11
11
|
Requires-Dist: scipy
|
|
12
|
-
Requires-Dist:
|
|
12
|
+
Requires-Dist: screeninfo
|
|
13
13
|
Dynamic: license-file
|
|
14
14
|
|
|
15
15
|
# structured-optics
|
|
@@ -75,7 +75,7 @@ Soon...
|
|
|
75
75
|
- Python 3.9+
|
|
76
76
|
- numpy
|
|
77
77
|
- scipy
|
|
78
|
-
-
|
|
78
|
+
- screeninfo
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "structured_optics"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5"
|
|
8
8
|
description = "Package for Structured Light simulation"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "BSD-3-Clause"
|
|
@@ -16,5 +16,5 @@ authors = [
|
|
|
16
16
|
dependencies = [
|
|
17
17
|
"numpy",
|
|
18
18
|
"scipy",
|
|
19
|
-
"
|
|
19
|
+
"screeninfo"
|
|
20
20
|
]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def fft_bluestein(f, dx, dk, D_out=None, x0=0.0, k0=0.0, inverse=False):
|
|
5
|
+
"""
|
|
6
|
+
Generalized 1D Fourier transform using Bluestein's algorithm.
|
|
7
|
+
|
|
8
|
+
Forward:
|
|
9
|
+
F(k) = ∫ f(x) exp(-i k x) dx
|
|
10
|
+
|
|
11
|
+
Inverse:
|
|
12
|
+
f(x) = 1/(2π) ∫ F(k) exp(+i k x) dk
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
f = np.asarray(f)
|
|
16
|
+
D = f.shape[-1]
|
|
17
|
+
|
|
18
|
+
if D_out is None:
|
|
19
|
+
D_out = D
|
|
20
|
+
if D < 1:
|
|
21
|
+
raise ValueError("Input cannot be empty.")
|
|
22
|
+
if D_out < 1:
|
|
23
|
+
raise ValueError("D_out must be positive.")
|
|
24
|
+
if dx <= 0:
|
|
25
|
+
raise ValueError("dx must be positive.")
|
|
26
|
+
if dk == 0:
|
|
27
|
+
raise ValueError("dk cannot be zero.")
|
|
28
|
+
|
|
29
|
+
n = np.arange(D)
|
|
30
|
+
m = np.arange(D_out)
|
|
31
|
+
a = dx * dk
|
|
32
|
+
|
|
33
|
+
# FORWARD
|
|
34
|
+
if not inverse:
|
|
35
|
+
# Chirps
|
|
36
|
+
chirp_n = np.exp(-0.5j * a * n**2)
|
|
37
|
+
chirp_m = np.exp(-0.5j * a * m**2)
|
|
38
|
+
|
|
39
|
+
# Coordinate-origin phases
|
|
40
|
+
input_phase = np.exp(-1j * k0 * dx * n)
|
|
41
|
+
output_phase = np.exp(-1j * x0 * dk * m)
|
|
42
|
+
global_phase = np.exp(-1j * k0 * x0)
|
|
43
|
+
|
|
44
|
+
# Input chirp
|
|
45
|
+
a_input = (f * input_phase * chirp_n)
|
|
46
|
+
|
|
47
|
+
# Convolution kernel
|
|
48
|
+
q = np.arange(-(D - 1),D_out)
|
|
49
|
+
kernel = np.exp(+0.5j * a * q**2)
|
|
50
|
+
|
|
51
|
+
# Linear convolution length
|
|
52
|
+
L = D + D_out - 1
|
|
53
|
+
|
|
54
|
+
# FFT padding
|
|
55
|
+
P = 1 << (L - 1).bit_length()
|
|
56
|
+
|
|
57
|
+
A = np.fft.fft(a_input, P, axis=-1)
|
|
58
|
+
B = np.fft.fft(kernel, P)
|
|
59
|
+
B = B.reshape((1,) * (f.ndim - 1) + (P,))
|
|
60
|
+
convolution = np.fft.ifft(A * B, axis=-1)
|
|
61
|
+
|
|
62
|
+
# Extract m = 0,...,D_out-1
|
|
63
|
+
convolution = np.take(convolution, np.arange(D - 1, D - 1 + D_out), axis=-1)
|
|
64
|
+
|
|
65
|
+
# Final result
|
|
66
|
+
F = (dx * global_phase * output_phase * chirp_m * convolution)
|
|
67
|
+
k = k0 + m * dk
|
|
68
|
+
|
|
69
|
+
return F, k
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# INVERSE
|
|
73
|
+
else:
|
|
74
|
+
# Positive exponential
|
|
75
|
+
chirp_n = np.exp(+0.5j * a * n**2)
|
|
76
|
+
chirp_m = np.exp(+0.5j * a * m**2)
|
|
77
|
+
input_phase = np.exp(+1j * x0 * dk * n)
|
|
78
|
+
output_phase = np.exp(+1j * k0 * dx * m)
|
|
79
|
+
global_phase = np.exp(+1j * k0 * x0)
|
|
80
|
+
a_input = (f * input_phase * chirp_n)
|
|
81
|
+
|
|
82
|
+
# Correct convolution kernel
|
|
83
|
+
q = np.arange(-(D - 1), D_out)
|
|
84
|
+
kernel = np.exp(-0.5j * a * q**2)
|
|
85
|
+
L = D + D_out - 1
|
|
86
|
+
P = 1 << (L - 1).bit_length()
|
|
87
|
+
A = np.fft.fft(a_input, P, axis=-1)
|
|
88
|
+
B = np.fft.fft(kernel, P)
|
|
89
|
+
B = B.reshape((1,) * (f.ndim - 1) + (P,))
|
|
90
|
+
convolution = np.fft.ifft(A * B, axis=-1)
|
|
91
|
+
convolution = np.take(convolution, np.arange(D - 1, D - 1 + D_out), axis=-1)
|
|
92
|
+
result = (dk/(2*np.pi) * global_phase * output_phase * chirp_m * convolution)
|
|
93
|
+
x = x0 + m * dx
|
|
94
|
+
return result, x
|
|
95
|
+
|
|
96
|
+
def fft2_bluestein(field, dx, dy, dkx, dky, Dx_out=None, Dy_out=None, x0=0.0, y0=0.0, kx0=0.0, ky0=0.0):
|
|
97
|
+
"""
|
|
98
|
+
Generalized 2D Fourier transform using Bluestein's algorithm. Performs fft_bluestein twice.
|
|
99
|
+
"""
|
|
100
|
+
field = np.asarray(field)
|
|
101
|
+
if field.ndim != 2:
|
|
102
|
+
raise ValueError("bluestein_fft2 expects a 2D array.")
|
|
103
|
+
|
|
104
|
+
Dy, Dx = field.shape
|
|
105
|
+
if Dx_out is None:
|
|
106
|
+
Dx_out = Dx
|
|
107
|
+
if Dy_out is None:
|
|
108
|
+
Dy_out = Dy
|
|
109
|
+
|
|
110
|
+
# x transform
|
|
111
|
+
F, kx = fft_bluestein(field, dx=dx, dk=dkx, D_out=Dx_out, x0=x0, k0=kx0)
|
|
112
|
+
|
|
113
|
+
# y transform
|
|
114
|
+
F = np.swapaxes(F, -1, -2)
|
|
115
|
+
F, ky = fft_bluestein(F, dx=dy, dk=dky, D_out=Dy_out, x0=y0, k0=ky0)
|
|
116
|
+
F = np.swapaxes(F, -1, -2)
|
|
117
|
+
return F, kx, ky
|
|
118
|
+
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy import fft
|
|
3
|
+
from structured_optics.bluestein import *
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def propagate_fresnel_conv(Beam, z):
|
|
7
|
+
#Propagate the field by a distance z using fresnel integral. convolution method
|
|
8
|
+
k = Beam.k()
|
|
9
|
+
prop = np.exp(-1j * z * (Beam.kx ** 2 + Beam.ky ** 2) / (2 * k) + 1j*k*z)
|
|
10
|
+
Beam.fourier_field = fft.fft2(Beam.field, axes=(-2,-1))
|
|
11
|
+
Beam.field = fft.ifft2(prop*Beam.fourier_field, axes=(-2,-1))
|
|
12
|
+
return Beam
|
|
13
|
+
|
|
14
|
+
def propagate_fresnel_fft(Beam, z):
|
|
15
|
+
"""
|
|
16
|
+
Propagate a complex scalar field with the Fresnel approximation using
|
|
17
|
+
a single Fourier transform.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
Beam : object
|
|
22
|
+
Beam class containing all physical information of your beam.
|
|
23
|
+
z : float
|
|
24
|
+
Propagation distance.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
Beam : object
|
|
29
|
+
Beam class containing all physical information of your beam, with updated field and x,y meshgrid.
|
|
30
|
+
"""
|
|
31
|
+
if z == 0:
|
|
32
|
+
return Beam
|
|
33
|
+
|
|
34
|
+
Dy, Dx = Beam.field.shape[-2:]
|
|
35
|
+
dx = 2*Beam.nix/Beam.Dx
|
|
36
|
+
dy = 2*Beam.niy/Beam.Dy
|
|
37
|
+
k = Beam.k()
|
|
38
|
+
|
|
39
|
+
# Output grid, fixed by the physics (fx = x_out / (lambda*z))
|
|
40
|
+
dx_out = Beam.lamb * z / (Dx * dx)
|
|
41
|
+
dy_out = Beam.lamb * z / (Dy * dy)
|
|
42
|
+
x_out = (np.arange(Dx) - Dx // 2) * dx_out
|
|
43
|
+
y_out = (np.arange(Dy) - Dy // 2) * dy_out
|
|
44
|
+
X_out, Y_out = np.meshgrid(x_out, y_out)
|
|
45
|
+
|
|
46
|
+
# Input quadratic phase
|
|
47
|
+
Q1 = np.exp(1j * k / (2 * z) * (Beam.x**2 + Beam.y**2))
|
|
48
|
+
|
|
49
|
+
# Single FFT (fftshift/ifftshift keep x=0 <-> zero frequency)
|
|
50
|
+
U1 = np.fft.fftshift(np.fft.fft2(np.fft.ifftshift(Beam.field * Q1, axes=(-2, -1)), axes=(-2, -1)), axes=(-2, -1)) * dx * dy
|
|
51
|
+
|
|
52
|
+
# Output prefactor and quadratic phase
|
|
53
|
+
Q2 = (np.exp(1j * k * z) / (1j * Beam.lamb * z)) * \
|
|
54
|
+
np.exp(1j * k / (2 * z) * (X_out**2 + Y_out**2))
|
|
55
|
+
field_z = Q2 * U1
|
|
56
|
+
|
|
57
|
+
if dx_out < 0:
|
|
58
|
+
x_out = x_out[::-1]
|
|
59
|
+
field_z = field_z[..., :, ::-1]
|
|
60
|
+
if dy_out < 0:
|
|
61
|
+
y_out = y_out[::-1]
|
|
62
|
+
field_z = field_z[..., ::-1, :]
|
|
63
|
+
|
|
64
|
+
Beam.field = field_z
|
|
65
|
+
Beam.x, Beam.y = np.meshgrid(x_out, y_out, sparse=True)
|
|
66
|
+
|
|
67
|
+
return Beam
|
|
68
|
+
|
|
69
|
+
def propagate_bluestein(Beam, z, x_out_range, y_out_range, Dx_out=None, Dy_out=None):
|
|
70
|
+
"""
|
|
71
|
+
Single-FFT Fresnel propagation with an arbitrary, independently
|
|
72
|
+
chosen output grid, using the Bluestein (chirp-z) transform.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
Beam : object
|
|
77
|
+
Beam class containing all physical information of your beam.
|
|
78
|
+
z : float
|
|
79
|
+
Propagation distance. Must be nonzero; negative z back-propagates.
|
|
80
|
+
x_out_range, y_out_range : (float, float)
|
|
81
|
+
(min, max) of the desired OUTPUT region, in the same units as x, y.
|
|
82
|
+
Dx_out, Dy_out : int
|
|
83
|
+
Number of output samples along x and y. Independent of the input
|
|
84
|
+
sample count, spacing, or of lambda*z/(N*dx) -- choose whatever
|
|
85
|
+
resolution/region you need.
|
|
86
|
+
|
|
87
|
+
Returns
|
|
88
|
+
-------
|
|
89
|
+
Beam : object
|
|
90
|
+
Beam class containing all physical information of your beam, with updated field and x,y meshgrid.
|
|
91
|
+
"""
|
|
92
|
+
if z == 0:
|
|
93
|
+
raise ValueError("Bluestein propagation requires z != 0.")
|
|
94
|
+
if Dx_out < 2 or Dy_out < 2:
|
|
95
|
+
raise ValueError("Dx_out and Dy_out must each be >= 2.")
|
|
96
|
+
|
|
97
|
+
x = np.asarray(Beam.x).ravel()
|
|
98
|
+
y = np.asarray(Beam.y).ravel()
|
|
99
|
+
|
|
100
|
+
dx_in = 2*Beam.nix/Beam.Dx
|
|
101
|
+
dy_in = 2*Beam.niy/Beam.Dy
|
|
102
|
+
x0_in = x[0]
|
|
103
|
+
y0_in = y[0]
|
|
104
|
+
|
|
105
|
+
x_out = np.linspace(x_out_range[0], x_out_range[1], Dx_out)
|
|
106
|
+
y_out = np.linspace(y_out_range[0], y_out_range[1], Dy_out)
|
|
107
|
+
dx_out = x_out[1] - x_out[0]
|
|
108
|
+
dy_out = y_out[1] - y_out[0]
|
|
109
|
+
x0_out = x_out[0]
|
|
110
|
+
y0_out = y_out[0]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
k = Beam.k()
|
|
114
|
+
X, Y = np.meshgrid(x, y) # input grid, (Dy, Dx)
|
|
115
|
+
X_out, Y_out = np.meshgrid(x_out, y_out) # output grid, (Dy_out, Dx_out)
|
|
116
|
+
|
|
117
|
+
# --- Input quadratic phase ---
|
|
118
|
+
Q1 = np.exp(1j * k / (2 * z) * (X**2 + Y**2))
|
|
119
|
+
g = Beam.field * Q1 # shape (..., Dy, Dx)
|
|
120
|
+
|
|
121
|
+
# --- Map desired output coordinates to CZT frequency-grid parameters ---
|
|
122
|
+
# cross term: exp[-i (k/z) x_out * x0] -> K = (k/z) * x_out
|
|
123
|
+
dKx = (k / z) * dx_out
|
|
124
|
+
Kx0 = (k / z) * x0_out
|
|
125
|
+
dKy = (k / z) * dy_out
|
|
126
|
+
Ky0 = (k / z) * y0_out
|
|
127
|
+
|
|
128
|
+
# --- CZT along x (last axis) ---
|
|
129
|
+
Gx, _ = fft_bluestein(g, dx=dx_in, dk=dKx, D_out=Dx_out,
|
|
130
|
+
x0=x0_in, k0=Kx0, inverse=False)
|
|
131
|
+
|
|
132
|
+
# --- CZT along y (swap so y is the last axis, then swap back) ---
|
|
133
|
+
Gx = np.swapaxes(Gx, -1, -2)
|
|
134
|
+
Gxy, _ = fft_bluestein(Gx, dx=dy_in, dk=dKy, D_out=Dy_out,
|
|
135
|
+
x0=y0_in, k0=Ky0, inverse=False)
|
|
136
|
+
Gxy = np.swapaxes(Gxy, -1, -2) # back to (..., Dy_out, Dx_out)
|
|
137
|
+
|
|
138
|
+
# --- Output prefactor and quadratic phase ---
|
|
139
|
+
Q2 = (np.exp(1j * k * z) / (1j * Beam.lamb * z)) * \
|
|
140
|
+
np.exp(1j * k / (2 * z) * (X_out**2 + Y_out**2))
|
|
141
|
+
|
|
142
|
+
Beam.field = Q2 * Gxy
|
|
143
|
+
Beam.x, Beam.y = np.meshgrid(x_out, y_out, sparse=True)
|
|
144
|
+
Beam.Dx = Dx_out
|
|
145
|
+
Beam.Dy = Dy_out
|
|
146
|
+
Beam.nix = (x_out_range[1] - x_out_range[0])/2
|
|
147
|
+
Beam.niy = (y_out_range[1] - y_out_range[0])/2
|
|
148
|
+
Beam.kx, Beam.ky = np.meshgrid(2*np.pi*fft.fftfreq(Beam.Dx, 2*Beam.nix/(Beam.Dx-1)), 2*np.pi*fft.fftfreq(Beam.Dy, 2*Beam.niy/(Beam.Dy-1)), sparse=True)
|
|
149
|
+
|
|
150
|
+
return Beam
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def propagate_angular_spectrum(Beam, z, evanescent=False):
|
|
155
|
+
#Propagate the field by a distance z using Angular Spectrum method.
|
|
156
|
+
k = Beam.k()
|
|
157
|
+
kz2 = k**2 - Beam.kx**2 - Beam.ky**2
|
|
158
|
+
kz = np.sqrt(kz2.astype(complex))
|
|
159
|
+
if not evanescent:
|
|
160
|
+
not_evanescent = kz2 >= 0
|
|
161
|
+
kz = kz*not_evanescent
|
|
162
|
+
prop = np.exp(1j*kz*z + 1j*k*z)
|
|
163
|
+
Beam.fourier_field = fft.fft2(Beam.field, axes=(-2,-1))
|
|
164
|
+
Beam.field = fft.ifft2(prop*Beam.fourier_field, axes=(-2,-1))
|
|
165
|
+
return Beam
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def propagate_incoherent(Beam, z):
|
|
169
|
+
#Propagates the INTENSITY by a distance z using incoherent propagation. Loses phase information.
|
|
170
|
+
#without pupil function doesn't seems to work
|
|
171
|
+
k = Beam.k()
|
|
172
|
+
if z != 0:
|
|
173
|
+
h2 = np.ones_like(Beam.field)/(Beam.lamb*z)**2
|
|
174
|
+
else:
|
|
175
|
+
h2 = np.ones_like(Beam.field)
|
|
176
|
+
h2_fourier = fft.fft2(h2)
|
|
177
|
+
I_fourier = fft.fft2(Beam.int_profile())
|
|
178
|
+
Beam.field = np.sqrt(fft.ifft2(h2_fourier*I_fourier))
|
|
179
|
+
return Beam
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def propagate_fraunhofer(Beam, z):
|
|
183
|
+
#Propagate the field by a distance z using fraunhofer integral. Changes XY grid.
|
|
184
|
+
|
|
185
|
+
if z < 2/Beam.lamb*Beam.waist**2:
|
|
186
|
+
print("z small. Fraunhofer is probably not a good aproximation.")
|
|
187
|
+
if z != 0:
|
|
188
|
+
k = Beam.k()
|
|
189
|
+
dx = Beam.x[0,1] - Beam.x[0,0]
|
|
190
|
+
dy = Beam.y[1,0] - Beam.y[0,0]
|
|
191
|
+
fx = np.fft.fftshift(np.fft.fftfreq(Beam.Dx, d=dx))
|
|
192
|
+
fy = np.fft.fftshift(np.fft.fftfreq(Beam.Dy, d=dy))
|
|
193
|
+
FX, FY = np.meshgrid(fx, fy, indexing="xy")
|
|
194
|
+
Beam.x = Beam.lamb * z * FX
|
|
195
|
+
Beam.y = Beam.lamb * z * FY
|
|
196
|
+
Beam.nix = np.max(Beam.x)
|
|
197
|
+
Beam.niy = np.max(Beam.y)
|
|
198
|
+
C = np.exp(1j*k*z)/(1j*Beam.lamb*z)*np.exp(1j*k*(Beam.x**2 + Beam.y**2)/(2*z))
|
|
199
|
+
Beam.fourier_field = fft.fft2(Beam.field, axes=(-2,-1))
|
|
200
|
+
Beam.field = C*fft.fftshift(Beam.fourier_field)*dx*dy
|
|
201
|
+
return Beam
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def suggest_propagation_method(Beam, z, aperture_sigma_factor=3.0,
|
|
210
|
+
fraunhofer_threshold=0.1):
|
|
211
|
+
"""
|
|
212
|
+
Recommend a propagation method for Beam.propagate(z, method=...).
|
|
213
|
+
|
|
214
|
+
Parameters
|
|
215
|
+
----------
|
|
216
|
+
Beam : object
|
|
217
|
+
Beam instance.
|
|
218
|
+
z : float
|
|
219
|
+
Intended propagation distance.
|
|
220
|
+
aperture_sigma_factor : float, optional
|
|
221
|
+
With sigma as the spatial standart deviation, the effective aperture half-width
|
|
222
|
+
used for the Fraunhofer/paraxial checks is
|
|
223
|
+
aperture_sigma_factor * sigma (default 3.0, i.e. ~99.7% of the
|
|
224
|
+
energy for a Gaussian profile). Only affects the far-field/paraxial
|
|
225
|
+
checks -- the transfer-function sampling check (z_crit) always
|
|
226
|
+
uses the full grid, since that's a property of the numerical grid,
|
|
227
|
+
not of the beam itself.
|
|
228
|
+
fraunhofer_threshold : float, optional
|
|
229
|
+
Fresnel number below which Fraunhofer is considered valid
|
|
230
|
+
(default 0.1, i.e. within a few percent of the true far field).
|
|
231
|
+
verbose : bool, optional
|
|
232
|
+
If True, print the diagnostic numbers behind the recommendation.
|
|
233
|
+
|
|
234
|
+
Returns
|
|
235
|
+
-------
|
|
236
|
+
method : str
|
|
237
|
+
One of: 'none', 'AS', 'fres_c', 'fraun'.
|
|
238
|
+
info : dict
|
|
239
|
+
The underlying numbers (z_crit, fresnel_number, z_paraxial_min,
|
|
240
|
+
aperture size used, and where that size came from) in case you
|
|
241
|
+
want to make the decision yourself.
|
|
242
|
+
"""
|
|
243
|
+
if z == 0:
|
|
244
|
+
print("z = 0: no propagation needed.")
|
|
245
|
+
return 'none', {}
|
|
246
|
+
|
|
247
|
+
abs_z = abs(z)
|
|
248
|
+
lam = Beam.lamb
|
|
249
|
+
|
|
250
|
+
dx = 2 * Beam.nix / Beam.Dx
|
|
251
|
+
dy = 2 * Beam.niy / Beam.Dy
|
|
252
|
+
Dx, Dy = Beam.Dx, Beam.Dy
|
|
253
|
+
|
|
254
|
+
# (1) transfer-function critical distance (smaller axis is the binding
|
|
255
|
+
# one) -- always based on the grid, not the beam
|
|
256
|
+
z_crit_x = Dx * dx**2 / lam
|
|
257
|
+
z_crit_y = Dy * dy**2 / lam
|
|
258
|
+
z_crit = min(z_crit_x, z_crit_y)
|
|
259
|
+
|
|
260
|
+
# (2) effective aperture size for the far-field/paraxial checks:
|
|
261
|
+
# prefer the beam's own extent (via Beam.std()) over the full
|
|
262
|
+
# simulation window, since a beam much smaller than the window
|
|
263
|
+
# reaches the far field far sooner than the window would suggest.
|
|
264
|
+
ax = ay = None
|
|
265
|
+
size_source = "simulation window half-width (nix, niy)"
|
|
266
|
+
if hasattr(Beam, 'std'):
|
|
267
|
+
try:
|
|
268
|
+
sigma_x, sigma_y = Beam.std()
|
|
269
|
+
if sigma_x > 0 and sigma_y > 0:
|
|
270
|
+
ax = aperture_sigma_factor * sigma_x
|
|
271
|
+
ay = aperture_sigma_factor * sigma_y
|
|
272
|
+
size_source = f"{aperture_sigma_factor}*sigma from Beam.std()"
|
|
273
|
+
except Exception:
|
|
274
|
+
pass # fall back to window size below
|
|
275
|
+
if ax is None or ay is None:
|
|
276
|
+
ax, ay = Beam.nix, Beam.niy
|
|
277
|
+
|
|
278
|
+
# (3) Fraunhofer / Fresnel number
|
|
279
|
+
a2 = ax**2 + ay**2
|
|
280
|
+
fresnel_number = a2 / (lam * abs_z)
|
|
281
|
+
|
|
282
|
+
# (4) paraxial validity floor (Goodman's criterion), same aperture size
|
|
283
|
+
z_paraxial_min = (np.pi / (4 * lam) * a2**2) ** (1 / 3)
|
|
284
|
+
|
|
285
|
+
info = {
|
|
286
|
+
'z_crit': z_crit,
|
|
287
|
+
'fresnel_number': fresnel_number,
|
|
288
|
+
'z_paraxial_min': z_paraxial_min,
|
|
289
|
+
'aperture_size': (ax, ay),
|
|
290
|
+
'aperture_size_source': size_source,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if fresnel_number < fraunhofer_threshold:
|
|
294
|
+
method = 'fraun'
|
|
295
|
+
elif abs_z <= z_crit:
|
|
296
|
+
method = 'AS'
|
|
297
|
+
elif abs_z < z_paraxial_min:
|
|
298
|
+
# Too close for paraxial single-step Fresnel to be trustworthy,
|
|
299
|
+
# yet past z_crit for the transfer-function sampling argument --
|
|
300
|
+
# fall back to the exact angular spectrum method.
|
|
301
|
+
method = 'AS'
|
|
302
|
+
else:
|
|
303
|
+
method = 'fres_c'
|
|
304
|
+
|
|
305
|
+
print("Estimating best propagation method.")
|
|
306
|
+
print(f"z = {z:.4g} m")
|
|
307
|
+
print(f" aperture size used ({size_source}): ax={ax:.4g} m, ay={ay:.4g} m")
|
|
308
|
+
print(f" critical (transfer-function) distance z_crit = {z_crit:.4g} m")
|
|
309
|
+
print(f" Fresnel number Nf = {fresnel_number:.4g}")
|
|
310
|
+
print(f" paraxial validity floor z_min = {z_paraxial_min:.4g} m")
|
|
311
|
+
print(f" -> recommended method: {method}")
|
|
312
|
+
|
|
313
|
+
return method, info
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def estimate_bluestein_range(Beam, z, n_sigma=5.0, equal_grid=True):
|
|
319
|
+
"""
|
|
320
|
+
Estimate the output window to pass to propagate_bluestein so the
|
|
321
|
+
propagated beam is fully contained without edge clipping.
|
|
322
|
+
|
|
323
|
+
Parameters
|
|
324
|
+
----------
|
|
325
|
+
Beam : object
|
|
326
|
+
Beam instance.
|
|
327
|
+
z : float
|
|
328
|
+
Propagation distance you intend to use.
|
|
329
|
+
n_sigma : float, optional
|
|
330
|
+
How many standard deviations of the predicted beam width to
|
|
331
|
+
include on each side (default 5.0 -- for a Gaussian-like profile
|
|
332
|
+
this captures effectively all the energy; increase for beams
|
|
333
|
+
with heavier tails, e.g. Bessel-like profiles).
|
|
334
|
+
equal_grid : Bool, optional
|
|
335
|
+
If True x and y grid have the same output size, set to the bigger one.
|
|
336
|
+
|
|
337
|
+
Returns
|
|
338
|
+
-------
|
|
339
|
+
x_out_range, y_out_range : (float, float)
|
|
340
|
+
Suggested ranges for propagate_bluestein.
|
|
341
|
+
info : dict
|
|
342
|
+
Diagnostic quantities: current/predicted sigma, angular spread,
|
|
343
|
+
predicted centroid, etc.
|
|
344
|
+
"""
|
|
345
|
+
field = Beam.field
|
|
346
|
+
X, Y = Beam.x, Beam.y # sparse grids, shapes (1,Dx) and (Dy,1)
|
|
347
|
+
dx = X[0, 1] - X[0, 0]
|
|
348
|
+
dy = Y[1, 0] - Y[0, 0]
|
|
349
|
+
k = Beam.k()
|
|
350
|
+
|
|
351
|
+
# --- Spatial intensity moments (sum over any polarization axis) ---
|
|
352
|
+
I = np.sum(np.abs(field) ** 2, axis=0) # shape (Dy, Dx)
|
|
353
|
+
P = np.sum(I) * dx * dy
|
|
354
|
+
|
|
355
|
+
xbar0 = np.sum(X * I) * dx * dy / P
|
|
356
|
+
ybar0 = np.sum(Y * I) * dx * dy / P
|
|
357
|
+
varx0 = np.sum((X - xbar0) ** 2 * I) * dx * dy / P
|
|
358
|
+
vary0 = np.sum((Y - ybar0) ** 2 * I) * dx * dy / P
|
|
359
|
+
sigma_x0 = np.sqrt(varx0)
|
|
360
|
+
sigma_y0 = np.sqrt(vary0)
|
|
361
|
+
|
|
362
|
+
# --- Angular-spectrum moments (beam's own Fourier content) ---
|
|
363
|
+
A = fft.fft2(field, axes=(-2, -1))
|
|
364
|
+
S = np.sum(np.abs(A) ** 2, axis=0) # shape (Dy, Dx)
|
|
365
|
+
S_tot = np.sum(S)
|
|
366
|
+
|
|
367
|
+
kx_bar0 = np.sum(Beam.kx * S) / S_tot
|
|
368
|
+
ky_bar0 = np.sum(Beam.ky * S) / S_tot
|
|
369
|
+
var_kx0 = np.sum((Beam.kx - kx_bar0) ** 2 * S) / S_tot
|
|
370
|
+
var_ky0 = np.sum((Beam.ky - ky_bar0) ** 2 * S) / S_tot
|
|
371
|
+
|
|
372
|
+
theta_x_bar0 = kx_bar0 / k
|
|
373
|
+
theta_y_bar0 = ky_bar0 / k
|
|
374
|
+
sigma_thetax0 = np.sqrt(var_kx0) / k
|
|
375
|
+
sigma_thetay0 = np.sqrt(var_ky0) / k
|
|
376
|
+
|
|
377
|
+
# --- Cross term: existing wavefront curvature, via local phase gradient ---
|
|
378
|
+
cross_x0 = 0.0
|
|
379
|
+
cross_y0 = 0.0
|
|
380
|
+
|
|
381
|
+
dUdx = fft.ifft2(1j * Beam.kx * A, axes=(-2, -1))
|
|
382
|
+
dUdy = fft.ifft2(1j * Beam.ky * A, axes=(-2, -1))
|
|
383
|
+
# Im(U* dU/dx) = |U|^2 * dphi/dx (local intensity-weighted phase slope)
|
|
384
|
+
local_px = np.sum(np.conj(field) * dUdx, axis=0).imag
|
|
385
|
+
local_py = np.sum(np.conj(field) * dUdy, axis=0).imag
|
|
386
|
+
cross_x0 = np.sum((X - xbar0) * local_px) * dx * dy / (k * P)
|
|
387
|
+
cross_y0 = np.sum((Y - ybar0) * local_py) * dx * dy / (k * P)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# --- Propagate moments to the target z (exact paraxial identity) ---
|
|
392
|
+
xbar_z = xbar0 + z * theta_x_bar0
|
|
393
|
+
ybar_z = ybar0 + z * theta_y_bar0
|
|
394
|
+
|
|
395
|
+
varx_z = varx0 + 2 * z * cross_x0 + z**2 * sigma_thetax0**2
|
|
396
|
+
vary_z = vary0 + 2 * z * cross_y0 + z**2 * sigma_thetay0**2
|
|
397
|
+
sigma_x_z = np.sqrt(max(varx_z, 0.0))
|
|
398
|
+
sigma_y_z = np.sqrt(max(vary_z, 0.0))
|
|
399
|
+
|
|
400
|
+
half_x = n_sigma * sigma_x_z
|
|
401
|
+
half_y = n_sigma * sigma_y_z
|
|
402
|
+
|
|
403
|
+
if equal_grid ==True:
|
|
404
|
+
hm = np.max((half_x, half_y))
|
|
405
|
+
half_x = hm
|
|
406
|
+
half_y = hm
|
|
407
|
+
|
|
408
|
+
x_out_range = (xbar_z - half_x, xbar_z + half_x)
|
|
409
|
+
y_out_range = (ybar_z - half_y, ybar_z + half_y)
|
|
410
|
+
|
|
411
|
+
info = {
|
|
412
|
+
'sigma_x0': sigma_x0, 'sigma_y0': sigma_y0,
|
|
413
|
+
'sigma_thetax0': sigma_thetax0, 'sigma_thetay0': sigma_thetay0,
|
|
414
|
+
'cross_x0': cross_x0, 'cross_y0': cross_y0,
|
|
415
|
+
'xbar0': xbar0, 'ybar0': ybar0,
|
|
416
|
+
'xbar_z': xbar_z, 'ybar_z': ybar_z,
|
|
417
|
+
'sigma_x_z': sigma_x_z, 'sigma_y_z': sigma_y_z,
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return x_out_range, y_out_range, info
|
|
421
|
+
|
|
@@ -0,0 +1,160 @@
|
|
|
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() """
|
|
86
|
+
|
|
87
|
+
import tkinter as tk
|
|
88
|
+
import numpy as np
|
|
89
|
+
from screeninfo import get_monitors
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class My_SLM:
|
|
93
|
+
"""
|
|
94
|
+
Fullscreen numpy-array display on a chosen physical monitor.
|
|
95
|
+
Uses plain tkinter (stdlib) - the image is shown as ordinary
|
|
96
|
+
CPU-composited window content, the same way PyQt6's QLabel/QPixmap
|
|
97
|
+
did. Unlike an OpenGL surface (pyglet/GLFW), this never becomes a
|
|
98
|
+
GPU flip-model surface, so it doesn't trigger the driver-level
|
|
99
|
+
scanout switch that causes the display to briefly flash/resync.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, screen_index: int = 0):
|
|
103
|
+
monitors = get_monitors()
|
|
104
|
+
if screen_index >= len(monitors):
|
|
105
|
+
raise ValueError("Invalid screen index")
|
|
106
|
+
|
|
107
|
+
m = monitors[screen_index]
|
|
108
|
+
self.width, self.height = m.width, m.height
|
|
109
|
+
|
|
110
|
+
self.root = tk.Tk()
|
|
111
|
+
self.root.overrideredirect(True) # borderless
|
|
112
|
+
self.root.geometry(f"{self.width}x{self.height}+{m.x}+{m.y}")
|
|
113
|
+
self.root.attributes("-topmost", True)
|
|
114
|
+
self.root.configure(bg="black")
|
|
115
|
+
|
|
116
|
+
self.label = tk.Label(self.root, bd=0, highlightthickness=0, bg="black")
|
|
117
|
+
self.label.pack(fill="both", expand=True)
|
|
118
|
+
|
|
119
|
+
self.root.update_idletasks()
|
|
120
|
+
self.root.update()
|
|
121
|
+
|
|
122
|
+
self._photo_ref = None # keep the PhotoImage alive (Tk drops it otherwise)
|
|
123
|
+
self._img_ref = None
|
|
124
|
+
|
|
125
|
+
def update_image(self, img: np.ndarray):
|
|
126
|
+
"""
|
|
127
|
+
img:
|
|
128
|
+
- (H, W) uint8 -> grayscale
|
|
129
|
+
- (H, W, 3) uint8 -> RGB
|
|
130
|
+
"""
|
|
131
|
+
if img.dtype != np.uint8:
|
|
132
|
+
raise TypeError("Image must be uint8")
|
|
133
|
+
|
|
134
|
+
img = np.ascontiguousarray(img)
|
|
135
|
+
self._img_ref = img # keep a reference alive
|
|
136
|
+
|
|
137
|
+
if img.ndim == 2:
|
|
138
|
+
h, w = img.shape
|
|
139
|
+
header = f"P5\n{w} {h}\n255\n".encode()
|
|
140
|
+
elif img.ndim == 3 and img.shape[2] == 3:
|
|
141
|
+
h, w, _ = img.shape
|
|
142
|
+
header = f"P6\n{w} {h}\n255\n".encode()
|
|
143
|
+
else:
|
|
144
|
+
raise ValueError("Image must be (H,W) or (H,W,3)")
|
|
145
|
+
|
|
146
|
+
# Build a raw PGM/PPM blob in memory - Tk's built-in photo image
|
|
147
|
+
# reader parses this directly in C, so no PIL/Pillow dependency
|
|
148
|
+
# and no per-pixel Python loop.
|
|
149
|
+
data = header + img.tobytes()
|
|
150
|
+
photo = tk.PhotoImage(data=data)
|
|
151
|
+
|
|
152
|
+
self._photo_ref = photo
|
|
153
|
+
self.label.configure(image=photo)
|
|
154
|
+
|
|
155
|
+
self.root.update()
|
|
156
|
+
|
|
157
|
+
def close(self):
|
|
158
|
+
self.root.destroy()
|
|
159
|
+
|
|
160
|
+
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
1
|
import numpy as np
|
|
4
|
-
|
|
2
|
+
import copy
|
|
3
|
+
from scipy import fft
|
|
5
4
|
from contextlib import contextmanager
|
|
6
5
|
from structured_optics.prop_methods import *
|
|
7
|
-
import copy
|
|
8
6
|
from structured_optics.utils import *
|
|
9
7
|
from structured_optics.modes import *
|
|
10
8
|
from structured_optics.algebra_utils import *
|
|
@@ -13,6 +11,7 @@ from structured_optics.polarization import *
|
|
|
13
11
|
|
|
14
12
|
|
|
15
13
|
|
|
14
|
+
|
|
16
15
|
class Beam():
|
|
17
16
|
"""
|
|
18
17
|
Represents a monochromatic paraxial optical beam on a discretized transverse grid.
|
|
@@ -855,6 +854,16 @@ class Beam():
|
|
|
855
854
|
Rayleigh range, zr = pi*waist**2/lamb.
|
|
856
855
|
"""
|
|
857
856
|
return np.pi*self.waist**2/self.lamb
|
|
857
|
+
|
|
858
|
+
def k(self) -> float:
|
|
859
|
+
"""
|
|
860
|
+
Compute k vector magnitude in vacum.
|
|
861
|
+
|
|
862
|
+
Returns
|
|
863
|
+
-------
|
|
864
|
+
float
|
|
865
|
+
k vector magniture in vaccum"""
|
|
866
|
+
return 2*np.pi/self.lamb
|
|
858
867
|
|
|
859
868
|
def int_profile(self, pol_index=None) -> np.ndarray:
|
|
860
869
|
"""
|
|
@@ -908,25 +917,73 @@ class Beam():
|
|
|
908
917
|
return p[0]
|
|
909
918
|
return p
|
|
910
919
|
|
|
911
|
-
|
|
920
|
+
|
|
921
|
+
def center_mass(self, pol_index=None) -> tuple:
|
|
922
|
+
"""
|
|
923
|
+
Compute the intensity-weighted center of mass of the field in physical x and y coordinates.
|
|
924
|
+
|
|
925
|
+
Parameters
|
|
926
|
+
----------
|
|
927
|
+
pol_index : int, optional
|
|
928
|
+
Polarization component to use; None uses total intensity.
|
|
929
|
+
|
|
930
|
+
Returns
|
|
931
|
+
-------
|
|
932
|
+
tuple (x_cm, y_cm)
|
|
933
|
+
center-of-mass coordinates in physical units. """
|
|
934
|
+
I = self.int_profile(pol_index)
|
|
935
|
+
|
|
936
|
+
x = np.asarray(self.x)
|
|
937
|
+
y = np.asarray(self.y)
|
|
938
|
+
if x.ndim == 2:
|
|
939
|
+
x = x[0, :] if x.shape[0] == 1 else x[0, :]
|
|
940
|
+
if y.ndim == 2:
|
|
941
|
+
y = y[:, 0] if y.shape[1] == 1 else y[:, 0]
|
|
942
|
+
# Total intensity
|
|
943
|
+
norm = np.sum(I)
|
|
944
|
+
|
|
945
|
+
if norm == 0:
|
|
946
|
+
raise ValueError("Cannot calculate center of mass of zero intensity.")
|
|
947
|
+
|
|
948
|
+
# Intensity marginalized along each direction
|
|
949
|
+
Ix = np.sum(I, axis=0)
|
|
950
|
+
Iy = np.sum(I, axis=1)
|
|
951
|
+
|
|
952
|
+
# Physical center of mass
|
|
953
|
+
x_cm = np.sum(x * Ix) / norm
|
|
954
|
+
y_cm = np.sum(y * Iy) / norm
|
|
955
|
+
|
|
956
|
+
return x_cm, y_cm
|
|
957
|
+
|
|
958
|
+
def std(self, pol_index=None):
|
|
912
959
|
"""
|
|
913
|
-
|
|
914
|
-
|
|
960
|
+
Calculate the intensity-weighted spatial standard deviation
|
|
961
|
+
along x and y.
|
|
915
962
|
|
|
916
|
-
Parameters
|
|
917
|
-
----------
|
|
918
|
-
pol_index : int, optional
|
|
919
|
-
Polarization component to use; None uses total intensity.
|
|
963
|
+
Parameters
|
|
964
|
+
----------
|
|
965
|
+
pol_index : int, optional
|
|
966
|
+
Polarization component to use; None uses total intensity.
|
|
920
967
|
|
|
921
968
|
Returns
|
|
922
969
|
-------
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
970
|
+
sigma_x, sigma_y : float
|
|
971
|
+
Intensity-weighted spatial standard deviations.
|
|
972
|
+
"""
|
|
973
|
+
I = self.int_profile(pol_index)
|
|
974
|
+
x = np.asarray(self.x)
|
|
975
|
+
y = np.asarray(self.y)
|
|
976
|
+
x = x[0, :] if x.ndim == 2 else x
|
|
977
|
+
y = y[:, 0] if y.ndim == 2 else y
|
|
978
|
+
norm = np.sum(I)
|
|
979
|
+
Ix = np.sum(I, axis=0)
|
|
980
|
+
Iy = np.sum(I, axis=1)
|
|
981
|
+
x_cm, y_cm = self.center_mass(pol_index)
|
|
982
|
+
var_x = np.sum(Ix * (x - x_cm)**2) / norm
|
|
983
|
+
var_y = np.sum(Iy * (y - y_cm)**2) / norm
|
|
984
|
+
return np.sqrt(var_x), np.sqrt(var_y)
|
|
985
|
+
|
|
986
|
+
def radial_std(self, pol_index=None) -> float:
|
|
930
987
|
"""
|
|
931
988
|
Calculate the intensity-weighted radial standard deviation of the field
|
|
932
989
|
about its center of mass.
|
|
@@ -941,12 +998,9 @@ class Beam():
|
|
|
941
998
|
float
|
|
942
999
|
Intensity-weighted radial standard deviation.
|
|
943
1000
|
"""
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
y0 = self.y[int(round(c[0])), 0]
|
|
948
|
-
r2 = (self.x - x0)**2 + (self.y - y0)**2
|
|
949
|
-
return np.sqrt(np.average(r2, weights=I))
|
|
1001
|
+
sx, sy = self.std(pol_index)
|
|
1002
|
+
|
|
1003
|
+
return np.sqrt(sx**2 + sy**2)
|
|
950
1004
|
|
|
951
1005
|
def section(self, ang_min:float, ang_max:float, pol_index:int=0) -> np.ndarray:
|
|
952
1006
|
"""
|
|
@@ -1040,6 +1094,29 @@ class Beam():
|
|
|
1040
1094
|
A new, spatially cropped Beam.
|
|
1041
1095
|
"""
|
|
1042
1096
|
return get_crop(self, center, std, window)
|
|
1097
|
+
|
|
1098
|
+
def apply_zernike(self, n: int, m: int, strength: float = 1) -> object:
|
|
1099
|
+
"""
|
|
1100
|
+
Apply a zernike polynomial of indexes n,m where (n-m)%2 == 0 and m <=n. Action normalized by waist.
|
|
1101
|
+
Used to perform aberration correction.
|
|
1102
|
+
|
|
1103
|
+
Parameters
|
|
1104
|
+
----------
|
|
1105
|
+
n : int
|
|
1106
|
+
Zernike Polynomial radial degree.
|
|
1107
|
+
m : int
|
|
1108
|
+
Zernike Polynomial azimuthal degree.
|
|
1109
|
+
strength: float, optional
|
|
1110
|
+
Intensity of the aberration correction normalized by waist. Default equals one waist.
|
|
1111
|
+
|
|
1112
|
+
Returns
|
|
1113
|
+
-------
|
|
1114
|
+
Beam
|
|
1115
|
+
self, with zernike polynomial applyed to field phase.
|
|
1116
|
+
|
|
1117
|
+
"""
|
|
1118
|
+
self.field = apply_zernike(self.x, self.y, self.waist, self.field, n, m, strength= strength)
|
|
1119
|
+
return self
|
|
1043
1120
|
|
|
1044
1121
|
|
|
1045
1122
|
|
|
@@ -1384,29 +1461,54 @@ class Beam():
|
|
|
1384
1461
|
|
|
1385
1462
|
|
|
1386
1463
|
#Propagation
|
|
1387
|
-
def propagate(self, z, method='
|
|
1464
|
+
def propagate(self, z, method='fres_c', renorm=False, **kwargs):
|
|
1388
1465
|
"""
|
|
1389
1466
|
Parameters
|
|
1390
1467
|
----------
|
|
1391
1468
|
z : float
|
|
1392
1469
|
Propagation distance.
|
|
1393
|
-
method : {'
|
|
1394
|
-
Diffraction model used for propagation.
|
|
1395
|
-
currently prints a warning and leaves the field unchanged.
|
|
1470
|
+
method : {'auto', 'fresn_c', 'AS', 'fraun', 'fres_f', 'blue', 'blue_fix', 'inc'}, optional
|
|
1471
|
+
Diffraction model used for propagation. Default is 'fres_c'.
|
|
1396
1472
|
renorm : bool, optional
|
|
1397
1473
|
If True, renormalize total power to 1 after propagation.
|
|
1474
|
+
evanescent : bool, optional
|
|
1475
|
+
Optional in Angular Spectrum method. Default is False, but if True the code keeps the evanscent contribution to the field.
|
|
1476
|
+
x_out_range, y_out_range : tuple
|
|
1477
|
+
Necessary for bluestein method without fixed window range. tuple containing (x_min, x_max), (y_min, y_max) of the output window.
|
|
1478
|
+
Dx_out, Dy_out : int, optional
|
|
1479
|
+
Optional for bluestein method. Sets number of output samples along x and y.
|
|
1480
|
+
n_sigma : float, optional
|
|
1481
|
+
Optional in Bluestein Fix. How many standard deviations the bluestein_fix window range should be.
|
|
1482
|
+
|
|
1398
1483
|
|
|
1399
1484
|
Returns
|
|
1400
1485
|
-------
|
|
1401
1486
|
Beam
|
|
1402
1487
|
self, with field propagated by distance z (and renormalized if requested).
|
|
1403
1488
|
"""
|
|
1404
|
-
if method == '
|
|
1405
|
-
|
|
1406
|
-
|
|
1489
|
+
if method == 'auto':
|
|
1490
|
+
method, _ = suggest_propagation_method(self, z)
|
|
1491
|
+
if method == 'none':
|
|
1492
|
+
return self
|
|
1493
|
+
|
|
1494
|
+
if method == 'fres_c':
|
|
1495
|
+
self = propagate_fresnel_conv(self, z)
|
|
1496
|
+
elif method == 'AS':
|
|
1497
|
+
self = propagate_angular_spectrum(self, z, **kwargs)
|
|
1498
|
+
elif method == 'fraun':
|
|
1407
1499
|
self = propagate_fraunhofer(self, z)
|
|
1408
|
-
|
|
1500
|
+
|
|
1501
|
+
elif method == 'fres_f':
|
|
1502
|
+
self = propagate_fresnel_fft(self, z)
|
|
1503
|
+
elif method == 'blue':
|
|
1504
|
+
self = propagate_bluestein(self, z, **kwargs)
|
|
1505
|
+
elif method == 'blue_fix':
|
|
1506
|
+
x_out_range, y_out_range, _ = estimate_bluestein_range(self, z, **kwargs)
|
|
1507
|
+
self = propagate_bluestein(self, z, x_out_range = x_out_range, y_out_range = y_out_range, Dx_out = self.Dx, Dy_out = self.Dy)
|
|
1508
|
+
|
|
1509
|
+
elif method == 'inc':
|
|
1409
1510
|
self = propagate_incoherent(self, z)
|
|
1511
|
+
|
|
1410
1512
|
else:
|
|
1411
1513
|
raise Exception('Unable to propagate, insert valid method.')
|
|
1412
1514
|
if renorm == True:
|
|
@@ -3,6 +3,7 @@ from scipy import ndimage
|
|
|
3
3
|
from scipy.special import hermite, genlaguerre, jv, j0, j1, kv, jn_zeros
|
|
4
4
|
from scipy.optimize import brentq
|
|
5
5
|
from functools import wraps
|
|
6
|
+
import math
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
|
|
@@ -471,3 +472,29 @@ def inv_J1(A, a=None, n=10000):
|
|
|
471
472
|
return np.interp(a * A, y, x)
|
|
472
473
|
|
|
473
474
|
|
|
475
|
+
|
|
476
|
+
#utils for aberration correction
|
|
477
|
+
|
|
478
|
+
def radial_poly(n, m, r):
|
|
479
|
+
s = 0
|
|
480
|
+
if (n - m) % 2 == 0 and m <= n:
|
|
481
|
+
for k in range((n - m) // 2 + 1):
|
|
482
|
+
s += (-1)**k * math.factorial(n-k) / (math.factorial(k) * math.factorial((n + m) // 2 - k)
|
|
483
|
+
* math.factorial((n + m) // 2 - k))* r**(n - 2*k)
|
|
484
|
+
elif (n - m) % 2 == 1 and m <= n:
|
|
485
|
+
raise ValueError("Warning: (n - m) is odd, Zernike Polynomial is zero.")
|
|
486
|
+
else:
|
|
487
|
+
raise ValueError("Invalid n and m values for Zernike polynomials.")
|
|
488
|
+
return s
|
|
489
|
+
|
|
490
|
+
def zernike(n, m, r, phi):
|
|
491
|
+
if m >= 0:
|
|
492
|
+
return radial_poly(n, m, r) * np.cos(m*phi)
|
|
493
|
+
else:
|
|
494
|
+
return radial_poly(n, -m, r) * np.sin(-m*phi)
|
|
495
|
+
|
|
496
|
+
def apply_zernike(x, y, waist, desired, n, m, strength):
|
|
497
|
+
r = np.sqrt(x**2 + y**2)/waist
|
|
498
|
+
phi = np.arctan2(y, x)
|
|
499
|
+
phase = strength * zernike(n, m, r, phi)
|
|
500
|
+
return desired*np.exp(1j*phase)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structured_optics
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5
|
|
4
4
|
Summary: Package for Structured Light simulation
|
|
5
5
|
Author: Altilano C. Barbosa
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -9,7 +9,7 @@ Description-Content-Type: text/markdown
|
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Dist: numpy
|
|
11
11
|
Requires-Dist: scipy
|
|
12
|
-
Requires-Dist:
|
|
12
|
+
Requires-Dist: screeninfo
|
|
13
13
|
Dynamic: license-file
|
|
14
14
|
|
|
15
15
|
# structured-optics
|
|
@@ -75,7 +75,7 @@ Soon...
|
|
|
75
75
|
- Python 3.9+
|
|
76
76
|
- numpy
|
|
77
77
|
- scipy
|
|
78
|
-
-
|
|
78
|
+
- screeninfo
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
|
structured_optics-0.4/setup.py
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
from scipy import fft
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def propagate_fresnel(Beam, z):
|
|
6
|
-
#Propagate the field by a distance z using fresnel integral. convolution method
|
|
7
|
-
k = 2*np.pi/Beam.lamb
|
|
8
|
-
prop = np.exp(-1j * z * (Beam.kx ** 2 + Beam.ky ** 2) / (2 * k) + 1j*k*z)
|
|
9
|
-
Beam.fourier_field = fft.fft2(Beam.field, axes=(-2,-1))
|
|
10
|
-
Beam.field = fft.ifft2(prop*Beam.fourier_field, axes=(-2,-1))
|
|
11
|
-
return Beam
|
|
12
|
-
|
|
13
|
-
def propagate_incoherent(Beam, z):
|
|
14
|
-
#Propagates the INTENSITY by a distance z using incoherent propagation. Loses phase information.
|
|
15
|
-
#without pupil function doesn't seems to work
|
|
16
|
-
k = 2*np.pi/Beam.lamb
|
|
17
|
-
if z != 0:
|
|
18
|
-
h2 = np.ones_like(Beam.field)/(Beam.lamb*z)**2
|
|
19
|
-
else:
|
|
20
|
-
h2 = np.ones_like(Beam.field)
|
|
21
|
-
h2_fourier = fft.fft2(h2)
|
|
22
|
-
I_fourier = fft.fft2(Beam.int_profile())
|
|
23
|
-
Beam.field = np.sqrt(fft.ifft2(h2_fourier*I_fourier))
|
|
24
|
-
return Beam
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def propagate_fraunhofer(Beam, z):
|
|
28
|
-
#Propagate the field by a distance z using fraunhofer integral. Changes XY grid.
|
|
29
|
-
|
|
30
|
-
if z < 2/Beam.lamb*Beam.waist**2:
|
|
31
|
-
print("z small. Fraunhofer is probably not a good aproximation.")
|
|
32
|
-
if z != 0:
|
|
33
|
-
k = 2*np.pi/Beam.lamb
|
|
34
|
-
dx = Beam.x[0,1] - Beam.x[0,0]
|
|
35
|
-
dy = Beam.y[1,0] - Beam.y[0,0]
|
|
36
|
-
fx = np.fft.fftshift(np.fft.fftfreq(Beam.Dx, d=dx))
|
|
37
|
-
fy = np.fft.fftshift(np.fft.fftfreq(Beam.Dy, d=dy))
|
|
38
|
-
FX, FY = np.meshgrid(fx, fy, indexing="xy")
|
|
39
|
-
Beam.x = Beam.lamb * z * FX
|
|
40
|
-
Beam.y = Beam.lamb * z * FY
|
|
41
|
-
Beam.nix = np.max(Beam.x)
|
|
42
|
-
Beam.niy = np.max(Beam.y)
|
|
43
|
-
C = np.exp(1j*k*z)/(1j*Beam.lamb*z)*np.exp(1j*k*(Beam.x**2 + Beam.y**2)/(2*z))
|
|
44
|
-
Beam.fourier_field = fft.fft2(Beam.field, axes=(-2,-1))
|
|
45
|
-
Beam.field = C*fft.fftshift(Beam.fourier_field)*dx*dy
|
|
46
|
-
return Beam
|
|
@@ -1,85 +0,0 @@
|
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{structured_optics-0.4 → structured_optics-0.5}/structured_optics.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|