miepom 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
miepom/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
miepom/backend.py ADDED
@@ -0,0 +1,33 @@
1
+ """Backend switching for the MIEPOM library.
2
+
3
+ Backend switching allows the user to change the underlying computational library (e.g., NumPy, CuPy) used by the MIEPOM library.
4
+ Use the `set_backend(xp)` function to switch the backend, where `xp` is the desired computational library (e.g., `numpy`, `cupy`).
5
+ """
6
+
7
+ from .sim import set_backend as _set_simulator_backend
8
+ from .linalg import set_backend as _set_linalg_backend
9
+ from .sensor import set_backend as _set_sensor_backend
10
+ from .pom import set_backend as _set_microscope_backend
11
+ from .config import set_backend as _set_config_backend
12
+
13
+ import numpy as np
14
+
15
+ globals()["xp"] = np
16
+
17
+ def set_backend(xp):
18
+ """Set the linear algebra backend for the MIEPOM library."""
19
+ _set_linalg_backend(xp)
20
+ _set_sensor_backend(xp)
21
+ _set_microscope_backend(xp)
22
+ _set_simulator_backend(xp)
23
+ _set_config_backend(xp)
24
+ globals()["xp"] = xp
25
+ print(f"MIEPOM backend set to {xp.__name__}")
26
+
27
+ def get_backend():
28
+ """Get the name of the current linear algebra backend for the MIEPOM library."""
29
+ return globals()["xp"]
30
+
31
+ def get_backend_name():
32
+ """Get the name of the current linear algebra backend for the MIEPOM library."""
33
+ return globals()["xp"].__name__
miepom/config.py ADDED
@@ -0,0 +1,216 @@
1
+ """Configuration settings for the MIEPOM library."""
2
+
3
+ import numpy as np
4
+ import os, time
5
+ from .utils import print_line
6
+
7
+ globals()["xp"] = np
8
+
9
+ def set_backend(xp):
10
+ globals()["xp"] = xp
11
+ try:
12
+ import scipy as sp
13
+ USESCIPY = True
14
+ except ImportError:
15
+ USESCIPY = False
16
+ try:
17
+ import numba as nb
18
+ USENUMBA = True
19
+ except ImportError:
20
+ USENUMBA = False
21
+
22
+ try:
23
+ import mlx.core as mx
24
+ USEMLX = True
25
+ except ImportError:
26
+ USEMLX = False
27
+
28
+ try:
29
+ import cupy as cp
30
+ USECUPY = True
31
+ except ImportError:
32
+ USECUPY = False
33
+
34
+ VERBOSE = False if os.environ.get("MIEPOM_VERBOSE", "0") == "0" else True
35
+
36
+ # always use numpy as the default backend for the core object, but allow switching to other backends if available
37
+ DEFAULT_BACKEND = "numpy"
38
+
39
+ #: Computation precision. Note that if MLX (or CuPy) is used, it is always single precision
40
+ PRECISION = "single" if os.environ.get("MIEPOM_USE_DOUBLE", "0") == "0" else "double" # 'single' or 'double'
41
+
42
+ #: whether to use fast math (numba)
43
+ NBFASTMATH = False if os.environ.get("MIEPOM_FASTMATH", "0") == "0" else True
44
+
45
+ #: numba's compile target argument for vectorize and guvectorize functions.
46
+ NBTARGET = os.environ.get("MIEPOM_TARGET", "parallel")
47
+ if NBTARGET not in ["cpu", "parallel", "cuda"]:
48
+ raise ValueError(f"Invalid MIEPOM_TARGET value: {NBTARGET}. Must be one of 'cpu', 'parallel', or 'cuda'.")
49
+
50
+ #: if set to True, it assumes a vectorized version of the miepython library
51
+ VECTORIZED_MIE = False if os.environ.get("MIEPOM_VECTORIZED_MIE", "0") == "0" else True
52
+
53
+ # Computed constants below. Do not modify!
54
+ #-----------------------------------------
55
+
56
+ if USENUMBA:
57
+ import numba as nb
58
+
59
+ class nbtype():
60
+ def __getitem__(self, key):
61
+ pass
62
+
63
+ try:
64
+ import mlx.core as mx
65
+ mx_compile = mx.compile
66
+ mx_eval = mx.eval
67
+ except ImportError:
68
+ mx_compile = lambda f: f
69
+ mx_eval = lambda *args, **kwargs: None
70
+
71
+ def mx_compile_out(func):
72
+ """A decorator to compile a function with mlx.core.compile. This is used to compile the sensor_image function with mlx.
73
+ It also handles the 'out' keyword argument for in-place computation.
74
+ """
75
+ func = mx_compile(func)
76
+ def wrapper(*args, **kwargs):
77
+ if 'out' in kwargs:
78
+ out = kwargs.pop('out')
79
+ else:
80
+ out = None
81
+ if out is not None:
82
+ out[:] = func(*args, **kwargs)[:]
83
+ else:
84
+ out = func(*args, **kwargs)
85
+ return out
86
+ return wrapper
87
+
88
+ def mx_out(func):
89
+ def wrapper(*args, **kwargs):
90
+ if 'out' in kwargs:
91
+ out = kwargs.pop('out')
92
+ else:
93
+ out = None
94
+ if out is not None:
95
+ out[:] = func(*args, **kwargs)[:]
96
+ else:
97
+ out = func(*args, **kwargs)
98
+ return out
99
+ return wrapper
100
+
101
+ if USENUMBA:
102
+ vectorize = nb.vectorize
103
+ guvectorize = nb.guvectorize
104
+ jit = nb.jit
105
+ njit = nb.njit
106
+ else:
107
+ vectorize = lambda f,*args,**kwargs: f
108
+ guvectorize = lambda f,*args,**kwargs: f
109
+ jit = lambda f,*args,**kwargs: f
110
+ njit = lambda f,*args,**kwargs: f
111
+ NBFLOAT = nbtype()
112
+ NBCOMPLEX = nbtype()
113
+ NBUINT = nbtype()
114
+ NBBOOL = nbtype()
115
+
116
+ nbfloat = nbtype()
117
+ nbcomplex = nbtype()
118
+ nbuint = nbtype()
119
+ nbbool = nbtype()
120
+
121
+ os.environ["MIEPYTHON_CACHE"] = "0"
122
+ os.environ["MIEPYTHON_FASTMATH"] = "1"
123
+ os.environ["MIEPYTHON_TARGET"] = NBTARGET
124
+
125
+ if PRECISION == "single":
126
+ os.environ["MIEPYTHON_USE_DOUBLE"] = "0"
127
+ if USENUMBA:
128
+ NBFLOAT = nb.float32
129
+ NBCOMPLEX = nb.complex64
130
+ NBUINT = nb.uint16
131
+ NBBOOL = nb.boolean
132
+
133
+ nbfloat = nb.float32
134
+ nbcomplex = nb.complex64
135
+ nbuint = nb.uint16
136
+ nbbool = nb.boolean
137
+
138
+ CDTYPE = np.complex64
139
+ FDTYPE = np.float32
140
+ UDTYPE = "uint16"
141
+
142
+ cdtype = lambda: xp.complex64
143
+ fdtype = lambda: xp.float32
144
+ udtype = lambda: xp.uint16
145
+
146
+ elif PRECISION == "double":
147
+ os.environ["MIEPYTHON_USE_DOUBLE"] = "1"
148
+ if USENUMBA:
149
+ NBFLOAT = nb.float64
150
+ NBCOMPLEX = nb.complex128
151
+ NBUINT = nb.uint16
152
+ NBBOOL = nb.boolean
153
+
154
+ nbfloat = nb.float64
155
+ nbcomplex = nb.complex128
156
+ nbuint = nb.uint16
157
+ nbbool = nb.boolean
158
+
159
+ CDTYPE = np.complex128
160
+ FDTYPE = np.float64
161
+ UDTYPE = "uint16"
162
+
163
+ cdtype = lambda: xp.complex128
164
+ fdtype = lambda: xp.float64
165
+ udtype = lambda: xp.uint16
166
+ else:
167
+ raise ValueError("Invalid precision name")
168
+
169
+ import miepython as mie
170
+
171
+ def print_settings():
172
+ print("Library settings")
173
+ print_line("-")
174
+
175
+ print(f"Using Numba: {USENUMBA}")
176
+ if USENUMBA:
177
+ print(f"Using fastmath: {NBFASTMATH}")
178
+ print(f"Numba target: '{NBTARGET}'")
179
+ print(f"Num threads: {nb.get_num_threads()}")
180
+ print(f"Precission: '{PRECISION}'")
181
+ print(f"Using mlx: {USEMLX}")
182
+ print(f"Using cupy: {USECUPY}")
183
+ print_line("=")
184
+
185
+ if VERBOSE:
186
+ print_settings()
187
+
188
+ print("numpy", np.__version__)
189
+ print("miepython", mie.__version__)
190
+ if USESCIPY:
191
+ print("scipy", sp.__version__)
192
+ if USECUPY:
193
+ print("cupy", cp.__version__)
194
+ if USEMLX:
195
+ print("mlx", mx.__version__)
196
+ if USENUMBA:
197
+ print("numba", nb.__version__)
198
+
199
+ print("-----------------------------------------------------------------------")
200
+
201
+ def get_array(x):
202
+ """Convert array to numpy array if it is a cupy or mlx array, otherwise return the input as is.
203
+ This is useful for converting arrays to numpy for further processing or visualization."""
204
+ if xp.__name__ == 'mlx.core':
205
+ # Evaluate the eoutput using mlx.eval to ensure it is a computed array in the mlx backend.
206
+ # This is necessary because mlx uses lazy evaluation, and we want to ensure that the result is a concrete array.
207
+ mx_eval(x)
208
+ return np.asarray(x)
209
+ elif xp.__name__ == 'cupy':
210
+ return x.get() # Convert CuPy array to NumPy array
211
+ else:
212
+ #try conversion to numpy array, if it fails, return the input as is
213
+ try:
214
+ return np.asarray(x)
215
+ except Exception:
216
+ return x
miepom/core.py ADDED
@@ -0,0 +1,12 @@
1
+ """Core module for the MIEPOM library.
2
+
3
+ This module provides convenient access to the main components of the MIEPOM library, including particles, simulators, microscopes, sensors, and backend management.
4
+ """
5
+
6
+ from .particles import *
7
+ from .sim import *
8
+ from .pom import *
9
+ from .sensor import *
10
+ from .backend import *
11
+
12
+ from .config import get_array
miepom/linalg.py ADDED
@@ -0,0 +1,195 @@
1
+ """Linear algebra utilities for the PPOM library."""
2
+
3
+ # Compiled numba code
4
+ from .config import USENUMBA, nbfloat,nbcomplex, nbuint, nbbool, NBFASTMATH, NBTARGET, vectorize, guvectorize, jit, njit, mx_compile, mx_compile_out, mx_compile
5
+ import numpy as np
6
+
7
+ # Set the default linear algebra backend to numpy so that we can compile and run numba functions correctly.
8
+ globals()["xp"] = np
9
+
10
+ def backend_name():
11
+ """Return the name of the current linear algebra backend."""
12
+ return globals()["xp"].__name__
13
+
14
+ def set_backend(xp):
15
+ globals()["xp"] = xp
16
+
17
+ # data packing and unpacking functions for boolean masks
18
+
19
+ # reference implementation of the pack function using numpy advanced indexing.
20
+ def _pack_np(data,mask):
21
+ return data[...,mask]
22
+
23
+ # numba implementation of the pack function, requires a dummy argument so that we can determine the output shape at compile time. This is a hack, but it works.
24
+ @guvectorize([(nbcomplex[:,:],nbbool[:,:],nbcomplex[:],nbcomplex[:])],
25
+ "(m,n),(m,n),(l)->(l)",boundscheck = False, target = NBTARGET, fastmath = NBFASTMATH)
26
+ def __pack_nb(a, mask, _dummy, out):
27
+ n = 0
28
+ for i in range(mask.shape[0]):
29
+ for j in range(mask.shape[1]):
30
+ if mask[i,j] == True:
31
+ if n < len(out):
32
+ out[n] = a[i,j]
33
+ n+=1
34
+
35
+ # numba wrapper to remove the dummy argument
36
+ def _pack_nb(a,mask, out = None):
37
+ if out is None:
38
+ out = np.empty(a.shape[:-2] + (np.sum(mask),), dtype = a.dtype)
39
+ return __pack_nb(a,mask,out,out = out)
40
+
41
+ # generic xp implementation that calls the numpy version and adds support for output array
42
+ def _pack_xp(data,mask, out = None):
43
+ if out is None:
44
+ return _pack_np(data,mask)
45
+ else:
46
+ out[:] = _pack_np(data,mask)
47
+ return out
48
+
49
+ # boolean indices are not supported in mlx, so we need to implement a custom pack function for mlx
50
+ def _pack_mx(data,mask, out = None):
51
+ if out is None:
52
+ #we convert to numpy array first, because mlx does not support boolean indexing
53
+ return xp.array(np.asarray(data)[...,mask])
54
+ else:
55
+ out[:] = np.asarray(data)[...,mask]
56
+ return out
57
+
58
+ def _unpack_np(mask,data, out = None):
59
+ if out is None:
60
+ out = xp.zeros(data.shape[:-1] + mask.shape, dtype = data.dtype)
61
+ out[...] = 0.
62
+ out[...,mask] = data
63
+ return out
64
+
65
+ _unpack_xp = _unpack_np
66
+
67
+ # the mx version cannot use vectorize version because it does not support boolean indexing, so we need to implement a custom unpack function for mlx
68
+ def __unpack_mx(mask,data, out = None):
69
+ if out is None:
70
+ out = xp.zeros(mask.shape, dtype = data.dtype)
71
+ out[:] = 0.
72
+ out[mask] = data
73
+ return out
74
+
75
+ # the vectorized version of the mlx unpack function is used when the data is 2D, so that we can use vmap to speed up the computation.
76
+ def __unpack_mx_vec(mask,data, out = None):
77
+ if out is None:
78
+ return xp.vmap(__unpack_mx, in_axes=(None, 0), out_axes=0)(mask,data)
79
+ else:
80
+ out[:] = 0.
81
+ tmp = xp.vmap(__unpack_mx, in_axes=(None, 0), out_axes=0)(mask,data)
82
+ out[:] = tmp
83
+ return out
84
+
85
+ def _unpack_mx(mask,data, out = None):
86
+ if data.ndim == 2:
87
+ return __unpack_mx_vec(mask,data, out = out)
88
+ else:
89
+ return __unpack_mx(mask,data, out = out)
90
+
91
+ @jit([(nbbool[:,:],nbcomplex[:],nbcomplex[:,:])])
92
+ def __unpack_nb(mask,data,out):
93
+ k = 0
94
+ for i in range(out.shape[0]):
95
+ for j in range(out.shape[1]):
96
+ if mask[i,j] == True:
97
+ out[i,j] = data[k]
98
+ k+=1
99
+ else:
100
+ out[i,j] = 0.
101
+
102
+ @guvectorize([(nbbool[:,:],nbcomplex[:],nbcomplex[:,:])], "(n,m),(k)->(n,m)", target = NBTARGET, fastmath = NBFASTMATH)
103
+ def _unpack_nb(mask,data,out):
104
+ __unpack_nb(mask,data,out)
105
+
106
+ def unpack(mask, data, out = None):
107
+ """Unpack a 1D array into a 2D array at the locations specified by a boolean mask."""
108
+ if backend_name() == "mlx.core":
109
+ return _unpack_mx(mask,data, out = out)
110
+ elif backend_name() == "numba":
111
+ return _unpack_nb(mask,data, out = out)
112
+ else:
113
+ return _unpack_xp(mask,data, out)
114
+
115
+ def pack(data, mask, out = None):
116
+ """Pack a 2D array into a 1D array at the locations specified by a boolean mask."""
117
+ if backend_name() == "mlx.core":
118
+ return _pack_mx(data, mask, out)
119
+ elif backend_name() == "numba":
120
+ return _pack_nb(data, mask, out)
121
+ else:
122
+ return _pack_xp(data, mask, out)
123
+
124
+ def _abs2_np(a):
125
+ return a.real**2 + a.imag**2
126
+
127
+ _abs2_nb = vectorize([nbfloat(nbcomplex)],target = NBTARGET,fastmath = NBFASTMATH)(_abs2_np)
128
+
129
+ def _abs2_mx(a, out = None):
130
+ if out is None:
131
+ return mx_compile(_abs2_np)(a)
132
+ else:
133
+ out[:] = mx_compile(_abs2_np)(a)
134
+ return out
135
+
136
+ def _abs2_xp(a, out = None):
137
+ x2 = np.real(a)**2
138
+ y = np.imag(a)
139
+ y2 = np.multiply(y, y, out = out)
140
+ return np.add(x2, y2, out = y2)
141
+
142
+ def abs2(a, out = None):
143
+ if backend_name() == "mlx.core":
144
+ return _abs2_mx(a, out)
145
+ elif backend_name() == "numba":
146
+ return _abs2_nb(a, out)
147
+ else:
148
+ return _abs2_xp(a, out)
149
+
150
+ _vecdot_xp = np.vecdot
151
+
152
+ def _vecdot_mx(a,b, out = None):
153
+ if out is None:
154
+ return mx_compile(_vecdot_xp)(a,b)
155
+ else:
156
+ out[:] = mx_compile(_vecdot_xp)(a,b)
157
+ return out
158
+
159
+ @guvectorize([(nbfloat[:],nbfloat[:],nbfloat[:])], "(n),(n)->()",
160
+ fastmath = NBFASTMATH, cache = True, target = NBTARGET)
161
+ def _vecdot_nb(a,b, out):
162
+ tmp = 0
163
+ for i in range(3):
164
+ tmp += a[i] * b[i]
165
+ out[0] = tmp
166
+
167
+ def vecdot(a,b, out = None):
168
+ if backend_name() == "mlx.core":
169
+ return _vecdot_mx(a,b, out)
170
+ elif backend_name() == "numba":
171
+ return _vecdot_nb(a,b, out)
172
+ else:
173
+ return _vecdot_xp(a,b, out)
174
+
175
+ def _exp_xp(x):
176
+ return xp.exp(x)
177
+
178
+ def _exp_mx(x, out = None):
179
+ if out is None:
180
+ return mx_compile(_exp_xp)(x)
181
+ else:
182
+ out[:] = mx_compile(_exp_xp)(x)
183
+ return out
184
+
185
+ _exp_nb = vectorize([nbcomplex(nbcomplex)], target = NBTARGET, fastmath = NBFASTMATH)(_exp_xp)
186
+
187
+ def exp(x):
188
+ """Compute the complex exponential of a real array."""
189
+ if backend_name() == "mlx.core":
190
+ return _exp_mx(x)
191
+ elif backend_name() == "numba":
192
+ return _exp_nb(x)
193
+ else:
194
+ return _exp_xp(x)
195
+