structured-optics 0.4__tar.gz → 0.4.1__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 (21) hide show
  1. {structured_optics-0.4 → structured_optics-0.4.1}/PKG-INFO +3 -3
  2. {structured_optics-0.4 → structured_optics-0.4.1}/README.md +1 -1
  3. {structured_optics-0.4 → structured_optics-0.4.1}/pyproject.toml +2 -2
  4. structured_optics-0.4.1/structured_optics/slmdisplay.py +160 -0
  5. {structured_optics-0.4 → structured_optics-0.4.1}/structured_optics/struct_opt.py +91 -24
  6. {structured_optics-0.4 → structured_optics-0.4.1}/structured_optics/utils.py +27 -0
  7. {structured_optics-0.4 → structured_optics-0.4.1}/structured_optics.egg-info/PKG-INFO +3 -3
  8. {structured_optics-0.4 → structured_optics-0.4.1}/structured_optics.egg-info/requires.txt +1 -1
  9. structured_optics-0.4/structured_optics/slmdisplay.py +0 -85
  10. {structured_optics-0.4 → structured_optics-0.4.1}/LICENSE +0 -0
  11. {structured_optics-0.4 → structured_optics-0.4.1}/setup.cfg +0 -0
  12. {structured_optics-0.4 → structured_optics-0.4.1}/setup.py +0 -0
  13. {structured_optics-0.4 → structured_optics-0.4.1}/structured_optics/__init__.py +0 -0
  14. {structured_optics-0.4 → structured_optics-0.4.1}/structured_optics/algebra_utils.py +0 -0
  15. {structured_optics-0.4 → structured_optics-0.4.1}/structured_optics/hologram.py +0 -0
  16. {structured_optics-0.4 → structured_optics-0.4.1}/structured_optics/modes.py +0 -0
  17. {structured_optics-0.4 → structured_optics-0.4.1}/structured_optics/polarization.py +0 -0
  18. {structured_optics-0.4 → structured_optics-0.4.1}/structured_optics/prop_methods.py +0 -0
  19. {structured_optics-0.4 → structured_optics-0.4.1}/structured_optics.egg-info/SOURCES.txt +0 -0
  20. {structured_optics-0.4 → structured_optics-0.4.1}/structured_optics.egg-info/dependency_links.txt +0 -0
  21. {structured_optics-0.4 → structured_optics-0.4.1}/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.4.1
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.4.1"
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,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.
@@ -908,25 +907,73 @@ class Beam():
908
907
  return p[0]
909
908
  return p
910
909
 
911
- def center_mass(self, pol_index=None) -> tuple:
910
+
911
+ def center_mass(self, pol_index=None) -> tuple:
912
+ """
913
+ Compute the intensity-weighted center of mass of the field in physical x and y coordinates.
914
+
915
+ Parameters
916
+ ----------
917
+ pol_index : int, optional
918
+ Polarization component to use; None uses total intensity.
919
+
920
+ Returns
921
+ -------
922
+ tuple (x_cm, y_cm)
923
+ center-of-mass coordinates in physical units. """
924
+ I = self.int_profile(pol_index)
925
+
926
+ x = np.asarray(self.x)
927
+ y = np.asarray(self.y)
928
+ if x.ndim == 2:
929
+ x = x[0, :] if x.shape[0] == 1 else x[0, :]
930
+ if y.ndim == 2:
931
+ y = y[:, 0] if y.shape[1] == 1 else y[:, 0]
932
+ # Total intensity
933
+ norm = np.sum(I)
934
+
935
+ if norm == 0:
936
+ raise ValueError("Cannot calculate center of mass of zero intensity.")
937
+
938
+ # Intensity marginalized along each direction
939
+ Ix = np.sum(I, axis=0)
940
+ Iy = np.sum(I, axis=1)
941
+
942
+ # Physical center of mass
943
+ x_cm = np.sum(x * Ix) / norm
944
+ y_cm = np.sum(y * Iy) / norm
945
+
946
+ return x_cm, y_cm
947
+
948
+ def std(self, pol_index=None):
912
949
  """
913
- Compute the intensity-weighted center of mass of the field, in grid
914
- index coordinates.
950
+ Calculate the intensity-weighted spatial standard deviation
951
+ along x and y.
915
952
 
916
- Parameters
917
- ----------
918
- pol_index : int, optional
919
- Polarization component to use; None uses total intensity.
953
+ Parameters
954
+ ----------
955
+ pol_index : int, optional
956
+ Polarization component to use; None uses total intensity.
920
957
 
921
958
  Returns
922
959
  -------
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:
960
+ sigma_x, sigma_y : float
961
+ Intensity-weighted spatial standard deviations.
962
+ """
963
+ I = self.int_profile(pol_index)
964
+ x = np.asarray(self.x)
965
+ y = np.asarray(self.y)
966
+ x = x[0, :] if x.ndim == 2 else x
967
+ y = y[:, 0] if y.ndim == 2 else y
968
+ norm = np.sum(I)
969
+ Ix = np.sum(I, axis=0)
970
+ Iy = np.sum(I, axis=1)
971
+ x_cm, y_cm = self.center_mass(pol_index)
972
+ var_x = np.sum(Ix * (x - x_cm)**2) / norm
973
+ var_y = np.sum(Iy * (y - y_cm)**2) / norm
974
+ return np.sqrt(var_x), np.sqrt(var_y)
975
+
976
+ def radial_std(self, pol_index=None) -> float:
930
977
  """
931
978
  Calculate the intensity-weighted radial standard deviation of the field
932
979
  about its center of mass.
@@ -941,12 +988,9 @@ class Beam():
941
988
  float
942
989
  Intensity-weighted radial standard deviation.
943
990
  """
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))
991
+ sx, sy = self.std(pol_index)
992
+
993
+ return np.sqrt(sx**2 + sy**2)
950
994
 
951
995
  def section(self, ang_min:float, ang_max:float, pol_index:int=0) -> np.ndarray:
952
996
  """
@@ -1040,6 +1084,29 @@ class Beam():
1040
1084
  A new, spatially cropped Beam.
1041
1085
  """
1042
1086
  return get_crop(self, center, std, window)
1087
+
1088
+ def apply_zernike(self, n: int, m: int, strength: float = 1) -> object:
1089
+ """
1090
+ Apply a zernike polynomial of indexes n,m where (n-m)%2 == 0 and m <=n. Action normalized by waist.
1091
+ Used to perform aberration correction.
1092
+
1093
+ Parameters
1094
+ ----------
1095
+ n : int
1096
+ Zernike Polynomial radial degree.
1097
+ m : int
1098
+ Zernike Polynomial azimuthal degree.
1099
+ strength: float, optional
1100
+ Intensity of the aberration correction normalized by waist. Default equals one waist.
1101
+
1102
+ Returns
1103
+ -------
1104
+ Beam
1105
+ self, with zernike polynomial applyed to field phase.
1106
+
1107
+ """
1108
+ self.field = apply_zernike(self.x, self.y, self.waist, self.field, n, m, strenght= strength)
1109
+ return self
1043
1110
 
1044
1111
 
1045
1112
 
@@ -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.4.1
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,3 +1,3 @@
1
1
  numpy
2
2
  scipy
3
- PyQt6
3
+ screeninfo
@@ -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()