msptools 0.1.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.
- msptools/GreenTensor_Electric.py +236 -0
- msptools/MSP.py +175 -0
- msptools/OFO_calculations.py +36 -0
- msptools/__init__.py +192 -0
- msptools/backend.py +22 -0
- msptools/dipole_moments.py +27 -0
- msptools/field_mod.py +220 -0
- msptools/particle_types.py +31 -0
- msptools/particles_mod.py +78 -0
- msptools/permittivity.py +55 -0
- msptools/polarizability_mod.py +191 -0
- msptools/tools/field_tools.py +170 -0
- msptools/tools/ridx_usage.py +27 -0
- msptools/tools/unit_calcs.py +192 -0
- msptools-0.1.0.dist-info/METADATA +81 -0
- msptools-0.1.0.dist-info/RECORD +19 -0
- msptools-0.1.0.dist-info/WHEEL +5 -0
- msptools-0.1.0.dist-info/licenses/LICENSE +21 -0
- msptools-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from .backend import get_backend
|
|
2
|
+
from numpy.typing import ArrayLike
|
|
3
|
+
import numpy as np
|
|
4
|
+
from scipy.constants import pi
|
|
5
|
+
from cmath import exp
|
|
6
|
+
|
|
7
|
+
def G_0_function(r: float | ArrayLike, wave_number: float) -> complex | ArrayLike:
|
|
8
|
+
"""
|
|
9
|
+
Computes the G_0 function for a given distance r and wave number.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
r :
|
|
14
|
+
The distance between two points.
|
|
15
|
+
wave_number :
|
|
16
|
+
The wave number.
|
|
17
|
+
|
|
18
|
+
Returns
|
|
19
|
+
-------
|
|
20
|
+
complex | ArrayLike
|
|
21
|
+
The value of the G_0 function.
|
|
22
|
+
"""
|
|
23
|
+
if isinstance(r, (float, int)):
|
|
24
|
+
if r > 0:
|
|
25
|
+
kr = wave_number * r
|
|
26
|
+
return exp(1j * wave_number * r) / (4 * pi * r) * (1 + 1j/kr - 1/kr**2)
|
|
27
|
+
else:
|
|
28
|
+
return 0.0j
|
|
29
|
+
else:
|
|
30
|
+
xp = get_backend(r)
|
|
31
|
+
mask = r > 0
|
|
32
|
+
result = xp.zeros_like(r, dtype=complex)
|
|
33
|
+
kr = wave_number * r[mask]
|
|
34
|
+
result[mask] = xp.exp(1j * kr) / (4 * pi * r[mask]) * (1 + 1j/kr - 1/kr**2)
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
def G_1_function(r: float | ArrayLike, wave_number: float) -> complex | ArrayLike:
|
|
38
|
+
"""
|
|
39
|
+
Computes the G_1 function for a given distance r and wave number.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
r :
|
|
44
|
+
The distance between two points.
|
|
45
|
+
wave_number :
|
|
46
|
+
The wave number.
|
|
47
|
+
|
|
48
|
+
Returns
|
|
49
|
+
-------
|
|
50
|
+
complex | ArrayLike
|
|
51
|
+
The value of the G_1 function.
|
|
52
|
+
"""
|
|
53
|
+
if isinstance(r, (float, int)):
|
|
54
|
+
if r > 0:
|
|
55
|
+
kr = wave_number * r
|
|
56
|
+
return -exp(1j * wave_number * r) / (4 * pi * r**3) * (1 + 3j/kr - 3/kr**2)
|
|
57
|
+
else:
|
|
58
|
+
return 0.0j
|
|
59
|
+
else:
|
|
60
|
+
xp = get_backend(r)
|
|
61
|
+
mask = r > 0
|
|
62
|
+
result = xp.zeros_like(r, dtype=complex)
|
|
63
|
+
kr = wave_number * r[mask]
|
|
64
|
+
result[mask] = -xp.exp(1j * kr) / (4 * xp.pi * r[mask]**3) * (1 + 3j/kr - 3/kr**2)
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
def G_0_derivative_function(r: float | ArrayLike, wave_number: float) -> complex:
|
|
68
|
+
"""
|
|
69
|
+
Computes the derivative of the G_0 function with respect to r.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
r : float
|
|
74
|
+
The distance between two points.
|
|
75
|
+
wave_number : float
|
|
76
|
+
The wave number.
|
|
77
|
+
|
|
78
|
+
Returns
|
|
79
|
+
-------
|
|
80
|
+
complex
|
|
81
|
+
The value of the derivative of the G_0 function.
|
|
82
|
+
"""
|
|
83
|
+
if isinstance(r, (float, int)):
|
|
84
|
+
xp = np
|
|
85
|
+
else:
|
|
86
|
+
xp = get_backend(r)
|
|
87
|
+
return wave_number * xp.exp(1j * wave_number * r) / (4 * xp.pi * r) * \
|
|
88
|
+
(1j - 2/(wave_number * r) - 3j/(wave_number * r)**2 + 3/(wave_number * r)**3)
|
|
89
|
+
|
|
90
|
+
def G_1_derivative_function(r: float | ArrayLike, wave_number: float) -> complex:
|
|
91
|
+
"""
|
|
92
|
+
Computes the derivative of the G_1 function with respect to r.
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
r : float
|
|
97
|
+
The distance between two points.
|
|
98
|
+
wave_number : float
|
|
99
|
+
The wave number.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
complex
|
|
104
|
+
The value of the derivative of the G_1 function.
|
|
105
|
+
"""
|
|
106
|
+
if isinstance(r, (float, int)):
|
|
107
|
+
xp = np
|
|
108
|
+
else:
|
|
109
|
+
xp = get_backend(r)
|
|
110
|
+
return -wave_number * xp.exp(1j * wave_number * r) / (4 * xp.pi * r**3) * \
|
|
111
|
+
(1j - 6/(wave_number * r) - 15j/(wave_number * r)**2 + 15/(wave_number * r)**3)
|
|
112
|
+
|
|
113
|
+
def v_cross_derivative(r_vec: ArrayLike, coordinate: int) -> np.ndarray:
|
|
114
|
+
"""
|
|
115
|
+
Computes the derivative of a vector cross dyadic product with respect to a specific coordinate.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
r_vec :
|
|
120
|
+
The vector for which the derivative is computed.
|
|
121
|
+
coordinate :
|
|
122
|
+
The coordinate with respect to which the derivative is taken (0, 1, or 2).
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
np.ndarray
|
|
127
|
+
The derivative of the cross product with respect to the specified coordinate.
|
|
128
|
+
"""
|
|
129
|
+
xp = get_backend(r_vec)
|
|
130
|
+
|
|
131
|
+
dimensions = r_vec.shape[0]
|
|
132
|
+
if coordinate < 0 or coordinate >= dimensions:
|
|
133
|
+
raise ValueError("Coordinate must be in the range [0, {}]".format(dimensions - 1))
|
|
134
|
+
|
|
135
|
+
der_R_cross = xp.zeros((dimensions, dimensions))
|
|
136
|
+
|
|
137
|
+
for i in range(dimensions):
|
|
138
|
+
if i == coordinate:
|
|
139
|
+
der_R_cross[i, i] = 2 * r_vec[i]
|
|
140
|
+
else:
|
|
141
|
+
der_R_cross[i, coordinate] = r_vec[i]
|
|
142
|
+
der_R_cross[coordinate, i] = r_vec[i]
|
|
143
|
+
|
|
144
|
+
return der_R_cross
|
|
145
|
+
|
|
146
|
+
def construct_green_tensor(positions : np.ndarray, wave_number: float) -> np.ndarray:
|
|
147
|
+
"""
|
|
148
|
+
Constructs the Green's tensor for a given set of positions and wave number.
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
positions : np.ndarray
|
|
153
|
+
Array of shape (num_particles, dimension) containing the positions of the particles.
|
|
154
|
+
wave_number : float
|
|
155
|
+
The wave number.
|
|
156
|
+
|
|
157
|
+
Returns
|
|
158
|
+
-------
|
|
159
|
+
np.ndarray
|
|
160
|
+
Green's tensor of shape (num_particles, num_particles, dimension, dimension).
|
|
161
|
+
"""
|
|
162
|
+
xp = get_backend(positions)
|
|
163
|
+
dimensions = positions.shape[1]
|
|
164
|
+
rel_vec_matrix = positions[:, None, :] - positions[None, :, :]
|
|
165
|
+
distances = xp.linalg.norm(rel_vec_matrix, axis=-1)
|
|
166
|
+
G_0_matrix = G_0_function(distances, wave_number)
|
|
167
|
+
G_1_matrix = G_1_function(distances, wave_number)
|
|
168
|
+
R_cross_matrix = rel_vec_matrix[:, :, :, None] * rel_vec_matrix[:, :, None, :]
|
|
169
|
+
green_tensor = G_0_matrix[:, :, None, None] * xp.eye(dimensions) + G_1_matrix[:, :, None, None] * R_cross_matrix
|
|
170
|
+
|
|
171
|
+
return green_tensor
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def pair_green_tensor_derivative(pos_i: np.ndarray, pos_j: np.ndarray, coordinate : int, wave_number: float):
|
|
175
|
+
"""
|
|
176
|
+
Constructs the derivative of the pair Green's tensor with respect to a specific coordinate.
|
|
177
|
+
|
|
178
|
+
Parameters
|
|
179
|
+
----------
|
|
180
|
+
pos_i : np.ndarray
|
|
181
|
+
Position of the first particle.
|
|
182
|
+
pos_j : np.ndarray
|
|
183
|
+
Position of the second particle.
|
|
184
|
+
coordinate : int
|
|
185
|
+
The coordinate with respect to which the derivative is taken (0, 1, or 2).
|
|
186
|
+
wave_number : float
|
|
187
|
+
The wave number.
|
|
188
|
+
|
|
189
|
+
Returns
|
|
190
|
+
-------
|
|
191
|
+
np.ndarray
|
|
192
|
+
Derivative of the pair Green's tensor with respect to the specified coordinate.
|
|
193
|
+
"""
|
|
194
|
+
xp = get_backend(pos_i)
|
|
195
|
+
dimensions = pos_i.shape[0]
|
|
196
|
+
R_vec = pos_i - pos_j
|
|
197
|
+
r = xp.linalg.norm(R_vec)
|
|
198
|
+
|
|
199
|
+
g_1 = G_1_function(r, wave_number)
|
|
200
|
+
der_g_0 = G_0_derivative_function(r, wave_number) * R_vec[coordinate] / r
|
|
201
|
+
der_g_1 = G_1_derivative_function(r, wave_number) * R_vec[coordinate] / r
|
|
202
|
+
R_cross = R_vec[:, None] @ R_vec[None, :]
|
|
203
|
+
der_R_cross = v_cross_derivative(R_vec, coordinate)
|
|
204
|
+
|
|
205
|
+
derivative_tensor = der_g_0 * xp.eye(dimensions) + der_g_1 * R_cross + g_1 * der_R_cross
|
|
206
|
+
|
|
207
|
+
return derivative_tensor
|
|
208
|
+
|
|
209
|
+
def construct_green_tensor_gradient(positions : np.ndarray, wave_number: float) -> np.ndarray:
|
|
210
|
+
"""
|
|
211
|
+
Constructs the derivative of the Green's tensor for a given set of positions and wave number.
|
|
212
|
+
|
|
213
|
+
Parameters
|
|
214
|
+
----------
|
|
215
|
+
positions : np.ndarray
|
|
216
|
+
Array of shape (num_particles, dimension) containing the positions of the particles.
|
|
217
|
+
wave_number : float
|
|
218
|
+
The wave number.
|
|
219
|
+
|
|
220
|
+
Returns
|
|
221
|
+
-------
|
|
222
|
+
np.ndarray
|
|
223
|
+
Derivative of Green's tensor of shape (num_particles, num_particles, dimension, dimension, dimension).
|
|
224
|
+
"""
|
|
225
|
+
xp = get_backend(positions)
|
|
226
|
+
|
|
227
|
+
num_particles, dimensions = positions.shape
|
|
228
|
+
green_tensor_derivative = xp.zeros((num_particles, num_particles, dimensions, dimensions, dimensions), dtype=xp.complex128)
|
|
229
|
+
|
|
230
|
+
for i in range(num_particles):
|
|
231
|
+
for j in range(i + 1, num_particles):
|
|
232
|
+
for coord in range(dimensions):
|
|
233
|
+
green_tensor_derivative[i, j, coord, :, :] = pair_green_tensor_derivative(positions[i], positions[j], coord, wave_number)
|
|
234
|
+
green_tensor_derivative[j, i, coord, :, :] = -green_tensor_derivative[i, j, coord, :, :]
|
|
235
|
+
return green_tensor_derivative
|
|
236
|
+
|
msptools/MSP.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from .backend import get_backend
|
|
2
|
+
from typing import Iterable
|
|
3
|
+
from numpy.typing import ArrayLike
|
|
4
|
+
|
|
5
|
+
from msptools.dipole_moments import calculate_dipole_moments_linear
|
|
6
|
+
|
|
7
|
+
def solve_MSP_from_arrays(polarizability: ArrayLike,
|
|
8
|
+
external_field : ArrayLike,
|
|
9
|
+
wave_number : float,
|
|
10
|
+
green_tensor : ArrayLike,
|
|
11
|
+
method : str = 'Inverse',
|
|
12
|
+
**kwargs) -> ArrayLike:
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
Solve the Multiple Scattering Problem (MSP) using the provided arrays.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
polarizability :
|
|
20
|
+
Polarizability of the particles, can be a complex number, float, int, list, or numpy array.
|
|
21
|
+
external_field :
|
|
22
|
+
External field on particles positions.
|
|
23
|
+
wave_number :
|
|
24
|
+
Wave number of the incident wave.
|
|
25
|
+
green_tensor :
|
|
26
|
+
Green's tensor for the system.
|
|
27
|
+
method :
|
|
28
|
+
Method to solve the MSP, either 'Iterative' or 'Inverse'. The default is 'Iterative'.
|
|
29
|
+
|
|
30
|
+
Returns
|
|
31
|
+
-------
|
|
32
|
+
ArrayLike
|
|
33
|
+
The solution to the MSP.
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if green_tensor.ndim != 4 or green_tensor.shape[0] != green_tensor.shape[1] or green_tensor.shape[2] != green_tensor.shape[3]:
|
|
39
|
+
raise ValueError("Invalid green_tensor shape. Expected shape (N, N, d, d), got {}".format(green_tensor.shape))
|
|
40
|
+
if green_tensor.shape[0] != external_field.shape[0]:
|
|
41
|
+
raise ValueError("The first dimension of green_tensor must match the number of particles in external_field. Expected {}, got {}".format(external_field.shape[0], green_tensor.shape[0]))
|
|
42
|
+
if green_tensor.shape[2] != external_field.shape[1]:
|
|
43
|
+
raise ValueError("The third dimension of green_tensor must match the system dimensionality. Expected {}, got {}".format(external_field.shape[1], green_tensor.shape[2]))
|
|
44
|
+
|
|
45
|
+
if method == 'Iterative':
|
|
46
|
+
if 'tolerance' in kwargs:
|
|
47
|
+
tolerance = kwargs['tolerance']
|
|
48
|
+
return array_MSP_iterative(polarizability, external_field, wave_number, green_tensor, tolerance=tolerance)
|
|
49
|
+
else:
|
|
50
|
+
return array_MSP_iterative(polarizability, external_field, wave_number, green_tensor)
|
|
51
|
+
elif method == 'Inverse':
|
|
52
|
+
return array_MSP_inverse(polarizability, external_field, wave_number, green_tensor)
|
|
53
|
+
else:
|
|
54
|
+
raise ValueError("Unknown method: {}".format(method))
|
|
55
|
+
|
|
56
|
+
def array_MSP_iterative(polarizability : ArrayLike,
|
|
57
|
+
external_field : ArrayLike,
|
|
58
|
+
wave_number : float,
|
|
59
|
+
green_tensor : ArrayLike,
|
|
60
|
+
num_iterations : int = 500,
|
|
61
|
+
tolerance : float = 1e-6) -> ArrayLike:
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
Solve the MSP using an iterative method.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
polarizability :
|
|
69
|
+
Polarizability of the particles.
|
|
70
|
+
external_field :
|
|
71
|
+
External field on particles positions.
|
|
72
|
+
wave_number :
|
|
73
|
+
Wave number of the incident wave.
|
|
74
|
+
green_tensor :
|
|
75
|
+
Green's tensor for the system.
|
|
76
|
+
num_iterations : optional
|
|
77
|
+
Maximum number of iterations for the iterative method. Default is 500.
|
|
78
|
+
tolerance : optional
|
|
79
|
+
Convergence tolerance for the iterative method. Default is 1e-6.
|
|
80
|
+
|
|
81
|
+
Returns
|
|
82
|
+
-------
|
|
83
|
+
xp.ndarray
|
|
84
|
+
The solution to the MSP.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
xp = get_backend(external_field)
|
|
88
|
+
old_field = external_field.copy()
|
|
89
|
+
|
|
90
|
+
for iteration in range(num_iterations):
|
|
91
|
+
|
|
92
|
+
dipole_moments = calculate_dipole_moments_linear(polarizability, old_field)
|
|
93
|
+
scattered_field = wave_number**2 * xp.einsum('ijmn,jn->im', green_tensor, dipole_moments)
|
|
94
|
+
new_field = external_field + scattered_field
|
|
95
|
+
|
|
96
|
+
if xp.allclose(new_field, old_field, rtol=tolerance):
|
|
97
|
+
break
|
|
98
|
+
old_field = new_field.copy()
|
|
99
|
+
|
|
100
|
+
if iteration == num_iterations - 1:
|
|
101
|
+
print(f"Warning: MSP iterative solution did not converge within {num_iterations} iterations.")
|
|
102
|
+
|
|
103
|
+
return new_field
|
|
104
|
+
|
|
105
|
+
def array_MSP_inverse(polarizability : ArrayLike,
|
|
106
|
+
external_field : ArrayLike,
|
|
107
|
+
wave_number : float,
|
|
108
|
+
green_tensor : ArrayLike) -> ArrayLike:
|
|
109
|
+
"""
|
|
110
|
+
Solve the MSP using the inverse method.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
polarizability :
|
|
115
|
+
Polarizability of the particles.
|
|
116
|
+
external_field :
|
|
117
|
+
External field on particles positions.
|
|
118
|
+
wave_number :
|
|
119
|
+
Wave number of the incident wave.
|
|
120
|
+
green_tensor :
|
|
121
|
+
Green's tensor for the system.
|
|
122
|
+
|
|
123
|
+
Returns
|
|
124
|
+
-------
|
|
125
|
+
xp.ndarray
|
|
126
|
+
The solution to the MSP.
|
|
127
|
+
"""
|
|
128
|
+
xp = get_backend(external_field)
|
|
129
|
+
num_particles = external_field.shape[0]
|
|
130
|
+
dimensions = external_field.shape[1]
|
|
131
|
+
|
|
132
|
+
scattering_matrix = wave_number**2 * xp.einsum('ijmk,jkl->ijml', green_tensor, polarizability)
|
|
133
|
+
|
|
134
|
+
MSP_matrix = xp.eye(num_particles * dimensions) - scattering_matrix.transpose(0,2,1,3).reshape(num_particles * dimensions, num_particles * dimensions)
|
|
135
|
+
|
|
136
|
+
total_field = xp.linalg.solve(MSP_matrix, external_field.flatten())
|
|
137
|
+
total_field = total_field.reshape(num_particles, dimensions)
|
|
138
|
+
return total_field
|
|
139
|
+
|
|
140
|
+
def MSP_gradient_from_arrays(dipole_moments: ArrayLike,
|
|
141
|
+
external_gradient : ArrayLike,
|
|
142
|
+
wave_number : float,
|
|
143
|
+
green_tensor_derivative : ArrayLike) -> ArrayLike:
|
|
144
|
+
"""
|
|
145
|
+
Compute the gradient of the MSP solution with respect to particle positions.
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
polarizability :
|
|
150
|
+
Polarizability of the particles.
|
|
151
|
+
MS_field :
|
|
152
|
+
Multiple scattering field on particles positions.
|
|
153
|
+
external_gradient :
|
|
154
|
+
Gradient of the external field on particles positions.
|
|
155
|
+
wave_number :
|
|
156
|
+
Wave number of the incident wave.
|
|
157
|
+
green_tensor_derivative :
|
|
158
|
+
Derivative of the Green's tensor with respect to particle positions.
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
xp.ndarray
|
|
163
|
+
The gradient of the MSP solution with respect to particle positions.
|
|
164
|
+
|
|
165
|
+
Notes
|
|
166
|
+
-----
|
|
167
|
+
The gradient is returned as an array of shape (N, d, d) where N is the number of particles and d is the dimensionality.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
xp = get_backend(external_gradient)
|
|
171
|
+
scattered_gradient = wave_number**2 * xp.einsum('ijcmn,jn->icm', green_tensor_derivative, dipole_moments)
|
|
172
|
+
|
|
173
|
+
MSP_gradient = external_gradient + scattered_gradient
|
|
174
|
+
|
|
175
|
+
return MSP_gradient
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from typing import Iterable
|
|
2
|
+
from .backend import get_backend
|
|
3
|
+
from numpy.typing import ArrayLike
|
|
4
|
+
|
|
5
|
+
def calculate_forces_eppgrad(medium_permittivity: float, dipole_moments: ArrayLike, field_gradient: ArrayLike) -> ArrayLike:
|
|
6
|
+
"""
|
|
7
|
+
Calculate the force on a set of dipoles in an electric field gradient.
|
|
8
|
+
|
|
9
|
+
Parameters
|
|
10
|
+
----------
|
|
11
|
+
medium_permittivity :
|
|
12
|
+
The permittivity of the medium in which the dipoles are located.
|
|
13
|
+
dipole_moments :
|
|
14
|
+
An array representing the dipole moments of the particles. Shape should be (N, d),
|
|
15
|
+
where N is the number of dipoles and d is the dimensionality.
|
|
16
|
+
field_gradient :
|
|
17
|
+
An array representing the electric field gradient at the location of the dipoles.
|
|
18
|
+
Shape should be (N, d, d), where N is the number of dipoles and d is the dimensionality.
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
Forces :
|
|
23
|
+
An array representing the force on each dipole.
|
|
24
|
+
|
|
25
|
+
Notes
|
|
26
|
+
-----
|
|
27
|
+
The force is calculated using the formula:
|
|
28
|
+
F = (ε/2) * Re{ p · ∇E* }
|
|
29
|
+
where ε is the medium permittivity, p is the dipole moment, and ∇E* is the complex conjugate of the electric field gradient.
|
|
30
|
+
"""
|
|
31
|
+
xp = get_backend(dipole_moments)
|
|
32
|
+
|
|
33
|
+
forces = (medium_permittivity / 2) * xp.real(xp.einsum('im,inm->in', dipole_moments, xp.conj(field_gradient)))
|
|
34
|
+
|
|
35
|
+
return forces
|
|
36
|
+
|
msptools/__init__.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from dataclasses import Field
|
|
2
|
+
import types
|
|
3
|
+
from .OFO_calculations import *
|
|
4
|
+
from .dipole_moments import *
|
|
5
|
+
from .polarizability_mod import *
|
|
6
|
+
from .particle_types import *
|
|
7
|
+
from .particles_mod import *
|
|
8
|
+
from .permittivity import *
|
|
9
|
+
from .field_mod import *
|
|
10
|
+
from .tools.unit_calcs import *
|
|
11
|
+
from .GreenTensor_Electric import *
|
|
12
|
+
from .MSP import *
|
|
13
|
+
from .backend import get_backend
|
|
14
|
+
from numpy.typing import ArrayLike, NDArray
|
|
15
|
+
from typing import List
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"OFO_calculations",
|
|
19
|
+
"dipole_moments",
|
|
20
|
+
"polarizability_mod",
|
|
21
|
+
"particle_types",
|
|
22
|
+
"particles_mod",
|
|
23
|
+
"permittivity",
|
|
24
|
+
"field_mod",
|
|
25
|
+
"unit_calcs",
|
|
26
|
+
"GreenTensor_Electric",
|
|
27
|
+
"MSP"
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
class System:
|
|
31
|
+
"""Class representing a Optical_Forces physical system containing particles."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, device: str = "GPU") -> None:
|
|
34
|
+
"""Initialize a System object by specifying the device to use for calculations."""
|
|
35
|
+
|
|
36
|
+
print(f"Initializing System with device: {device}")
|
|
37
|
+
if device not in ["GPU", "CPU"]:
|
|
38
|
+
raise ValueError("Invalid device specified. Use 'GPU' or 'CPU'.")
|
|
39
|
+
elif device == "GPU":
|
|
40
|
+
try:
|
|
41
|
+
import cupy as xp
|
|
42
|
+
except ImportError:
|
|
43
|
+
print("CuPy is not available. Falling back to CPU.")
|
|
44
|
+
import numpy as xp
|
|
45
|
+
else:
|
|
46
|
+
import numpy as xp
|
|
47
|
+
self.xp = xp
|
|
48
|
+
|
|
49
|
+
def set_system(self, particle_types : ParticleType | List[ParticleType], field: Field, positions_unit: str, medium_permittivity: float = 1.0) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Set a System object by specifying the particle types, the field and the medium permittivity.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
particle_types :
|
|
56
|
+
The type(s) of the particles in the system. This can be a single ParticleType or a list of ParticleType objects.
|
|
57
|
+
field :
|
|
58
|
+
The field in which the particles are placed. This should be an instance of the Field class.
|
|
59
|
+
positions_unit :
|
|
60
|
+
The unit of the positions of the particles. This should be a string representing a unit of length (e.g., 'nm', 'um', 'm').
|
|
61
|
+
medium_permittivity :
|
|
62
|
+
The permittivity of the medium in which the particles are placed. This should be a float.
|
|
63
|
+
"""
|
|
64
|
+
if not isinstance(particle_types, list):
|
|
65
|
+
particle_types = [particle_types]
|
|
66
|
+
self.particle_types = particle_types
|
|
67
|
+
self.field = field
|
|
68
|
+
self.field.set_medium_permittivity(medium_permittivity)
|
|
69
|
+
self.medium_permittivity = medium_permittivity
|
|
70
|
+
self.positions_unit = positions_unit
|
|
71
|
+
self.particles = Particles(self.xp)
|
|
72
|
+
self.medium_wave_number_nm = frequency_to_wavenumber_nm(self.field.frequency_eV) * self.xp.sqrt(self.medium_permittivity)
|
|
73
|
+
for ptype in self.particle_types:
|
|
74
|
+
ptype.compute_polarizability(frequency = self.field.frequency_eV, medium_permittivity=self.medium_permittivity)
|
|
75
|
+
|
|
76
|
+
def add_particles(self,
|
|
77
|
+
positions: ArrayLike,
|
|
78
|
+
particle_type: ParticleType | None = None) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Add particles to the system at specified positions.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
positions :
|
|
85
|
+
The position of the particles to add. This can be a 1D-three-element or 2D array-like.
|
|
86
|
+
type :
|
|
87
|
+
The type of the particles to add. If not specified, and there is only one type in the system, that type will be used.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
if particle_type is None and len(self.particle_types) > 1:
|
|
91
|
+
raise ValueError("When adding particles to a multi-type system, the 'particle_type' parameter must be specified.")
|
|
92
|
+
else:
|
|
93
|
+
particle_type = self.particle_types[0]
|
|
94
|
+
|
|
95
|
+
if particle_type is not None and particle_type not in self.particle_types:
|
|
96
|
+
raise ValueError("The specified particle type is not part of the system's types.")
|
|
97
|
+
|
|
98
|
+
positions = positions* get_multiplier_nanometers(self.positions_unit)
|
|
99
|
+
if positions.ndim == 1:
|
|
100
|
+
positions = self.xp.array(positions)
|
|
101
|
+
|
|
102
|
+
polarizability = polarizability_to_matrix(particle_type.polarizability, positions.shape[0], 3, self.xp)
|
|
103
|
+
print(f"positions type: {type(positions)}, shape: {positions.shape}")
|
|
104
|
+
print(f"polarizability type: {type(polarizability)}, shape: {polarizability.shape}")
|
|
105
|
+
self.particles.add_particles(positions=positions, polarizabilities=polarizability)
|
|
106
|
+
|
|
107
|
+
def get_field_in_particles(self, method : str = 'Inverse') -> ArrayLike:
|
|
108
|
+
"""
|
|
109
|
+
Get the electric field at specified positions by solving the Multiple Scattering Problem (MSP).
|
|
110
|
+
|
|
111
|
+
Returns
|
|
112
|
+
-------
|
|
113
|
+
xp.ndarray
|
|
114
|
+
The electric field at the specified positions.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
positions = self.particles.positions
|
|
118
|
+
external_field = self.field.get_external_field_in_positions(positions)
|
|
119
|
+
green_tensor = construct_green_tensor(positions, self.medium_wave_number_nm)
|
|
120
|
+
field_solution = solve_MSP_from_arrays(polarizability=self.particles.polarizabilities,
|
|
121
|
+
external_field=external_field,
|
|
122
|
+
wave_number=self.medium_wave_number_nm,
|
|
123
|
+
green_tensor=green_tensor,
|
|
124
|
+
method=method)
|
|
125
|
+
return field_solution
|
|
126
|
+
|
|
127
|
+
def get_field_gradient_in_particles(self, current_field: ArrayLike) -> ArrayLike:
|
|
128
|
+
"""
|
|
129
|
+
Get the electric field gradient at specified positions by solving the Multiple Scattering Problem (MSP) for the gradient.
|
|
130
|
+
|
|
131
|
+
Returns
|
|
132
|
+
-------
|
|
133
|
+
ArrayLike
|
|
134
|
+
The electric field gradient at the specified positions.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
external_gradient = self.field.get_external_gradient_in_positions(self.particles.positions)
|
|
138
|
+
green_tensor_derivative = construct_green_tensor_gradient(self.particles.positions, self.medium_wave_number_nm)
|
|
139
|
+
dipole_moments = calculate_dipole_moments_linear(self.particles.polarizabilities,
|
|
140
|
+
current_field)
|
|
141
|
+
gradient_solution = MSP_gradient_from_arrays(dipole_moments=dipole_moments,
|
|
142
|
+
external_gradient=external_gradient,
|
|
143
|
+
wave_number=self.medium_wave_number_nm,
|
|
144
|
+
green_tensor_derivative=green_tensor_derivative)
|
|
145
|
+
return gradient_solution
|
|
146
|
+
|
|
147
|
+
def set_position(self, index: int, position: ArrayLike) -> None:
|
|
148
|
+
"""
|
|
149
|
+
Set the position of a particle at a specified index.
|
|
150
|
+
|
|
151
|
+
Parameters
|
|
152
|
+
----------
|
|
153
|
+
index :
|
|
154
|
+
The index of the particle to set the position for.
|
|
155
|
+
|
|
156
|
+
position :
|
|
157
|
+
The new position of the particle. This can be a 1D-three-element array-like.
|
|
158
|
+
"""
|
|
159
|
+
position = xp.array(position)* get_multiplier_nanometers(self.positions_unit)
|
|
160
|
+
if position.ndim != 1 or position.shape[0] != 3:
|
|
161
|
+
raise ValueError("Position must be a 1D-three-element array-like.")
|
|
162
|
+
self.particles.set_position(index, position.tolist())
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class ForceCalculator:
|
|
166
|
+
"""Class to compute optical forces on particles in a System."""
|
|
167
|
+
|
|
168
|
+
def __init__(self, system: System) -> None:
|
|
169
|
+
"""
|
|
170
|
+
Initialize a ForceCalculator object by specifying the System.
|
|
171
|
+
"""
|
|
172
|
+
self.system = system
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def compute_forces(self) -> xp.ndarray:
|
|
176
|
+
"""
|
|
177
|
+
Compute the optical forces on particles at specified positions.
|
|
178
|
+
|
|
179
|
+
Returns
|
|
180
|
+
-------
|
|
181
|
+
xp.ndarray
|
|
182
|
+
The computed optical forces on the particles.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
E_field = self.system.get_field_in_particles()
|
|
186
|
+
E_grad = self.system.get_field_gradient_in_particles(E_field)
|
|
187
|
+
dipole_moments = calculate_dipole_moments_linear(self.system.particles.polarizabilities, E_field)
|
|
188
|
+
forces = calculate_forces_eppgrad(self.system.medium_permittivity, dipole_moments, E_grad)
|
|
189
|
+
|
|
190
|
+
return forces
|
|
191
|
+
|
|
192
|
+
|
msptools/backend.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from numpy.typing import ArrayLike
|
|
2
|
+
import numpy as np
|
|
3
|
+
try:
|
|
4
|
+
import cupy as cp
|
|
5
|
+
except ImportError:
|
|
6
|
+
cp = None
|
|
7
|
+
|
|
8
|
+
def get_backend(array: ArrayLike) -> None:
|
|
9
|
+
_backend = None
|
|
10
|
+
if isinstance(array, np.ndarray):
|
|
11
|
+
_backend = np
|
|
12
|
+
elif isinstance(array, cp.ndarray) and cp is not None:
|
|
13
|
+
_backend = cp
|
|
14
|
+
else:
|
|
15
|
+
raise ValueError("Unsupported array type. Only NumPy and CuPy arrays are supported.")
|
|
16
|
+
return _backend
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from .backend import get_backend
|
|
2
|
+
from numpy.typing import ArrayLike
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def calculate_dipole_moments_linear(polarizability: ArrayLike,
|
|
6
|
+
electric_field : ArrayLike) -> ArrayLike:
|
|
7
|
+
"""
|
|
8
|
+
Calculate the dipole moments of particles in an electric field using a linear polarizability model.
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
polarizability :
|
|
13
|
+
The polarizability of the particles. This is in general an (N, d, d) array, where N is the number of particles and d is the dimensionality of the system. It can also be a scalar (complex, float, or int) which will be applied to all particles.
|
|
14
|
+
electric_field :
|
|
15
|
+
The electric field at the location of the particles. This should be an array of shape (N, d), where N is the number of particles and d is the dimensionality of the system.
|
|
16
|
+
|
|
17
|
+
Returns
|
|
18
|
+
-------
|
|
19
|
+
ArrayLike
|
|
20
|
+
An array of shape (N, d) representing the dipole moments of the particles.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
xp = get_backend(electric_field)
|
|
24
|
+
dipole_moments = xp.einsum('ikl,il->ik', polarizability, electric_field)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
return dipole_moments
|