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,98 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
|
4
|
+
from PyOptik.material.base_class import BaseMaterial
|
5
|
+
from pydantic import field_validator
|
6
|
+
from PyMieSim.units import Quantity, meter, RIU
|
7
|
+
from pint_pandas import PintArray
|
8
|
+
from PyMieSim.binary.interface_sets import CppScattererProperties, CppMediumProperties
|
9
|
+
import numpy
|
10
|
+
|
11
|
+
|
12
|
+
class BaseScatterer():
|
13
|
+
"""
|
14
|
+
Base class for scatterer objects. This class handles the initialization and setup of
|
15
|
+
scatterer parameters for use in PyMieSim simulations.
|
16
|
+
|
17
|
+
"""
|
18
|
+
mapping = None
|
19
|
+
binding_kwargs = None
|
20
|
+
binding = None
|
21
|
+
|
22
|
+
@field_validator('property', 'core_property', 'shell_property', 'medium_property', mode='plain')
|
23
|
+
def _validate_array(cls, value):
|
24
|
+
"""Ensure that arrays are properly converted to numpy arrays."""
|
25
|
+
value = numpy.atleast_1d(value)
|
26
|
+
|
27
|
+
if isinstance(value, Quantity):
|
28
|
+
assert value.check(RIU), f"{value} must be a Quantity with refractive index units (RIU)."
|
29
|
+
return value
|
30
|
+
|
31
|
+
assert numpy.all([isinstance(p, BaseMaterial) for p in value]), f"{value} must be either a refractive index quantity (RIU) or BaseMaterial."
|
32
|
+
|
33
|
+
return value
|
34
|
+
|
35
|
+
@field_validator('diameter', 'core_diameter', 'shell_thickness', mode='plain')
|
36
|
+
def _validate_length_quantity(cls, value):
|
37
|
+
"""
|
38
|
+
Ensures that diameter is Quantity objects with length units.
|
39
|
+
"""
|
40
|
+
value = numpy.atleast_1d(value)
|
41
|
+
|
42
|
+
if not isinstance(value, Quantity) or not value.check(meter):
|
43
|
+
raise ValueError(f"{value} must be a Quantity with meters units.")
|
44
|
+
|
45
|
+
return value
|
46
|
+
|
47
|
+
def _add_properties(self, name: str, properties: Quantity | BaseMaterial) -> None:
|
48
|
+
"""
|
49
|
+
Determines whether the provided property is a refractive index (Quantity) or a material (BaseMaterial),
|
50
|
+
and returns the corresponding values.
|
51
|
+
|
52
|
+
Parameters
|
53
|
+
----------
|
54
|
+
property : Quantity or BaseMaterial
|
55
|
+
The core property to be assigned, which can either be a refractive index (Quantity) or a material (BaseMaterial).
|
56
|
+
|
57
|
+
Raises
|
58
|
+
------
|
59
|
+
ValueError:
|
60
|
+
If the provided property is neither a Quantity (refractive index) nor a BaseMaterial.
|
61
|
+
"""
|
62
|
+
CPPClass = CppMediumProperties if name == 'medium' else CppScattererProperties
|
63
|
+
|
64
|
+
if all(isinstance(item, Quantity) for item in properties):
|
65
|
+
instance = CPPClass(index_properties=properties.magnitude)
|
66
|
+
self.binding_kwargs[f'{name}_properties'] = instance
|
67
|
+
|
68
|
+
return instance
|
69
|
+
|
70
|
+
elif all(isinstance(item, BaseMaterial) for item in properties):
|
71
|
+
eval_index = numpy.asarray([m.compute_refractive_index(self.source.wavelength.to_base_units().magnitude) for m in properties])
|
72
|
+
instance = CPPClass(material_properties=eval_index)
|
73
|
+
self.binding_kwargs[f'{name}_properties'] = instance
|
74
|
+
|
75
|
+
return instance
|
76
|
+
|
77
|
+
else:
|
78
|
+
raise TypeError("All elements in the list must be of type 'Quantity' or 'BaseMaterial', not a mix of both.")
|
79
|
+
|
80
|
+
def _generate_mapping(self) -> None:
|
81
|
+
"""
|
82
|
+
Constructs a table of the scatterer's properties formatted for data visualization.
|
83
|
+
This method populates the `mapping` dictionary with user-friendly descriptions and formats of the scatterer properties.
|
84
|
+
|
85
|
+
Returns
|
86
|
+
-------
|
87
|
+
list
|
88
|
+
A list of visual representations for each property in the `mapping` dictionary that has been populated.
|
89
|
+
"""
|
90
|
+
for attr in self.attributes:
|
91
|
+
values = getattr(self, attr)
|
92
|
+
if values is None:
|
93
|
+
continue
|
94
|
+
|
95
|
+
if hasattr(values, 'magnitude'):
|
96
|
+
self.mapping["scatterer:" + attr] = PintArray(values.magnitude, dtype=values.units)
|
97
|
+
else:
|
98
|
+
self.mapping["scatterer:" + attr] = [repr(m) for m in values]
|
@@ -0,0 +1,82 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
|
4
|
+
from pydantic.dataclasses import dataclass
|
5
|
+
from typing import List
|
6
|
+
from PyMieSim.binary.interface_sets import CppCoreShellSet
|
7
|
+
from PyOptik.material.base_class import BaseMaterial
|
8
|
+
from PyMieSim.units import Quantity
|
9
|
+
from PyMieSim.experiment.scatterer.base import BaseScatterer
|
10
|
+
from PyMieSim.experiment.source.base import BaseSource
|
11
|
+
from PyMieSim.experiment.utils import config_dict, Sequential
|
12
|
+
|
13
|
+
|
14
|
+
@dataclass(config=config_dict, kw_only=True)
|
15
|
+
class CoreShell(BaseScatterer, Sequential):
|
16
|
+
"""
|
17
|
+
A data class representing a core-shell scatterer configuration used in PyMieSim simulations.
|
18
|
+
|
19
|
+
This class facilitates the setup and manipulation of core-shell scatterers by providing structured
|
20
|
+
attributes and methods that ensure the scatterers are configured correctly for simulations.
|
21
|
+
It extends the BaseScatterer class, adding specific attributes and methods relevant to core-shell geometries.
|
22
|
+
|
23
|
+
Parameters
|
24
|
+
----------
|
25
|
+
source : PyMieSim.experiment.source.base.BaseSource
|
26
|
+
Light source configuration for the simulation.
|
27
|
+
core_diameter : Quantity
|
28
|
+
Diameters of the core components.
|
29
|
+
shell_thickness : Quantity
|
30
|
+
Thicknesses of the shell components.
|
31
|
+
core_property : List[BaseMaterial] | List[Quantity]
|
32
|
+
Refractive index or indices of the core.
|
33
|
+
shell_property : List[BaseMaterial] | List[Quantity]
|
34
|
+
Refractive index or indices of the shell.
|
35
|
+
medium_property : List[BaseMaterial] | List[Quantity]
|
36
|
+
BaseMaterial(s) defining the medium, used if `medium_index` is not provided.
|
37
|
+
|
38
|
+
"""
|
39
|
+
source: BaseSource
|
40
|
+
core_diameter: Quantity
|
41
|
+
shell_thickness: Quantity
|
42
|
+
core_property: List[BaseMaterial] | List[Quantity]
|
43
|
+
shell_property: List[BaseMaterial] | List[Quantity]
|
44
|
+
medium_property: List[BaseMaterial] | List[Quantity]
|
45
|
+
|
46
|
+
available_measure_list = [
|
47
|
+
'Qsca', 'Qext', 'Qabs', 'Qratio', 'Qforward', 'Qback', 'Qpr',
|
48
|
+
'Csca', 'Cext', 'Cabs', 'Cratio', 'Cforward', 'Cback', 'Cpr', 'a1',
|
49
|
+
'a2', 'a3', 'b1', 'b2', 'b3', 'g', 'coupling',
|
50
|
+
]
|
51
|
+
|
52
|
+
attributes = ['core_diameter', 'shell_thickness', 'core_property', 'shell_property', 'medium_property']
|
53
|
+
|
54
|
+
def _generate_binding(self) -> None:
|
55
|
+
"""
|
56
|
+
Assembles the keyword arguments necessary for C++ binding, tailored for core-shell scatterers.
|
57
|
+
Prepares structured data from scatterer properties for efficient simulation integration.
|
58
|
+
|
59
|
+
This function populates `binding_kwargs` with values formatted appropriately for the C++ backend used in simulations.
|
60
|
+
"""
|
61
|
+
self.mapping = {}
|
62
|
+
|
63
|
+
self.binding_kwargs = dict(
|
64
|
+
core_diameter=self.core_diameter,
|
65
|
+
shell_thickness=self.shell_thickness,
|
66
|
+
is_sequential=self.is_sequential
|
67
|
+
)
|
68
|
+
|
69
|
+
mediun_properties = self._add_properties(name='medium', properties=self.medium_property)
|
70
|
+
|
71
|
+
core_properties = self._add_properties(name='core', properties=self.core_property)
|
72
|
+
|
73
|
+
shell_properties = self._add_properties(name='shell', properties=self.shell_property)
|
74
|
+
|
75
|
+
self.binding = CppCoreShellSet(
|
76
|
+
core_diameter=self.core_diameter.to('meter').magnitude,
|
77
|
+
shell_thickness=self.shell_thickness.to('meter').magnitude,
|
78
|
+
core_properties=core_properties,
|
79
|
+
shell_properties=shell_properties,
|
80
|
+
medium_properties=mediun_properties,
|
81
|
+
is_sequential=self.is_sequential,
|
82
|
+
)
|
@@ -0,0 +1,63 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
|
4
|
+
from typing import List
|
5
|
+
from PyMieSim.units import Quantity
|
6
|
+
from pydantic.dataclasses import dataclass
|
7
|
+
from PyMieSim.binary.interface_sets import CppCylinderSet
|
8
|
+
from PyOptik.material.base_class import BaseMaterial
|
9
|
+
from PyMieSim.experiment.scatterer.base import BaseScatterer
|
10
|
+
from PyMieSim.experiment.source.base import BaseSource
|
11
|
+
from PyMieSim.experiment.utils import config_dict, Sequential
|
12
|
+
|
13
|
+
|
14
|
+
@dataclass(config=config_dict, kw_only=True)
|
15
|
+
class Cylinder(BaseScatterer, Sequential):
|
16
|
+
"""
|
17
|
+
Represents a cylindrical scatterer configuration for PyMieSim simulations.
|
18
|
+
|
19
|
+
Parameters
|
20
|
+
----------
|
21
|
+
source : PyMieSim.experiment.source.base.BaseSource
|
22
|
+
Light source configuration for the simulation.
|
23
|
+
diameter : Quantity
|
24
|
+
Diameter(s) of the cylinder in meters.
|
25
|
+
property : List[BaseMaterial] | List[Quantity]
|
26
|
+
Refractive index or indices of the spherical scatterers themselves.
|
27
|
+
medium_property : List[BaseMaterial] | List[Quantity]
|
28
|
+
BaseMaterial(s) defining the medium, used if `medium_index` is not provided.
|
29
|
+
"""
|
30
|
+
source: BaseSource
|
31
|
+
diameter: Quantity
|
32
|
+
property: List[BaseMaterial] | List[Quantity]
|
33
|
+
medium_property: List[BaseMaterial] | List[Quantity]
|
34
|
+
|
35
|
+
available_measure_list = [
|
36
|
+
'Qsca', 'Qext', 'Qabs', 'Csca', 'Cext', 'Cabs', 'a11',
|
37
|
+
'a21', 'a12', 'a22', 'a13', 'a23', 'b11', 'b21', 'b12',
|
38
|
+
'b22', 'b13', 'b23', 'coupling',
|
39
|
+
]
|
40
|
+
|
41
|
+
attributes = ['diameter', 'property', 'medium_property']
|
42
|
+
|
43
|
+
def _generate_binding(self) -> None:
|
44
|
+
"""
|
45
|
+
Constructs the keyword arguments necessary for the C++ binding interface, specifically tailored for spherical scatterers.
|
46
|
+
This includes processing material indices and organizing them into a structured dictionary for simulation interaction.
|
47
|
+
|
48
|
+
This method automatically configures the `binding_kwargs` attribute with appropriately formatted values.
|
49
|
+
"""
|
50
|
+
self.mapping = {}
|
51
|
+
|
52
|
+
self.binding_kwargs = dict(diameter=self.diameter, is_sequential=self.is_sequential)
|
53
|
+
|
54
|
+
mediun_properties = self._add_properties(name='medium', properties=self.medium_property)
|
55
|
+
|
56
|
+
scatterer_properties = self._add_properties(name='scatterer', properties=self.property)
|
57
|
+
|
58
|
+
self.binding = CppCylinderSet(
|
59
|
+
diameter=self.diameter.to('meter').magnitude,
|
60
|
+
scatterer_properties=scatterer_properties,
|
61
|
+
medium_properties=mediun_properties,
|
62
|
+
is_sequential=self.is_sequential,
|
63
|
+
)
|
@@ -0,0 +1,66 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
from typing import List
|
4
|
+
from pydantic.dataclasses import dataclass
|
5
|
+
from PyMieSim.binary.interface_sets import CppSphereSet
|
6
|
+
from PyOptik.material.base_class import BaseMaterial
|
7
|
+
from PyMieSim.units import Quantity
|
8
|
+
from PyMieSim.experiment.scatterer.base import BaseScatterer
|
9
|
+
from PyMieSim.experiment.source.base import BaseSource
|
10
|
+
from PyMieSim.experiment.utils import config_dict, Sequential
|
11
|
+
|
12
|
+
|
13
|
+
@dataclass(config=config_dict, kw_only=True)
|
14
|
+
class Sphere(BaseScatterer, Sequential):
|
15
|
+
"""
|
16
|
+
A data class that represents a spherical scatterer configuration used in PyMieSim simulations.
|
17
|
+
|
18
|
+
This class provides specific implementations for setting up and binding spherical scatterers
|
19
|
+
with their properties to a simulation environment. It extends the `BaseScatterer` class by
|
20
|
+
adding spherical-specific attributes and methods for handling simulation setups.
|
21
|
+
|
22
|
+
Parameters
|
23
|
+
----------
|
24
|
+
source : PyMieSim.experiment.source.base.BaseSource
|
25
|
+
Light source configuration for the simulation.
|
26
|
+
diameter : Quantity
|
27
|
+
Diameter(s) of the spherical scatterers in meters.
|
28
|
+
property : List[BaseMaterial] | List[Quantity]
|
29
|
+
Refractive index or indices of the spherical scatterers themselves.
|
30
|
+
medium_property : List, optional
|
31
|
+
BaseMaterial(s) defining the medium, used if `medium_index` is not provided.
|
32
|
+
"""
|
33
|
+
source: BaseSource
|
34
|
+
diameter: Quantity
|
35
|
+
property: List[BaseMaterial] | List[Quantity]
|
36
|
+
medium_property: List[BaseMaterial] | List[Quantity]
|
37
|
+
|
38
|
+
available_measure_list = [
|
39
|
+
'Qsca', 'Qext', 'Qabs', 'Qratio', 'Qforward', 'Qback', 'Qpr',
|
40
|
+
'Csca', 'Cext', 'Cabs', 'Cratio', 'Cforward', 'Cback', 'Cpr', 'a1',
|
41
|
+
'a2', 'a3', 'b1', 'b2', 'b3', 'g', 'coupling',
|
42
|
+
]
|
43
|
+
|
44
|
+
attributes = ['diameter', 'property', 'medium_property']
|
45
|
+
|
46
|
+
def _generate_binding(self):
|
47
|
+
"""
|
48
|
+
Constructs the keyword arguments necessary for the C++ binding interface, specifically tailored for spherical scatterers.
|
49
|
+
This includes processing material indices and organizing them into a structured dictionary for simulation interaction.
|
50
|
+
|
51
|
+
This method automatically configures the `binding_kwargs` attribute with appropriately formatted values.
|
52
|
+
"""
|
53
|
+
self.mapping = {}
|
54
|
+
|
55
|
+
self.binding_kwargs = dict(diameter=self.diameter, is_sequential=self.is_sequential)
|
56
|
+
|
57
|
+
mediun_properties = self._add_properties(name='medium', properties=self.medium_property)
|
58
|
+
|
59
|
+
scatterer_properties = self._add_properties(name='scatterer', properties=self.property)
|
60
|
+
|
61
|
+
self.binding = CppSphereSet(
|
62
|
+
diameter=self.diameter.to('meter').magnitude,
|
63
|
+
scatterer_properties=scatterer_properties,
|
64
|
+
medium_properties=mediun_properties,
|
65
|
+
is_sequential=self.is_sequential,
|
66
|
+
)
|
@@ -0,0 +1,356 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
|
4
|
+
import numpy
|
5
|
+
import pandas as pd
|
6
|
+
from pydantic.dataclasses import dataclass
|
7
|
+
from PyMieSim.binary.interface_experiment import EXPERIMENT
|
8
|
+
from PyMieSim.units import AU, meter, watt
|
9
|
+
from typing import Union, Optional, List
|
10
|
+
from PyMieSim.experiment.scatterer import Sphere, Cylinder, CoreShell
|
11
|
+
from PyMieSim.experiment.detector import Photodiode, CoherentMode
|
12
|
+
from PyMieSim.experiment.source import Gaussian, PlaneWave
|
13
|
+
from PyMieSim.experiment.dataframe_subclass import PyMieSimDataFrame
|
14
|
+
import PyMieSim
|
15
|
+
from PyMieSim.binary.interface_sets import CppDetectorSet
|
16
|
+
|
17
|
+
|
18
|
+
class EmptyDetector:
|
19
|
+
def __init__(self):
|
20
|
+
self.binding = CppDetectorSet()
|
21
|
+
self.mapping = {}
|
22
|
+
|
23
|
+
def _generate_binding(self, *args, **kwargs):
|
24
|
+
pass
|
25
|
+
|
26
|
+
def _generate_mapping(self, *args, **kwargs):
|
27
|
+
pass
|
28
|
+
|
29
|
+
@dataclass
|
30
|
+
class Setup:
|
31
|
+
"""
|
32
|
+
Orchestrates the setup and execution of light scattering experiments using PyMieSim.
|
33
|
+
|
34
|
+
Attributes:
|
35
|
+
scatterer (Union[Sphere, Cylinder, CoreShell]): Configuration for the scatterer in the experiment.
|
36
|
+
Defines the physical properties of the particle being studied.
|
37
|
+
source (Union[Gaussian, PlaneWave]): Configuration for the light source. Specifies the characteristics
|
38
|
+
of the light (e.g., wavelength, polarization) illuminating the scatterer.
|
39
|
+
detector (Union[Photodiode, CoherentMode, None], optional): Configuration for the detector, if any. Details the
|
40
|
+
method of detection for scattered light, including positional and analytical parameters. Defaults to None.
|
41
|
+
|
42
|
+
Methods provide functionality for initializing bindings, generating parameter tables for visualization,
|
43
|
+
and executing the simulation to compute and retrieve specified measures.
|
44
|
+
"""
|
45
|
+
scatterer: Union[Sphere, Cylinder, CoreShell]
|
46
|
+
source: Union[Gaussian, PlaneWave]
|
47
|
+
detector: Optional[Union[Photodiode, CoherentMode]] = EmptyDetector()
|
48
|
+
|
49
|
+
def __post_init__(self):
|
50
|
+
"""
|
51
|
+
Initializes the experiment by setting the source for the scatterer and establishing bindings
|
52
|
+
between the components and the simulation environment.
|
53
|
+
"""
|
54
|
+
self.scatterer._generate_binding()
|
55
|
+
|
56
|
+
self.source._generate_binding()
|
57
|
+
|
58
|
+
self.detector._generate_binding()
|
59
|
+
|
60
|
+
self.scatterer.source = self.source
|
61
|
+
|
62
|
+
self.binding = EXPERIMENT(debug_mode=PyMieSim.debug_mode)
|
63
|
+
|
64
|
+
def _generate_mapping(self) -> None:
|
65
|
+
self.source._generate_mapping()
|
66
|
+
self.scatterer._generate_mapping()
|
67
|
+
self.detector._generate_mapping()
|
68
|
+
|
69
|
+
def get_sequential(self, measure: str) -> numpy.ndarray:
|
70
|
+
"""
|
71
|
+
Executes the simulation to compute and retrieve the specified measures in a sequential manner contrary to the standard get function.
|
72
|
+
This means that ranges are not iterated in a structured fashion into a dataframe but are only run once.
|
73
|
+
This methods was developed for specific aims, for the best suited vizualization tools go for the .get() method.
|
74
|
+
|
75
|
+
Parameters
|
76
|
+
----------
|
77
|
+
measures : str
|
78
|
+
The measure to be computed in the simulation, provided as arguments by the user.
|
79
|
+
Returns
|
80
|
+
-------
|
81
|
+
numpy.ndarray
|
82
|
+
An array containing the computed measures.
|
83
|
+
"""
|
84
|
+
method_name = f'get_{measure}_sequential'
|
85
|
+
|
86
|
+
# Compute the values using the binding method
|
87
|
+
return getattr(self.binding, method_name)(
|
88
|
+
scatterer_set=self.scatterer.binding,
|
89
|
+
source_set=self.source.binding,
|
90
|
+
detector_set=self.detector.binding
|
91
|
+
)
|
92
|
+
|
93
|
+
def get(self, *measures, scale_unit: bool = False, drop_unique_level: bool = True, add_units: bool = True, as_numpy: bool = False) -> pd.DataFrame:
|
94
|
+
"""
|
95
|
+
Run the simulation to compute specified measures and return the results.
|
96
|
+
|
97
|
+
Parameters
|
98
|
+
----------
|
99
|
+
measures : tuple
|
100
|
+
Variable-length tuple of measure names to compute, specified by the user.
|
101
|
+
scale_unit : bool, optional
|
102
|
+
If True, scales the units in the output DataFrame to the most appropriate units. Defaults to False.
|
103
|
+
drop_unique_level : bool, optional
|
104
|
+
If True, removes levels from the DataFrame's MultiIndex where only one unique value exists, simplifying the DataFrame. Defaults to True.
|
105
|
+
add_units : bool, optional
|
106
|
+
If True, includes units in the output DataFrame using pint quantities. If False, the output will contain raw numeric values only. Defaults to True.
|
107
|
+
as_numpy : bool, optional
|
108
|
+
If True, returns the result as a NumPy array instead of a DataFrame. Defaults to False.
|
109
|
+
|
110
|
+
Returns
|
111
|
+
-------
|
112
|
+
pd.DataFrame or np.ndarray
|
113
|
+
A DataFrame containing the computed measures with optional units and simplified MultiIndex, or a NumPy array if `as_numpy` is set to True.
|
114
|
+
"""
|
115
|
+
measures = set(numpy.atleast_1d(measures))
|
116
|
+
|
117
|
+
if 'coupling' in measures:
|
118
|
+
assert self.detector is not None, "To compute the coupling power the detector has to be provided to Setup class"
|
119
|
+
|
120
|
+
if as_numpy:
|
121
|
+
return self._get_measure_array(measures)
|
122
|
+
|
123
|
+
is_complex = self._is_complex_measure(measures)
|
124
|
+
|
125
|
+
df = self.generate_dataframe(
|
126
|
+
measure=list(measures),
|
127
|
+
is_complex=is_complex,
|
128
|
+
drop_unique_level=drop_unique_level
|
129
|
+
)
|
130
|
+
|
131
|
+
df = self._get_measure_dataframe(df, measures, is_complex, add_units)
|
132
|
+
|
133
|
+
# Optionally scale the units in the DataFrame
|
134
|
+
if scale_unit:
|
135
|
+
df = self.scale_dataframe_units(df)
|
136
|
+
|
137
|
+
return df
|
138
|
+
|
139
|
+
def _is_complex_measure(self, measures: set) -> bool:
|
140
|
+
"""
|
141
|
+
Determines if the measures involve complex values based on measure names.
|
142
|
+
No complex value are computed now, as the a & b parameters a return as absolute values for convienience.
|
143
|
+
|
144
|
+
Parameters
|
145
|
+
----------
|
146
|
+
measures : set
|
147
|
+
A set of measures requested for computation.
|
148
|
+
|
149
|
+
Returns
|
150
|
+
-------
|
151
|
+
bool
|
152
|
+
True if any measure involves complex values, False otherwise.
|
153
|
+
"""
|
154
|
+
return False
|
155
|
+
|
156
|
+
def _get_measure_array(self, measures: List[str]) -> pd.DataFrame:
|
157
|
+
"""
|
158
|
+
Retrieve data for specified measures and return them as a NumPy array.
|
159
|
+
|
160
|
+
Parameters
|
161
|
+
----------
|
162
|
+
measures : List[str]
|
163
|
+
List of measure names to compute.
|
164
|
+
|
165
|
+
Returns
|
166
|
+
-------
|
167
|
+
np.ndarray
|
168
|
+
A NumPy array containing computed values for the specified measures.
|
169
|
+
"""
|
170
|
+
output_array = []
|
171
|
+
|
172
|
+
for measure in measures:
|
173
|
+
method_name = f'get_{measure}'
|
174
|
+
|
175
|
+
method = getattr(self.binding, method_name)
|
176
|
+
|
177
|
+
array = method(
|
178
|
+
scatterer_set=self.scatterer.binding,
|
179
|
+
source_set=self.source.binding,
|
180
|
+
detector_set=self.detector.binding
|
181
|
+
)
|
182
|
+
|
183
|
+
output_array.append(array)
|
184
|
+
|
185
|
+
return numpy.asarray(output_array).squeeze()
|
186
|
+
|
187
|
+
def _get_measure_dataframe(self, df: pd.DataFrame, measures: List[str], is_complex: bool, add_units: bool) -> pd.DataFrame:
|
188
|
+
"""
|
189
|
+
Populate a DataFrame with computed values for specified measures.
|
190
|
+
|
191
|
+
Parameters
|
192
|
+
----------
|
193
|
+
df : pd.DataFrame
|
194
|
+
DataFrame in which to store computed values for the specified measures.
|
195
|
+
measures : List[str]
|
196
|
+
List of measure names to compute and add to the DataFrame.
|
197
|
+
is_complex : bool
|
198
|
+
Indicates whether any of the measures involve complex values.
|
199
|
+
add_units : bool
|
200
|
+
If True, adds units to the computed values in the DataFrame.
|
201
|
+
|
202
|
+
Returns
|
203
|
+
-------
|
204
|
+
pd.DataFrame
|
205
|
+
Updated DataFrame with computed values for the specified measures, with optional units if `add_units` is True.
|
206
|
+
"""
|
207
|
+
for measure in measures:
|
208
|
+
|
209
|
+
method_name = f'get_{measure}'
|
210
|
+
|
211
|
+
# Compute the values using the binding method
|
212
|
+
array = getattr(self.binding, method_name)(
|
213
|
+
scatterer_set=self.scatterer.binding,
|
214
|
+
source_set=self.source.binding,
|
215
|
+
detector_set=self.detector.binding
|
216
|
+
)
|
217
|
+
|
218
|
+
# Determine the unit based on the measure type
|
219
|
+
dtype = self._determine_dtype(measure)
|
220
|
+
|
221
|
+
# Set the data in the DataFrame
|
222
|
+
df = self._set_data(
|
223
|
+
measure=measure,
|
224
|
+
dataframe=df,
|
225
|
+
array=array,
|
226
|
+
dtype=dtype,
|
227
|
+
is_complex=is_complex,
|
228
|
+
add_units=add_units
|
229
|
+
)
|
230
|
+
|
231
|
+
return df
|
232
|
+
|
233
|
+
def _determine_dtype(self, measure: str):
|
234
|
+
"""
|
235
|
+
Determine the appropriate unit (dtype) based on the measure's first character.
|
236
|
+
|
237
|
+
Parameters
|
238
|
+
----------
|
239
|
+
measure : str
|
240
|
+
The measure type to determine the unit for.
|
241
|
+
|
242
|
+
Returns
|
243
|
+
-------
|
244
|
+
pint.Quantity
|
245
|
+
The appropriate unit for the measure.
|
246
|
+
"""
|
247
|
+
match measure[0]:
|
248
|
+
case 'C':
|
249
|
+
return meter**2 # Cross-section measure
|
250
|
+
case 'c':
|
251
|
+
assert self.detector is not None, "Detector needs to be defined in order to measure coupling"
|
252
|
+
return watt # Power measure
|
253
|
+
case _:
|
254
|
+
return AU # Arbitrary units for other measures
|
255
|
+
|
256
|
+
def _set_data(self, measure: str, dataframe: pd.DataFrame, array: numpy.ndarray, dtype: type, is_complex: bool, add_units: bool) -> None:
|
257
|
+
"""
|
258
|
+
Sets the real and imaginary parts of a NumPy array as separate 'real' and 'imag' levels
|
259
|
+
in the 'type' index of a pandas DataFrame.
|
260
|
+
|
261
|
+
Parameters
|
262
|
+
----------
|
263
|
+
dataframe : pd.DataFrame
|
264
|
+
The target DataFrame with a MultiIndex that includes a 'type' level (with possible values 'real' and 'imag').
|
265
|
+
array : np.ndarray
|
266
|
+
A complex NumPy array whose real and imaginary parts will be assigned to the DataFrame.
|
267
|
+
dtype : type
|
268
|
+
The data type to cast the real and imaginary parts into, using PintArrays.
|
269
|
+
is_complex : bool
|
270
|
+
A flag that indicates whether the input array is complex. If False, the 'type' level is not saved, and the array is treated as purely real.
|
271
|
+
"""
|
272
|
+
dtype = f'pint[{dtype}]' if add_units else float
|
273
|
+
|
274
|
+
if is_complex:
|
275
|
+
dataframe[(measure, 'real')] = pd.Series(array.ravel().real, dtype=dtype, index=dataframe.index)
|
276
|
+
dataframe[(measure, 'imag')] = pd.Series(array.ravel().imag, dtype=dtype, index=dataframe.index)
|
277
|
+
|
278
|
+
else:
|
279
|
+
dataframe[measure] = pd.Series(array.ravel(), dtype=dtype, index=dataframe.index)
|
280
|
+
|
281
|
+
return dataframe
|
282
|
+
|
283
|
+
def scale_dataframe_units(self, dataframe: pd.DataFrame) -> pd.DataFrame:
|
284
|
+
"""
|
285
|
+
Scales the units of all columns in a pandas DataFrame to the most compact unit
|
286
|
+
based on the maximum value in each column.
|
287
|
+
|
288
|
+
This method iterates over each column in the DataFrame, retrieves the maximum value's
|
289
|
+
unit, and converts the entire column to that unit using pint's unit system.
|
290
|
+
|
291
|
+
Parameters
|
292
|
+
----------
|
293
|
+
dataframe : pd.DataFrame
|
294
|
+
A DataFrame where each column contains PintArray elements with units.
|
295
|
+
|
296
|
+
Returns
|
297
|
+
-------
|
298
|
+
pd.DataFrame
|
299
|
+
The updated DataFrame with all columns scaled to the most compact unit based on the maximum value in each column.
|
300
|
+
"""
|
301
|
+
max_value_unit = dataframe.max().max().to_compact().units
|
302
|
+
|
303
|
+
for name, col in dataframe.items():
|
304
|
+
dataframe[name] = col.pint.to(max_value_unit)
|
305
|
+
|
306
|
+
return dataframe
|
307
|
+
|
308
|
+
def generate_dataframe(self, measure, is_complex: bool = False, drop_unique_level: bool = True):
|
309
|
+
"""
|
310
|
+
Generates a pandas DataFrame with a MultiIndex based on the mapping
|
311
|
+
of 'source' and 'scatterer' from an experiment object.
|
312
|
+
|
313
|
+
Parameters
|
314
|
+
----------
|
315
|
+
experiment :
|
316
|
+
The experiment object containing 'source' and 'scatterer' mappings.
|
317
|
+
|
318
|
+
Returns
|
319
|
+
-------
|
320
|
+
pd.DataFrame
|
321
|
+
A DataFrame with a MultiIndex created from the experiment mappings.
|
322
|
+
"""
|
323
|
+
self._generate_mapping()
|
324
|
+
|
325
|
+
iterables = dict()
|
326
|
+
|
327
|
+
iterables.update(self.source.mapping)
|
328
|
+
|
329
|
+
iterables.update(self.scatterer.mapping)
|
330
|
+
|
331
|
+
if self.detector is not None:
|
332
|
+
iterables.update(self.detector.mapping)
|
333
|
+
|
334
|
+
if drop_unique_level:
|
335
|
+
_iterables = {
|
336
|
+
k: v for k, v in iterables.items() if len(v) > 1
|
337
|
+
}
|
338
|
+
|
339
|
+
if len(_iterables) != 0:
|
340
|
+
iterables = _iterables
|
341
|
+
|
342
|
+
# Create a MultiIndex from the iterables
|
343
|
+
row_index = pd.MultiIndex.from_product(
|
344
|
+
iterables.values(), names=iterables.keys()
|
345
|
+
)
|
346
|
+
|
347
|
+
if is_complex:
|
348
|
+
columns = pd.MultiIndex.from_product(
|
349
|
+
[measure, ['real', 'imag']],
|
350
|
+
names=['data', 'type']
|
351
|
+
)
|
352
|
+
else:
|
353
|
+
columns = pd.MultiIndex.from_product([measure], names=['data'])
|
354
|
+
|
355
|
+
# Return an empty DataFrame with the generated MultiIndex
|
356
|
+
return PyMieSimDataFrame(columns=columns, index=row_index)
|