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.
Files changed (23) hide show
  1. {structured_optics-0.4 → structured_optics-0.5}/PKG-INFO +3 -3
  2. {structured_optics-0.4 → structured_optics-0.5}/README.md +1 -1
  3. {structured_optics-0.4 → structured_optics-0.5}/pyproject.toml +2 -2
  4. structured_optics-0.5/structured_optics/bluestein.py +118 -0
  5. structured_optics-0.5/structured_optics/prop_methods.py +421 -0
  6. structured_optics-0.5/structured_optics/slmdisplay.py +160 -0
  7. {structured_optics-0.4 → structured_optics-0.5}/structured_optics/struct_opt.py +134 -32
  8. {structured_optics-0.4 → structured_optics-0.5}/structured_optics/utils.py +27 -0
  9. {structured_optics-0.4 → structured_optics-0.5}/structured_optics.egg-info/PKG-INFO +3 -3
  10. {structured_optics-0.4 → structured_optics-0.5}/structured_optics.egg-info/SOURCES.txt +1 -1
  11. {structured_optics-0.4 → structured_optics-0.5}/structured_optics.egg-info/requires.txt +1 -1
  12. structured_optics-0.4/setup.py +0 -8
  13. structured_optics-0.4/structured_optics/prop_methods.py +0 -46
  14. structured_optics-0.4/structured_optics/slmdisplay.py +0 -85
  15. {structured_optics-0.4 → structured_optics-0.5}/LICENSE +0 -0
  16. {structured_optics-0.4 → structured_optics-0.5}/setup.cfg +0 -0
  17. {structured_optics-0.4 → structured_optics-0.5}/structured_optics/__init__.py +0 -0
  18. {structured_optics-0.4 → structured_optics-0.5}/structured_optics/algebra_utils.py +0 -0
  19. {structured_optics-0.4 → structured_optics-0.5}/structured_optics/hologram.py +0 -0
  20. {structured_optics-0.4 → structured_optics-0.5}/structured_optics/modes.py +0 -0
  21. {structured_optics-0.4 → structured_optics-0.5}/structured_optics/polarization.py +0 -0
  22. {structured_optics-0.4 → structured_optics-0.5}/structured_optics.egg-info/dependency_links.txt +0 -0
  23. {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.4
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: PyQt6
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
- - PyQt6
78
+ - screeninfo
79
79
 
80
80
 
81
81
 
@@ -61,7 +61,7 @@ Soon...
61
61
  - Python 3.9+
62
62
  - numpy
63
63
  - scipy
64
- - PyQt6
64
+ - screeninfo
65
65
 
66
66
 
67
67
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "structured_optics"
7
- version = "0.4"
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
- "PyQt6"
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
- from scipy import fft, ndimage
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
- def center_mass(self, pol_index=None) -> tuple:
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
- Compute the intensity-weighted center of mass of the field, in grid
914
- index coordinates.
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
- tuple
924
- (row, col) center-of-mass indices, as returned by
925
- `scipy.ndimage.center_of_mass`.
926
- """
927
- return ndimage.center_of_mass(self.int_profile(pol_index))
928
-
929
- def std(self, pol_index=None) -> float:
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
- I = self.int_profile(pol_index)
945
- c = self.center_mass(pol_index)
946
- x0 = self.x[0, int(round(c[1]))]
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='fresnel', renorm=False):
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 : {'fresnel', 'fraunhofer', 'incoherent'}, optional
1394
- Diffraction model used for propagation. An unrecognized value
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 == 'fresnel':
1405
- self = propagate_fresnel(self, z)
1406
- elif method == 'fraunhofer':
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
- elif method == 'incoherent':
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.4
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: PyQt6
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
- - PyQt6
78
+ - screeninfo
79
79
 
80
80
 
81
81
 
@@ -1,9 +1,9 @@
1
1
  LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
- setup.py
5
4
  structured_optics/__init__.py
6
5
  structured_optics/algebra_utils.py
6
+ structured_optics/bluestein.py
7
7
  structured_optics/hologram.py
8
8
  structured_optics/modes.py
9
9
  structured_optics/polarization.py
@@ -1,3 +1,3 @@
1
1
  numpy
2
2
  scipy
3
- PyQt6
3
+ screeninfo
@@ -1,8 +0,0 @@
1
- from setuptools import setup, find_packages
2
-
3
- setup(
4
- name='structured_optics',
5
- version='0.3',
6
- packages=find_packages()
7
-
8
- )
@@ -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