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.
Files changed (101) hide show
  1. PyMieSim/__init__.py +16 -0
  2. PyMieSim/__main__.py +9 -0
  3. PyMieSim/_version.py +21 -0
  4. PyMieSim/binary/__init__.py +0 -0
  5. PyMieSim/binary/interface_detector.cp310-win_amd64.pyd +0 -0
  6. PyMieSim/binary/interface_detector.cp311-win_amd64.pyd +0 -0
  7. PyMieSim/binary/interface_detector.cp312-win_amd64.pyd +0 -0
  8. PyMieSim/binary/interface_detector.cp313-win_amd64.pyd +0 -0
  9. PyMieSim/binary/interface_experiment.cp310-win_amd64.pyd +0 -0
  10. PyMieSim/binary/interface_experiment.cp311-win_amd64.pyd +0 -0
  11. PyMieSim/binary/interface_experiment.cp312-win_amd64.pyd +0 -0
  12. PyMieSim/binary/interface_experiment.cp313-win_amd64.pyd +0 -0
  13. PyMieSim/binary/interface_scatterer.cp310-win_amd64.pyd +0 -0
  14. PyMieSim/binary/interface_scatterer.cp311-win_amd64.pyd +0 -0
  15. PyMieSim/binary/interface_scatterer.cp312-win_amd64.pyd +0 -0
  16. PyMieSim/binary/interface_scatterer.cp313-win_amd64.pyd +0 -0
  17. PyMieSim/binary/interface_sets.cp310-win_amd64.pyd +0 -0
  18. PyMieSim/binary/interface_sets.cp311-win_amd64.pyd +0 -0
  19. PyMieSim/binary/interface_sets.cp312-win_amd64.pyd +0 -0
  20. PyMieSim/binary/interface_sets.cp313-win_amd64.pyd +0 -0
  21. PyMieSim/binary/interface_source.cp310-win_amd64.pyd +0 -0
  22. PyMieSim/binary/interface_source.cp311-win_amd64.pyd +0 -0
  23. PyMieSim/binary/interface_source.cp312-win_amd64.pyd +0 -0
  24. PyMieSim/binary/interface_source.cp313-win_amd64.pyd +0 -0
  25. PyMieSim/binary/libcpp_coordinates.a +0 -0
  26. PyMieSim/binary/libcpp_detector.a +0 -0
  27. PyMieSim/binary/libcpp_experiment.a +0 -0
  28. PyMieSim/binary/libcpp_fibonacci.a +0 -0
  29. PyMieSim/binary/libcpp_mode_field.a +0 -0
  30. PyMieSim/binary/libcpp_sets.a +0 -0
  31. PyMieSim/binary/libcpp_source.a +0 -0
  32. PyMieSim/directories.py +31 -0
  33. PyMieSim/experiment/__init__.py +1 -0
  34. PyMieSim/experiment/dataframe_subclass.py +220 -0
  35. PyMieSim/experiment/detector/__init__.py +2 -0
  36. PyMieSim/experiment/detector/base.py +169 -0
  37. PyMieSim/experiment/detector/coherent_mode.py +50 -0
  38. PyMieSim/experiment/detector/photodiode.py +52 -0
  39. PyMieSim/experiment/scatterer/__init__.py +4 -0
  40. PyMieSim/experiment/scatterer/base.py +98 -0
  41. PyMieSim/experiment/scatterer/core_shell.py +82 -0
  42. PyMieSim/experiment/scatterer/cylinder.py +63 -0
  43. PyMieSim/experiment/scatterer/sphere.py +66 -0
  44. PyMieSim/experiment/setup.py +356 -0
  45. PyMieSim/experiment/source/__init__.py +2 -0
  46. PyMieSim/experiment/source/base.py +85 -0
  47. PyMieSim/experiment/source/gaussian.py +60 -0
  48. PyMieSim/experiment/source/planewave.py +69 -0
  49. PyMieSim/experiment/utils.py +132 -0
  50. PyMieSim/gui/__init__.py +0 -0
  51. PyMieSim/gui/helper.py +60 -0
  52. PyMieSim/gui/interface.py +136 -0
  53. PyMieSim/gui/section.py +606 -0
  54. PyMieSim/mesh.py +368 -0
  55. PyMieSim/polarization.py +174 -0
  56. PyMieSim/single/__init__.py +48 -0
  57. PyMieSim/single/detector/__init__.py +2 -0
  58. PyMieSim/single/detector/base.py +271 -0
  59. PyMieSim/single/detector/coherent.py +99 -0
  60. PyMieSim/single/detector/uncoherent.py +105 -0
  61. PyMieSim/single/representations.py +734 -0
  62. PyMieSim/single/scatterer/__init__.py +4 -0
  63. PyMieSim/single/scatterer/base.py +405 -0
  64. PyMieSim/single/scatterer/core_shell.py +126 -0
  65. PyMieSim/single/scatterer/cylinder.py +113 -0
  66. PyMieSim/single/scatterer/sphere.py +108 -0
  67. PyMieSim/single/source/__init__.py +3 -0
  68. PyMieSim/single/source/base.py +7 -0
  69. PyMieSim/single/source/gaussian.py +137 -0
  70. PyMieSim/single/source/planewave.py +97 -0
  71. PyMieSim/special_functions.py +81 -0
  72. PyMieSim/units.py +130 -0
  73. PyMieSim/validation_data/bohren_huffman/figure_810.csv +245 -0
  74. PyMieSim/validation_data/bohren_huffman/figure_87.csv +2 -0
  75. PyMieSim/validation_data/bohren_huffman/figure_88.csv +2 -0
  76. PyMieSim/validation_data/pymiescatt/example_coreshell_0.csv +41 -0
  77. PyMieSim/validation_data/pymiescatt/example_coreshell_1.csv +401 -0
  78. PyMieSim/validation_data/pymiescatt/example_shpere_0.csv +51 -0
  79. PyMieSim/validation_data/pymiescatt/example_shpere_1.csv +801 -0
  80. PyMieSim/validation_data/pymiescatt/example_shpere_2.csv +41 -0
  81. PyMieSim/validation_data/pymiescatt/example_shpere_3.csv +401 -0
  82. PyMieSim/validation_data/pymiescatt/example_sphere_0.csv +51 -0
  83. PyMieSim/validation_data/pymiescatt/example_sphere_1.csv +801 -0
  84. PyMieSim/validation_data/pymiescatt/example_sphere_2.csv +41 -0
  85. PyMieSim/validation_data/pymiescatt/example_sphere_3.csv +401 -0
  86. PyMieSim/validation_data/pymiescatt/validation_Qsca.csv +800 -0
  87. PyMieSim/validation_data/pymiescatt/validation_Qsca_coreshell_1.csv +400 -0
  88. PyMieSim/validation_data/pymiescatt/validation_Qsca_coreshell_2.csv +400 -0
  89. PyMieSim/validation_data/pymiescatt/validation_Qsca_medium.csv +800 -0
  90. PyMieSim/validation_data/pymiescatt/validation_coreshell.csv +81 -0
  91. PyMieSim/validation_data/pymiescatt/validation_sphere.csv +801 -0
  92. lib/libZBessel.a +0 -0
  93. lib/lib_ZBessel.a +0 -0
  94. lib/libcpp_base_scatterer.a +0 -0
  95. lib/libcpp_coreshell.a +0 -0
  96. lib/libcpp_cylinder.a +0 -0
  97. lib/libcpp_sphere.a +0 -0
  98. pymiesim-3.6.0.dist-info/METADATA +246 -0
  99. pymiesim-3.6.0.dist-info/RECORD +101 -0
  100. pymiesim-3.6.0.dist-info/WHEEL +5 -0
  101. 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()