PyMieSim 3.6.0__cp313-cp313-win_amd64.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.
- PyMieSim/__init__.py +16 -0
- PyMieSim/__main__.py +9 -0
- PyMieSim/_version.py +21 -0
- PyMieSim/binary/__init__.py +0 -0
- PyMieSim/binary/interface_detector.cp310-win_amd64.pyd +0 -0
- PyMieSim/binary/interface_detector.cp311-win_amd64.pyd +0 -0
- PyMieSim/binary/interface_detector.cp312-win_amd64.pyd +0 -0
- PyMieSim/binary/interface_detector.cp313-win_amd64.pyd +0 -0
- PyMieSim/binary/interface_experiment.cp310-win_amd64.pyd +0 -0
- PyMieSim/binary/interface_experiment.cp311-win_amd64.pyd +0 -0
- PyMieSim/binary/interface_experiment.cp312-win_amd64.pyd +0 -0
- PyMieSim/binary/interface_experiment.cp313-win_amd64.pyd +0 -0
- PyMieSim/binary/interface_scatterer.cp310-win_amd64.pyd +0 -0
- PyMieSim/binary/interface_scatterer.cp311-win_amd64.pyd +0 -0
- PyMieSim/binary/interface_scatterer.cp312-win_amd64.pyd +0 -0
- PyMieSim/binary/interface_scatterer.cp313-win_amd64.pyd +0 -0
- PyMieSim/binary/interface_sets.cp310-win_amd64.pyd +0 -0
- PyMieSim/binary/interface_sets.cp311-win_amd64.pyd +0 -0
- PyMieSim/binary/interface_sets.cp312-win_amd64.pyd +0 -0
- PyMieSim/binary/interface_sets.cp313-win_amd64.pyd +0 -0
- PyMieSim/binary/interface_source.cp310-win_amd64.pyd +0 -0
- PyMieSim/binary/interface_source.cp311-win_amd64.pyd +0 -0
- PyMieSim/binary/interface_source.cp312-win_amd64.pyd +0 -0
- PyMieSim/binary/interface_source.cp313-win_amd64.pyd +0 -0
- PyMieSim/binary/libcpp_coordinates.a +0 -0
- PyMieSim/binary/libcpp_detector.a +0 -0
- PyMieSim/binary/libcpp_experiment.a +0 -0
- PyMieSim/binary/libcpp_fibonacci.a +0 -0
- PyMieSim/binary/libcpp_mode_field.a +0 -0
- PyMieSim/binary/libcpp_sets.a +0 -0
- PyMieSim/binary/libcpp_source.a +0 -0
- PyMieSim/directories.py +31 -0
- PyMieSim/experiment/__init__.py +1 -0
- PyMieSim/experiment/dataframe_subclass.py +220 -0
- PyMieSim/experiment/detector/__init__.py +2 -0
- PyMieSim/experiment/detector/base.py +169 -0
- PyMieSim/experiment/detector/coherent_mode.py +50 -0
- PyMieSim/experiment/detector/photodiode.py +52 -0
- PyMieSim/experiment/scatterer/__init__.py +4 -0
- PyMieSim/experiment/scatterer/base.py +98 -0
- PyMieSim/experiment/scatterer/core_shell.py +82 -0
- PyMieSim/experiment/scatterer/cylinder.py +63 -0
- PyMieSim/experiment/scatterer/sphere.py +66 -0
- PyMieSim/experiment/setup.py +356 -0
- PyMieSim/experiment/source/__init__.py +2 -0
- PyMieSim/experiment/source/base.py +85 -0
- PyMieSim/experiment/source/gaussian.py +60 -0
- PyMieSim/experiment/source/planewave.py +69 -0
- PyMieSim/experiment/utils.py +132 -0
- PyMieSim/gui/__init__.py +0 -0
- PyMieSim/gui/helper.py +60 -0
- PyMieSim/gui/interface.py +136 -0
- PyMieSim/gui/section.py +606 -0
- PyMieSim/mesh.py +368 -0
- PyMieSim/polarization.py +174 -0
- PyMieSim/single/__init__.py +48 -0
- PyMieSim/single/detector/__init__.py +2 -0
- PyMieSim/single/detector/base.py +271 -0
- PyMieSim/single/detector/coherent.py +99 -0
- PyMieSim/single/detector/uncoherent.py +105 -0
- PyMieSim/single/representations.py +734 -0
- PyMieSim/single/scatterer/__init__.py +4 -0
- PyMieSim/single/scatterer/base.py +405 -0
- PyMieSim/single/scatterer/core_shell.py +126 -0
- PyMieSim/single/scatterer/cylinder.py +113 -0
- PyMieSim/single/scatterer/sphere.py +108 -0
- PyMieSim/single/source/__init__.py +3 -0
- PyMieSim/single/source/base.py +7 -0
- PyMieSim/single/source/gaussian.py +137 -0
- PyMieSim/single/source/planewave.py +97 -0
- PyMieSim/special_functions.py +81 -0
- PyMieSim/units.py +130 -0
- PyMieSim/validation_data/bohren_huffman/figure_810.csv +245 -0
- PyMieSim/validation_data/bohren_huffman/figure_87.csv +2 -0
- PyMieSim/validation_data/bohren_huffman/figure_88.csv +2 -0
- PyMieSim/validation_data/pymiescatt/example_coreshell_0.csv +41 -0
- PyMieSim/validation_data/pymiescatt/example_coreshell_1.csv +401 -0
- PyMieSim/validation_data/pymiescatt/example_shpere_0.csv +51 -0
- PyMieSim/validation_data/pymiescatt/example_shpere_1.csv +801 -0
- PyMieSim/validation_data/pymiescatt/example_shpere_2.csv +41 -0
- PyMieSim/validation_data/pymiescatt/example_shpere_3.csv +401 -0
- PyMieSim/validation_data/pymiescatt/example_sphere_0.csv +51 -0
- PyMieSim/validation_data/pymiescatt/example_sphere_1.csv +801 -0
- PyMieSim/validation_data/pymiescatt/example_sphere_2.csv +41 -0
- PyMieSim/validation_data/pymiescatt/example_sphere_3.csv +401 -0
- PyMieSim/validation_data/pymiescatt/validation_Qsca.csv +800 -0
- PyMieSim/validation_data/pymiescatt/validation_Qsca_coreshell_1.csv +400 -0
- PyMieSim/validation_data/pymiescatt/validation_Qsca_coreshell_2.csv +400 -0
- PyMieSim/validation_data/pymiescatt/validation_Qsca_medium.csv +800 -0
- PyMieSim/validation_data/pymiescatt/validation_coreshell.csv +81 -0
- PyMieSim/validation_data/pymiescatt/validation_sphere.csv +801 -0
- lib/libZBessel.a +0 -0
- lib/lib_ZBessel.a +0 -0
- lib/libcpp_base_scatterer.a +0 -0
- lib/libcpp_coreshell.a +0 -0
- lib/libcpp_cylinder.a +0 -0
- lib/libcpp_sphere.a +0 -0
- pymiesim-3.6.0.dist-info/METADATA +246 -0
- pymiesim-3.6.0.dist-info/RECORD +101 -0
- pymiesim-3.6.0.dist-info/WHEEL +5 -0
- pymiesim-3.6.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,734 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
|
4
|
+
import numpy
|
5
|
+
import MPSPlots
|
6
|
+
from pydantic.dataclasses import dataclass
|
7
|
+
from PyMieSim.special_functions import spherical_to_cartesian, rotate_on_x
|
8
|
+
from typing import List
|
9
|
+
import pyvista
|
10
|
+
from MPSPlots.colormaps import blue_black_red
|
11
|
+
import matplotlib.pyplot as plt
|
12
|
+
from PyMieSim.units import Quantity, meter
|
13
|
+
|
14
|
+
|
15
|
+
config_dict = dict(
|
16
|
+
arbitrary_types_allowed=True,
|
17
|
+
kw_only=True,
|
18
|
+
slots=True,
|
19
|
+
extra='forbid'
|
20
|
+
)
|
21
|
+
|
22
|
+
|
23
|
+
@dataclass(config=config_dict, kw_only=True)
|
24
|
+
class BaseRepresentation():
|
25
|
+
"""
|
26
|
+
Base class for scattering representations.
|
27
|
+
|
28
|
+
Parameters
|
29
|
+
----------
|
30
|
+
scatterer : BaseScatterer
|
31
|
+
The scatterer object, representing the physical scatterer in the simulation.
|
32
|
+
sampling : int
|
33
|
+
The number of points used for evaluating the Stokes parameters in spherical coordinates (default is 100).
|
34
|
+
distance : float
|
35
|
+
The distance from the scatterer at which fields are evaluated (default is 1.0).
|
36
|
+
|
37
|
+
Methods:
|
38
|
+
compute_components: A placeholder method intended to be overridden by subclasses for computing specific scattering components.
|
39
|
+
"""
|
40
|
+
scatterer: object
|
41
|
+
sampling: int
|
42
|
+
distance: Quantity
|
43
|
+
|
44
|
+
def __post_init__(self):
|
45
|
+
fields = self.scatterer._cpp_get_full_fields(
|
46
|
+
sampling=self.sampling,
|
47
|
+
distance=self.distance.to_base_units().magnitude
|
48
|
+
)
|
49
|
+
|
50
|
+
self.E_phi, self.E_theta, self.theta, self.phi = fields
|
51
|
+
|
52
|
+
self.compute_components()
|
53
|
+
|
54
|
+
def compute_components(self) -> None:
|
55
|
+
"""
|
56
|
+
Placeholder method for computing scattering components. Intended to be overridden by subclasses.
|
57
|
+
"""
|
58
|
+
raise NotImplementedError("This method should be implemented by subclasses.")
|
59
|
+
|
60
|
+
def get_colormap_limits(self, scalar: numpy.ndarray, symmetric: bool = False):
|
61
|
+
if symmetric:
|
62
|
+
max_abs = numpy.abs(scalar).max()
|
63
|
+
return [-max_abs, max_abs]
|
64
|
+
else:
|
65
|
+
return None
|
66
|
+
|
67
|
+
def add_theta_vector_to_3d_plot(
|
68
|
+
self,
|
69
|
+
scene: pyvista.Plotter,
|
70
|
+
n_points: int = 20,
|
71
|
+
opacity: float = 1.0,
|
72
|
+
radius: float = 1.0,
|
73
|
+
color: str = 'black') -> None:
|
74
|
+
"""
|
75
|
+
Adds a vector field to the 3D plot, representing vectors in the theta direction.
|
76
|
+
|
77
|
+
Parameters
|
78
|
+
----------
|
79
|
+
scene : pyvista.Plotter
|
80
|
+
The 3D plotting scene to which the vectors will be added.
|
81
|
+
n_points : int
|
82
|
+
Number of points to generate along the theta and phi directions. Default is 100.
|
83
|
+
opacity : float
|
84
|
+
Opacity of the vectors. Default is 1.0.
|
85
|
+
radius : float
|
86
|
+
Radius at which to place the vectors. Default is 1.0.
|
87
|
+
color : str
|
88
|
+
Color of the vectors. Default is 'black'.
|
89
|
+
"""
|
90
|
+
theta = numpy.linspace(0, 360, n_points)
|
91
|
+
phi = numpy.linspace(180, 0, n_points)
|
92
|
+
|
93
|
+
# Define the vector direction (unit vector along x-axis)
|
94
|
+
vector = numpy.array([1, 0, 0])
|
95
|
+
|
96
|
+
# Convert spherical coordinates to Cartesian coordinates
|
97
|
+
x, y, z = pyvista.transform_vectors_sph_to_cart(theta, phi, radius, *vector)
|
98
|
+
|
99
|
+
# Combine the Cartesian coordinates into a vector array
|
100
|
+
vector_field = numpy.c_[x.ravel(), y.ravel(), z.ravel()]
|
101
|
+
|
102
|
+
# Create a structured grid from spherical coordinates
|
103
|
+
spherical_grid = pyvista.grid_from_sph_coords(theta, phi, radius)
|
104
|
+
spherical_grid.point_data["component"] = vector_field * 0.1
|
105
|
+
|
106
|
+
# Generate glyphs (arrows) for the vectors
|
107
|
+
glyphs = spherical_grid.glyph(orient="component", scale="component", tolerance=0.005)
|
108
|
+
|
109
|
+
# Add the vector glyphs to the scene
|
110
|
+
scene.add_mesh(glyphs, color=color, opacity=opacity)
|
111
|
+
|
112
|
+
def add_phi_vector_to_3d_plot(
|
113
|
+
self,
|
114
|
+
scene: pyvista.Plotter,
|
115
|
+
n_points: int = 20,
|
116
|
+
opacity: float = 1.0,
|
117
|
+
radius: float = 1.0,
|
118
|
+
color: str = 'black') -> None:
|
119
|
+
"""
|
120
|
+
Adds a vector field to the 3D plot, representing vectors in the phi direction.
|
121
|
+
|
122
|
+
Parameters
|
123
|
+
----------
|
124
|
+
scene : pyvista.Plotter
|
125
|
+
The 3D plotting scene to which the vectors will be added.
|
126
|
+
n_points : int
|
127
|
+
Number of points to generate along the theta and phi directions. Default is 100.
|
128
|
+
opacity : float
|
129
|
+
Opacity of the vectors. Default is 1.0.
|
130
|
+
radius : float
|
131
|
+
Radius at which to place the vectors. Default is 1.0.
|
132
|
+
color : str
|
133
|
+
Color of the vectors. Default is 'black'.
|
134
|
+
"""
|
135
|
+
theta = numpy.linspace(0, 360, n_points)
|
136
|
+
phi = numpy.linspace(180, 0, n_points)
|
137
|
+
|
138
|
+
# Define the vector direction (unit vector along y-axis)
|
139
|
+
vector = numpy.array([0, 1, 0])
|
140
|
+
|
141
|
+
# Convert spherical coordinates to Cartesian coordinates
|
142
|
+
x, y, z = pyvista.transform_vectors_sph_to_cart(theta, phi, radius, *vector)
|
143
|
+
|
144
|
+
# Combine the Cartesian coordinates into a vector array
|
145
|
+
vector_field = numpy.c_[x.ravel(), y.ravel(), z.ravel()]
|
146
|
+
|
147
|
+
# Create a structured grid from spherical coordinates
|
148
|
+
spherical_grid = pyvista.grid_from_sph_coords(theta, phi, radius)
|
149
|
+
spherical_grid.point_data["component"] = vector_field * 0.1
|
150
|
+
|
151
|
+
# Generate glyphs (arrows) for the vectors
|
152
|
+
glyphs = spherical_grid.glyph(orient="component", scale="component", tolerance=0.005)
|
153
|
+
|
154
|
+
# Add the vector glyphs to the scene
|
155
|
+
scene.add_mesh(glyphs, color=color, opacity=opacity)
|
156
|
+
|
157
|
+
|
158
|
+
@dataclass(config=config_dict, kw_only=True)
|
159
|
+
class Stokes(BaseRepresentation):
|
160
|
+
r"""
|
161
|
+
Represents the scattering far-field in the Stokes representation.
|
162
|
+
|
163
|
+
Inherits from BaseRepresentation and calculates the Stokes parameters which describe the polarization state of light.
|
164
|
+
|
165
|
+
The Stokes parameters (I, Q, U, V) are defined according to their conventional definitions, representing the total intensity,
|
166
|
+
difference in intensities between horizontal and vertical polarizations, difference in intensities between two diagonal polarizations,
|
167
|
+
and the right and left circular polarizations, respectively.
|
168
|
+
|
169
|
+
| The stokes parameters are:
|
170
|
+
| I : intensity of the fields
|
171
|
+
| Q : linear polarization parallel to incident polarization
|
172
|
+
| U : linear polarization 45 degree to incident polarization
|
173
|
+
| V : Circular polarization
|
174
|
+
|
175
|
+
.. math:
|
176
|
+
I &= \big| E_x \big|^2 + \big| E_y \big|^2 \\[10pt]
|
177
|
+
|
178
|
+
Q &= \big| E_x \big|^2 - \big| E_y \big|^2 \\[10pt]
|
179
|
+
|
180
|
+
U &= 2 \mathcal{Re} \big\{ E_x E_y^* \big\} \\[10pt]
|
181
|
+
|
182
|
+
V &= 2 \mathcal{Im} \big\{ E_x E_y^* \big\} \\[10pt]
|
183
|
+
|
184
|
+
Methods:
|
185
|
+
compute_components: Computes the Stokes parameters based on the electric field components.
|
186
|
+
plot: Visualizes the Stokes parameters on a 3D plot.
|
187
|
+
|
188
|
+
"""
|
189
|
+
|
190
|
+
def compute_components(self) -> None:
|
191
|
+
r"""
|
192
|
+
Computes the Stokes parameters (I, Q, U, V) based on the electric field components (E_phi and E_theta).
|
193
|
+
|
194
|
+
The method calculates the normalized intensity (I), linear polarizations (Q and U), and circular polarization (V) of the light
|
195
|
+
scattered by the particle, using the electric field components in spherical coordinates.
|
196
|
+
|
197
|
+
The Stokes parameters are calculated using the following formulas:
|
198
|
+
|
199
|
+
.. math:
|
200
|
+
- I = |E_phi|^2 + |E_theta|^2
|
201
|
+
- Q = |E_phi|^2 - |E_theta|^2
|
202
|
+
- U = 2 * Re{E_phi * E_theta*}
|
203
|
+
- V = -2 * Im{E_phi * E_theta*}
|
204
|
+
|
205
|
+
The results are stored as attributes of the instance: I, Q, U, and V.
|
206
|
+
|
207
|
+
"""
|
208
|
+
intensity = numpy.abs(self.E_phi)**2 + numpy.abs(self.E_theta)**2
|
209
|
+
|
210
|
+
self.I = intensity / numpy.max(intensity) # noqa: E741
|
211
|
+
self.Q = (numpy.abs(self.E_phi)**2 - numpy.abs(self.E_theta)**2) / intensity
|
212
|
+
self.U = (+2 * numpy.real(self.E_phi * self.E_theta.conjugate())) / intensity
|
213
|
+
self.V = (-2 * numpy.imag(self.E_phi * self.E_theta.conjugate())) / intensity
|
214
|
+
|
215
|
+
def plot(
|
216
|
+
self,
|
217
|
+
unit_size: List[float] = (400, 400),
|
218
|
+
background_color: str = 'white',
|
219
|
+
show_edges: bool = False,
|
220
|
+
colormap: str = blue_black_red,
|
221
|
+
opacity: float = 1.0,
|
222
|
+
symmetric_colormap: bool = False,
|
223
|
+
show_axis_label: bool = False) -> None:
|
224
|
+
"""
|
225
|
+
Visualizes the Stokes parameters (I, Q, U, V) on a 3D plot.
|
226
|
+
|
227
|
+
Parameters
|
228
|
+
----------
|
229
|
+
unit_size : List[float]
|
230
|
+
The size of each subplot in pixels (width, height). Default is (400, 400).
|
231
|
+
background_color : str
|
232
|
+
The background color of the plot. Default is 'white'.
|
233
|
+
show_edges : bool
|
234
|
+
If True, displays the edges of the mesh. Default is False.
|
235
|
+
colormap : str
|
236
|
+
The colormap to use for scalar mapping. Default is 'blue_black_red'.
|
237
|
+
opacity : float
|
238
|
+
The opacity of the mesh. Default is 1.0.
|
239
|
+
symmetric_colormap : bool
|
240
|
+
If True, the colormap will be symmetric around zero. Default is False.
|
241
|
+
show_axis_label : bool
|
242
|
+
If True, shows the axis labels. Default is False.
|
243
|
+
"""
|
244
|
+
phi_mesh, theta_mesh = numpy.meshgrid(self.phi, self.theta)
|
245
|
+
x, y, z = spherical_to_cartesian(r=numpy.full_like(phi_mesh, 0.5), phi=phi_mesh, theta=theta_mesh)
|
246
|
+
|
247
|
+
window_size = (unit_size[1] * 4, unit_size[0]) # Four subplots horizontally
|
248
|
+
|
249
|
+
scene = pyvista.Plotter(theme=pyvista.themes.DocumentTheme(), window_size=window_size, shape=(1, 4))
|
250
|
+
scene.set_background(background_color)
|
251
|
+
|
252
|
+
for idx, (name, field) in enumerate(zip(['I', 'Q', 'U', 'V'], [self.I, self.Q, self.U, self.V])):
|
253
|
+
field = field.flatten(order='F')
|
254
|
+
mesh = pyvista.StructuredGrid(x, y, z)
|
255
|
+
scene.subplot(0, idx)
|
256
|
+
|
257
|
+
colormap_limits = self.get_colormap_limits(
|
258
|
+
scalar=field,
|
259
|
+
symmetric=symmetric_colormap
|
260
|
+
)
|
261
|
+
|
262
|
+
mapping = scene.add_mesh(
|
263
|
+
mesh,
|
264
|
+
cmap=colormap,
|
265
|
+
scalars=field, opacity=opacity,
|
266
|
+
style='surface',
|
267
|
+
show_edges=show_edges,
|
268
|
+
clim=colormap_limits,
|
269
|
+
show_scalar_bar=False
|
270
|
+
)
|
271
|
+
|
272
|
+
scene.add_axes_at_origin(labels_off=not show_axis_label)
|
273
|
+
scene.add_scalar_bar(mapper=mapping.mapper, title=f'{name} field')
|
274
|
+
|
275
|
+
scene.show()
|
276
|
+
|
277
|
+
|
278
|
+
@dataclass(config=config_dict, kw_only=True)
|
279
|
+
class FarField(BaseRepresentation):
|
280
|
+
r"""
|
281
|
+
Represents the scattering far-field in spherical coordinates.
|
282
|
+
|
283
|
+
Inherits from BaseRepresentation and visualizes the far-field pattern characterized by the perpendicular and parallel components
|
284
|
+
of the electric field in spherical coordinates.
|
285
|
+
|
286
|
+
.. math::
|
287
|
+
\text{Fields} = E_{||}(\phi,\theta)^2, E_{\perp}(\phi,\theta)^2
|
288
|
+
|
289
|
+
Methods:
|
290
|
+
compute_components: Calculates the field components. This implementation is a placeholder, as the components are precomputed.
|
291
|
+
plot: Visualizes the far-field pattern in a 3D plot.
|
292
|
+
|
293
|
+
"""
|
294
|
+
|
295
|
+
def compute_components(self) -> None:
|
296
|
+
"""
|
297
|
+
Placeholder method in FarField class. Does not perform any computation as field components are precomputed.
|
298
|
+
|
299
|
+
This method is intended to be consistent with the structure of BaseRepresentation but does not need to modify or compute
|
300
|
+
any attributes for FarField instances.
|
301
|
+
"""
|
302
|
+
return
|
303
|
+
|
304
|
+
def plot(
|
305
|
+
self,
|
306
|
+
unit_size: List[float] = (400, 400),
|
307
|
+
background_color: str = 'white',
|
308
|
+
show_edges: bool = False,
|
309
|
+
colormap: str = blue_black_red,
|
310
|
+
opacity: float = 1.0,
|
311
|
+
symmetric_colormap: bool = False,
|
312
|
+
show_axis_label: bool = False) -> None:
|
313
|
+
"""
|
314
|
+
Visualizes the Far field (in phi and theta vector projections) on a 3D plot.
|
315
|
+
|
316
|
+
Parameters
|
317
|
+
----------
|
318
|
+
unit_size : List[float]
|
319
|
+
The size of each subplot in pixels (width, height). Default is (400, 400).
|
320
|
+
background_color : str
|
321
|
+
The background color of the plot. Default is 'white'.
|
322
|
+
show_edges : bool
|
323
|
+
If True, displays the edges of the mesh. Default is False.
|
324
|
+
colormap : str
|
325
|
+
The colormap to use for scalar mapping. Default is 'blue_black_red'.
|
326
|
+
opacity : float
|
327
|
+
The opacity of the mesh. Default is 1.0.
|
328
|
+
symmetric_colormap : bool
|
329
|
+
If True, the colormap will be symmetric around zero. Default is False.
|
330
|
+
show_axis_label : bool
|
331
|
+
If True, shows the axis labels. Default is False.
|
332
|
+
"""
|
333
|
+
phi_mesh, theta_mesh = numpy.meshgrid(self.phi, self.theta)
|
334
|
+
x, y, z = spherical_to_cartesian(r=numpy.full_like(phi_mesh, 0.5), phi=phi_mesh, theta=theta_mesh)
|
335
|
+
|
336
|
+
window_size = (unit_size[1] * 4, unit_size[0]) # Two subplots horizontally
|
337
|
+
|
338
|
+
scene = pyvista.Plotter(theme=pyvista.themes.DocumentTheme(), window_size=window_size, shape=(1, 4))
|
339
|
+
scene.set_background(background_color)
|
340
|
+
|
341
|
+
repr = [self.E_phi.real, self.E_phi.imag, self.E_theta.real, self.E_theta.imag]
|
342
|
+
repr_label = ['phi real', 'phi imag', 'theta real', 'theta imag']
|
343
|
+
|
344
|
+
for idx, (label, field) in enumerate(zip(repr_label, repr)):
|
345
|
+
field = field.flatten(order='F')
|
346
|
+
mesh = pyvista.StructuredGrid(x, y, z)
|
347
|
+
scene.subplot(0, idx)
|
348
|
+
|
349
|
+
colormap_limits = self.get_colormap_limits(
|
350
|
+
scalar=field,
|
351
|
+
symmetric=symmetric_colormap
|
352
|
+
)
|
353
|
+
|
354
|
+
mapping = scene.add_mesh(
|
355
|
+
mesh,
|
356
|
+
cmap=colormap,
|
357
|
+
scalars=field,
|
358
|
+
opacity=opacity,
|
359
|
+
style='surface',
|
360
|
+
show_edges=show_edges,
|
361
|
+
clim=colormap_limits,
|
362
|
+
show_scalar_bar=False
|
363
|
+
)
|
364
|
+
if 'theta' in label:
|
365
|
+
self.add_theta_vector_to_3d_plot(scene=scene, radius=0.6)
|
366
|
+
|
367
|
+
if 'phi' in label:
|
368
|
+
self.add_phi_vector_to_3d_plot(scene=scene, radius=0.6)
|
369
|
+
|
370
|
+
scene.add_axes_at_origin(labels_off=not show_axis_label)
|
371
|
+
scene.add_scalar_bar(mapper=mapping.mapper, title=f'{label} field')
|
372
|
+
|
373
|
+
scene.show()
|
374
|
+
|
375
|
+
|
376
|
+
@dataclass(config=config_dict, kw_only=True)
|
377
|
+
class SPF(BaseRepresentation):
|
378
|
+
r"""
|
379
|
+
Represents the Scattering Phase Function (SPF).
|
380
|
+
|
381
|
+
Inherits from BaseRepresentation and computes the SPF, which is a measure of how light is scattered by a particle at different angles.
|
382
|
+
|
383
|
+
.. math::
|
384
|
+
\text{SPF} = E_{\parallel}(\phi,\theta)^2 + E_{\perp}(\phi,\theta)^2
|
385
|
+
|
386
|
+
Methods:
|
387
|
+
compute_components: Computes the SPF based on the electric field components.
|
388
|
+
plot: Visualizes the SPF on a 3D plot.
|
389
|
+
|
390
|
+
"""
|
391
|
+
|
392
|
+
def compute_components(self) -> None:
|
393
|
+
"""
|
394
|
+
Computes the Scattering Phase Function (SPF) based on the electric field components (E_phi and E_theta).
|
395
|
+
|
396
|
+
The SPF is calculated as the square root of the sum of the squared magnitudes of the electric field components, representing
|
397
|
+
the total scattering intensity distribution as a function of angles.
|
398
|
+
|
399
|
+
The result is stored as the SPF attribute of the instance.
|
400
|
+
"""
|
401
|
+
self.SPF = numpy.sqrt(numpy.abs(self.E_phi)**2 + numpy.abs(self.E_theta)**2)
|
402
|
+
|
403
|
+
def plot(
|
404
|
+
self,
|
405
|
+
unit_size: List[float] = (400, 400),
|
406
|
+
background_color: str = 'white',
|
407
|
+
show_edges: bool = False,
|
408
|
+
colormap: str = 'viridis',
|
409
|
+
opacity: float = 1.0,
|
410
|
+
set_surface: bool = True,
|
411
|
+
show_axis_label: bool = False) -> None:
|
412
|
+
"""
|
413
|
+
Visualizes the scattering phase function on a 3D plot.
|
414
|
+
|
415
|
+
This method creates a 3D visualization of the scattering phase function (SPF). It allows customization
|
416
|
+
of the plot's appearance, including the colormap, mesh opacity, and whether or not to display mesh edges
|
417
|
+
and axis labels.
|
418
|
+
|
419
|
+
Parameters
|
420
|
+
----------
|
421
|
+
unit_size : List[float]
|
422
|
+
The size of the plot window in pixels (width, height). Default is (400, 400).
|
423
|
+
background_color : str
|
424
|
+
The background color of the plot. Default is 'white'.
|
425
|
+
show_edges : bool
|
426
|
+
If True, displays the edges of the mesh. Default is False.
|
427
|
+
colormap : str
|
428
|
+
The colormap to use for scalar mapping. Default is 'viridis'.
|
429
|
+
opacity : float
|
430
|
+
The opacity of the mesh. Default is 1.0.
|
431
|
+
set_surface : bool
|
432
|
+
If True, the surface represents the scaled SPF; if False, a unit sphere is used. Default is True.
|
433
|
+
show_axis_label : bool
|
434
|
+
If True, shows the axis labels. Default is False.
|
435
|
+
"""
|
436
|
+
# Define the window size based on the unit size provided
|
437
|
+
window_size = (unit_size[1], unit_size[0]) # One subplot
|
438
|
+
|
439
|
+
# Create a PyVista plotting scene with the specified theme and window size
|
440
|
+
scene = pyvista.Plotter(theme=pyvista.themes.DocumentTheme(), window_size=window_size)
|
441
|
+
|
442
|
+
# Set the background color of the scene
|
443
|
+
scene.set_background(background_color)
|
444
|
+
|
445
|
+
# Add the 3D axis-aligned plot to the scene using the specified settings
|
446
|
+
mapping = self._add_to_3d_ax(
|
447
|
+
scene=scene,
|
448
|
+
colormap=colormap,
|
449
|
+
opacity=opacity,
|
450
|
+
show_edges=show_edges,
|
451
|
+
set_surface=set_surface
|
452
|
+
)
|
453
|
+
|
454
|
+
# Optionally add axis labels
|
455
|
+
scene.add_axes_at_origin(labels_off=not show_axis_label)
|
456
|
+
|
457
|
+
# Add a scalar bar to the scene to represent the scattering phase function
|
458
|
+
scene.add_scalar_bar(mapper=mapping.mapper, title='Scattering Phase Function')
|
459
|
+
|
460
|
+
# Display the scene
|
461
|
+
scene.show()
|
462
|
+
|
463
|
+
def _add_to_3d_ax(self, scene: pyvista.Plotter, set_surface: bool = False, show_edges: bool = False, colormap: str = 'viridis', opacity: float = 1.0) -> None:
|
464
|
+
"""
|
465
|
+
Adds a 3D surface plot to the provided PyVista scene based on the scattering phase function (SPF).
|
466
|
+
|
467
|
+
This method generates a 3D surface plot of the SPF using spherical coordinates, and adds it to the scene.
|
468
|
+
The surface can either represent the actual SPF or a normalized unit sphere, depending on the `set_surface` flag.
|
469
|
+
The appearance of the surface can be customized using various parameters.
|
470
|
+
|
471
|
+
Parameters
|
472
|
+
----------
|
473
|
+
scene : pyvista.Plotter
|
474
|
+
The PyVista plotting scene where the surface will be added.
|
475
|
+
set_surface : bool
|
476
|
+
If True, the surface will represent the scaled SPF; if False, a unit sphere is used. Default is True.
|
477
|
+
show_edges : bool
|
478
|
+
If True, edges of the mesh will be displayed. Default is False.
|
479
|
+
colormap : str
|
480
|
+
The colormap to use for visualizing the scalar field. Default is 'viridis'.
|
481
|
+
opacity : float
|
482
|
+
The opacity of the surface mesh. Default is 1.0.
|
483
|
+
"""
|
484
|
+
# Create mesh grids for phi and theta
|
485
|
+
phi_mesh, theta_mesh = numpy.meshgrid(self.phi, self.theta)
|
486
|
+
|
487
|
+
# Normalize the scattering phase function (SPF) for visualization
|
488
|
+
scalar = self.SPF / self.SPF.max() * 2
|
489
|
+
|
490
|
+
# Determine the coordinates based on whether the surface represents the SPF or a unit sphere
|
491
|
+
if set_surface:
|
492
|
+
x, y, z = spherical_to_cartesian(r=scalar, phi=phi_mesh, theta=theta_mesh)
|
493
|
+
else:
|
494
|
+
x, y, z = spherical_to_cartesian(r=numpy.ones(phi_mesh.shape) * 0.5, phi=phi_mesh, theta=theta_mesh)
|
495
|
+
|
496
|
+
# Create a structured grid from the calculated coordinates
|
497
|
+
mesh = pyvista.StructuredGrid(x, y, z)
|
498
|
+
|
499
|
+
# Add the surface mesh to the scene
|
500
|
+
mapping = scene.add_mesh(
|
501
|
+
mesh,
|
502
|
+
cmap=colormap,
|
503
|
+
scalars=scalar.flatten(order='F'),
|
504
|
+
opacity=opacity,
|
505
|
+
style='surface',
|
506
|
+
show_edges=show_edges,
|
507
|
+
show_scalar_bar=False
|
508
|
+
)
|
509
|
+
|
510
|
+
return mapping
|
511
|
+
|
512
|
+
|
513
|
+
@dataclass(config=config_dict, kw_only=True)
|
514
|
+
class S1S2(BaseRepresentation):
|
515
|
+
"""
|
516
|
+
Represents the S1 and S2 scattering functions, which are components of the scattering matrix.
|
517
|
+
|
518
|
+
Parameters
|
519
|
+
----------
|
520
|
+
scatterer : BaseScatterer
|
521
|
+
The scatterer object.
|
522
|
+
sampling : int
|
523
|
+
Number of points for evaluating the S1 and S2 functions.
|
524
|
+
distance : Quantity
|
525
|
+
Distance at which the fields are evaluated.
|
526
|
+
|
527
|
+
Methods:
|
528
|
+
compute_components: Computes the S1 and S2 functions based on the scatterer's properties.
|
529
|
+
plot: Visualizes the S1 and S2 functions on a polar plot.
|
530
|
+
"""
|
531
|
+
def __post_init__(self):
|
532
|
+
self.phi = numpy.linspace(-180, 180, self.sampling)
|
533
|
+
|
534
|
+
self.compute_components()
|
535
|
+
|
536
|
+
def compute_components(self) -> None:
|
537
|
+
"""
|
538
|
+
Computes the S1 and S2 scattering parameters based on the scatterer's properties and the scattering angle phi.
|
539
|
+
|
540
|
+
S1 and S2 are integral parts of the scattering matrix describing the change in polarization state of light upon scattering.
|
541
|
+
|
542
|
+
The method calculates these parameters for a range of phi angles and stores them as the S1 and S2 attributes of the instance.
|
543
|
+
"""
|
544
|
+
self.S1, self.S2 = self.scatterer._cpp_get_s1s2(phi=numpy.deg2rad(self.phi) + numpy.pi / 2)
|
545
|
+
|
546
|
+
def plot(self) -> None:
|
547
|
+
"""
|
548
|
+
Plots the S1 and S2 Stokes parameters on polar plots.
|
549
|
+
|
550
|
+
The method generates two polar plots: one for the absolute values of the S1 parameter and another
|
551
|
+
for the S2 parameter, filling the area between the radial axis and the parameter values.
|
552
|
+
|
553
|
+
Returns
|
554
|
+
-------
|
555
|
+
None
|
556
|
+
This method does not return a value. It displays the polar plots.
|
557
|
+
"""
|
558
|
+
with plt.style.context(MPSPlots.styles.mps):
|
559
|
+
figure, axes = plt.subplots(nrows=1, ncols=2, subplot_kw={'polar': True})
|
560
|
+
|
561
|
+
# Plot for S1 parameter
|
562
|
+
axes[0].set(title=r'S$_1$ parameter')
|
563
|
+
axes[0].fill_between(
|
564
|
+
numpy.deg2rad(self.phi),
|
565
|
+
y1=0,
|
566
|
+
y2=numpy.abs(self.S1),
|
567
|
+
color='C0',
|
568
|
+
edgecolor='black'
|
569
|
+
)
|
570
|
+
|
571
|
+
# Plot for S2 parameter
|
572
|
+
axes[1].set(title=r'S$_2$ parameter')
|
573
|
+
axes[1].fill_between(
|
574
|
+
numpy.deg2rad(self.phi),
|
575
|
+
y1=0,
|
576
|
+
y2=numpy.abs(self.S2),
|
577
|
+
color='C1',
|
578
|
+
edgecolor='black'
|
579
|
+
)
|
580
|
+
|
581
|
+
plt.show()
|
582
|
+
|
583
|
+
|
584
|
+
@dataclass(config=config_dict, kw_only=True)
|
585
|
+
class Footprint():
|
586
|
+
r"""
|
587
|
+
Represents the footprint of the scatterer as detected by various detectors.
|
588
|
+
|
589
|
+
.. math::
|
590
|
+
\text{Footprint} = \big| \mathscr{F}^{-1} \big\{ \tilde{ \psi }\
|
591
|
+
(\xi, \nu), \tilde{ \phi}_{l,m}(\xi, \nu) \big\} \
|
592
|
+
(\delta_x, \delta_y) \big|^2
|
593
|
+
|
594
|
+
Parameters
|
595
|
+
----------
|
596
|
+
detector : BaseDetector
|
597
|
+
The detector object.
|
598
|
+
scatterer : BaseScatterer
|
599
|
+
The scatterer object.
|
600
|
+
sampling : int
|
601
|
+
Number of points to evaluate the Stokes parameters in spherical coordinates (default is 500).
|
602
|
+
padding_factor : int
|
603
|
+
Padding factor for the Fourier transform (default is 20).
|
604
|
+
|
605
|
+
Methods:
|
606
|
+
compute_footprint: Computes the footprint based on the far-field patterns and detector characteristics.
|
607
|
+
plot: Visualizes the computed footprint.
|
608
|
+
"""
|
609
|
+
detector: object
|
610
|
+
scatterer: object
|
611
|
+
sampling: int = 200
|
612
|
+
padding_factor: int = 20
|
613
|
+
|
614
|
+
def __post_init__(self):
|
615
|
+
self.compute_footprint()
|
616
|
+
|
617
|
+
def compute_footprint(self):
|
618
|
+
"""
|
619
|
+
Computes the footprint of the scatterer as detected by the specified detector.
|
620
|
+
|
621
|
+
The footprint is calculated based on the far-field scattering patterns and the characteristics of the detector,
|
622
|
+
using a Fourier transform to project the far-field onto the detector plane.
|
623
|
+
|
624
|
+
The computed footprint and the corresponding spatial coordinates are stored as attributes of the instance.
|
625
|
+
|
626
|
+
Warning: this function do not currently take account of the cache block on the detector.
|
627
|
+
"""
|
628
|
+
max_angle = self.detector.max_angle
|
629
|
+
n_point = complex(self.sampling)
|
630
|
+
|
631
|
+
phi, theta = numpy.mgrid[
|
632
|
+
-max_angle.to('radian').magnitude: max_angle.to('radian').magnitude: n_point, 0: numpy.pi: n_point
|
633
|
+
]
|
634
|
+
|
635
|
+
max_distance_direct_space = 1 / (numpy.sin(max_angle) * self.scatterer.source.wavenumber / (2 * numpy.pi))
|
636
|
+
|
637
|
+
x = y = numpy.linspace(-1, 1, self.sampling) * self.sampling / 2 * max_distance_direct_space / self.padding_factor
|
638
|
+
|
639
|
+
_, phi, theta = rotate_on_x(phi + numpy.pi / 2, theta, numpy.pi / 2)
|
640
|
+
|
641
|
+
far_field_para, far_field_perp = self.scatterer.get_farfields_array(
|
642
|
+
phi=phi.ravel() + numpy.pi / 2,
|
643
|
+
theta=theta.ravel(),
|
644
|
+
r=1.0 * meter,
|
645
|
+
)
|
646
|
+
|
647
|
+
detector_structured_farfield = self.detector.get_structured_scalarfield(sampling=self.sampling)
|
648
|
+
|
649
|
+
perpendicular_projection = detector_structured_farfield * far_field_perp.reshape(theta.shape)
|
650
|
+
|
651
|
+
parallel_projection = detector_structured_farfield * far_field_para.reshape(theta.shape)
|
652
|
+
|
653
|
+
fourier_parallel = self.get_fourier_component(parallel_projection)
|
654
|
+
fourier_perpendicular = self.get_fourier_component(perpendicular_projection)
|
655
|
+
|
656
|
+
self.mapping = (fourier_parallel + fourier_perpendicular)
|
657
|
+
self.direct_x = x
|
658
|
+
self.direct_y = y
|
659
|
+
|
660
|
+
def get_fourier_component(self, scalar: numpy.ndarray) -> numpy.ndarray:
|
661
|
+
"""
|
662
|
+
Computes the Fourier component of a given scalar field.
|
663
|
+
|
664
|
+
This method performs a two-dimensional inverse Fourier transform on the input scalar field, which represents
|
665
|
+
a projection (either parallel or perpendicular) of the far-field pattern. It then extracts a central portion
|
666
|
+
of the result, effectively applying a padding factor to increase the resolution of the Fourier transform.
|
667
|
+
|
668
|
+
Parameters
|
669
|
+
----------
|
670
|
+
- scalar : numpy.ndarray
|
671
|
+
A two-dimensional numpy array representing the scalar field of which the Fourier component
|
672
|
+
is to be computed. This field could represent either the parallel or perpendicular projection of the far-field
|
673
|
+
pattern onto the detector plane.
|
674
|
+
|
675
|
+
Returns
|
676
|
+
-------
|
677
|
+
numpy.ndarray
|
678
|
+
A two-dimensional numpy array representing the computed Fourier component. This array is a square
|
679
|
+
section, extracted from the center of the full Fourier transform, with dimensions determined by the original
|
680
|
+
sampling rate and the padding factor of the instance. The values in the array represent the intensity distribution
|
681
|
+
of the light in the detector plane, providing insights into the spatial characteristics of the scattering pattern.
|
682
|
+
|
683
|
+
The method uses numpy's fft module to perform the Fourier transform, applying a padding factor to the input to
|
684
|
+
achieve a higher resolution in the Fourier domain. The resulting Fourier transform is then squared and fftshifted
|
685
|
+
to center the zero-frequency component, and a central portion is extracted to match the intended output size.
|
686
|
+
"""
|
687
|
+
# Calculate the target size based on the sampling and padding factor, and the indices for the central portion extraction.
|
688
|
+
total_size = self.sampling * self.padding_factor
|
689
|
+
offset = (total_size - self.sampling) // 2
|
690
|
+
|
691
|
+
# Apply zero-padding to the scalar field to increase the resolution of the Fourier transform.
|
692
|
+
padded_scalar = numpy.pad(scalar, pad_width=((offset, offset), (offset, offset)), mode='constant', constant_values=0)
|
693
|
+
|
694
|
+
# Perform the two-dimensional inverse Fourier transform on the padded scalar field.
|
695
|
+
fourier_transformed = numpy.fft.ifft2(padded_scalar)
|
696
|
+
|
697
|
+
# Compute the squared magnitude and center the zero-frequency component.
|
698
|
+
fourier_magnitude_squared = numpy.abs(numpy.fft.fftshift(fourier_transformed))**2
|
699
|
+
|
700
|
+
# Extract the central portion corresponding to the original sampling rate adjusted by the padding factor.
|
701
|
+
central_portion = fourier_magnitude_squared[offset:-offset, offset:-offset]
|
702
|
+
|
703
|
+
return central_portion
|
704
|
+
|
705
|
+
def plot(self, colormap: str = 'gray') -> None:
|
706
|
+
"""
|
707
|
+
Plots the scatterer footprint using a 2D colormap.
|
708
|
+
|
709
|
+
The method generates a plot representing the footprint of the scatterer, with the X and Y axes showing
|
710
|
+
offset distances in micrometers, and the colormap depicting the mapping values.
|
711
|
+
|
712
|
+
Parameters
|
713
|
+
----------
|
714
|
+
colormap : str
|
715
|
+
The colormap to use for the plot. Default is 'gray'.
|
716
|
+
|
717
|
+
"""
|
718
|
+
with plt.style.context(MPSPlots.styles.mps):
|
719
|
+
figure, ax = plt.subplots()
|
720
|
+
|
721
|
+
ax.set(
|
722
|
+
title='Scatterer Footprint',
|
723
|
+
xlabel=r'Offset distance in X-axis [$\mu$m]',
|
724
|
+
ylabel=r'Offset distance in Y-axis [$\mu$m]',
|
725
|
+
)
|
726
|
+
|
727
|
+
ax.pcolormesh(
|
728
|
+
self.direct_y,
|
729
|
+
self.direct_x,
|
730
|
+
self.mapping,
|
731
|
+
cmap=colormap
|
732
|
+
)
|
733
|
+
|
734
|
+
plt.show()
|