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.
@@ -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