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,85 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
import numpy
|
4
|
+
from pydantic import field_validator
|
5
|
+
import pint_pandas
|
6
|
+
from dataclasses import fields
|
7
|
+
from PyMieSim.polarization import BasePolarization, Linear
|
8
|
+
from PyMieSim.units import Quantity, meter, watt, AU, degree
|
9
|
+
|
10
|
+
|
11
|
+
class BaseSource:
|
12
|
+
"""
|
13
|
+
Base class for light sources in PyMieSim experiments.
|
14
|
+
"""
|
15
|
+
|
16
|
+
@field_validator('wavelength', mode='plain')
|
17
|
+
def _validate_length(cls, value):
|
18
|
+
"""
|
19
|
+
Ensures that diameter is Quantity objects with length units.
|
20
|
+
"""
|
21
|
+
if not isinstance(value, Quantity) or not value.check(meter):
|
22
|
+
raise ValueError(f"{value} must be a Quantity with meters units [meter].")
|
23
|
+
|
24
|
+
return numpy.atleast_1d(value)
|
25
|
+
|
26
|
+
@field_validator('optical_power', mode='plain')
|
27
|
+
def _validate_power(cls, value):
|
28
|
+
"""
|
29
|
+
Ensure that arrays are properly converted to numpy arrays.
|
30
|
+
"""
|
31
|
+
if not isinstance(value, Quantity) or not value.check(watt):
|
32
|
+
raise ValueError(f"{value} must be a Quantity with power units [watt].")
|
33
|
+
|
34
|
+
if not isinstance(value, numpy.ndarray):
|
35
|
+
value = numpy.atleast_1d(value)
|
36
|
+
|
37
|
+
return value
|
38
|
+
|
39
|
+
@field_validator('NA', mode='plain')
|
40
|
+
def _validate_arbitrary_units(cls, value):
|
41
|
+
"""
|
42
|
+
Ensure that arrays are properly converted to numpy arrays.
|
43
|
+
"""
|
44
|
+
if not isinstance(value, Quantity) or not value.check(AU):
|
45
|
+
raise ValueError(f"{value} must be a Quantity with arbitrary units [AU].")
|
46
|
+
|
47
|
+
if not isinstance(value, numpy.ndarray):
|
48
|
+
value = numpy.atleast_1d(value)
|
49
|
+
|
50
|
+
return value
|
51
|
+
|
52
|
+
@field_validator('polarization', mode='before')
|
53
|
+
def _validate_polarization(cls, value):
|
54
|
+
"""
|
55
|
+
Ensures that polarization is well defined.
|
56
|
+
"""
|
57
|
+
if (not isinstance(value, Quantity) or not value.check(degree)) and not isinstance(value, BasePolarization):
|
58
|
+
raise ValueError(f"{value} must be a Quantity with degree units [degree] or a Polarization instance.")
|
59
|
+
|
60
|
+
if isinstance(value, BasePolarization):
|
61
|
+
return value
|
62
|
+
|
63
|
+
if isinstance(value, Quantity):
|
64
|
+
return Linear(value)
|
65
|
+
|
66
|
+
def _generate_mapping(self) -> None:
|
67
|
+
"""
|
68
|
+
Constructs a table of the scatterer's properties formatted for data visualization.
|
69
|
+
This method populates the `mapping` dictionary with user-friendly descriptions and formats of the scatterer properties.
|
70
|
+
|
71
|
+
Returns:
|
72
|
+
list: A list of visual representations for each property in the `mapping` dictionary that has been populated.
|
73
|
+
"""
|
74
|
+
for attr in [f.name for f in fields(self) if f.name != 'source']:
|
75
|
+
values = getattr(self, attr)
|
76
|
+
|
77
|
+
if values is None:
|
78
|
+
continue
|
79
|
+
|
80
|
+
if hasattr(values, 'magnitude'):
|
81
|
+
magnitude = values.magnitude
|
82
|
+
units = values.units
|
83
|
+
self.mapping["source:" + attr] = pint_pandas.PintArray(magnitude, dtype=units)
|
84
|
+
else:
|
85
|
+
self.mapping["source:" + attr] = [repr(m) for m in values]
|
@@ -0,0 +1,60 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
from typing import Union
|
4
|
+
from PyMieSim.single.source import Gaussian as _ # noqa:F401 # Necessary for pybind11 binding initialization
|
5
|
+
from pydantic.dataclasses import dataclass
|
6
|
+
from PyMieSim.units import Quantity
|
7
|
+
from PyMieSim.experiment.source.base import BaseSource
|
8
|
+
from PyMieSim.experiment.utils import config_dict, Sequential
|
9
|
+
from PyMieSim.polarization import BasePolarization, Linear
|
10
|
+
from PyMieSim.binary.interface_sets import CppGaussianSourceSet
|
11
|
+
|
12
|
+
@dataclass(config=config_dict)
|
13
|
+
class Gaussian(BaseSource, Sequential):
|
14
|
+
"""
|
15
|
+
Represents a Gaussian light source with a specified numerical aperture and optical power.
|
16
|
+
|
17
|
+
Inherits from BaseSource and adds specific attributes for Gaussian sources.
|
18
|
+
|
19
|
+
Parameters
|
20
|
+
----------
|
21
|
+
wavelength : Quantity
|
22
|
+
The wavelength(s) of the light source.
|
23
|
+
polarization : Union[UnitPolarizationAngle, Quantity]
|
24
|
+
Polarization state of the light field, if float is given it is assumed Linear polarization of angle theta.
|
25
|
+
NA : Quantity
|
26
|
+
The numerical aperture(s) of the Gaussian source.
|
27
|
+
optical_power : Quantity
|
28
|
+
The optical power of the source, in Watts.
|
29
|
+
"""
|
30
|
+
NA: Quantity
|
31
|
+
optical_power: Quantity
|
32
|
+
wavelength: Quantity
|
33
|
+
polarization: Union[BasePolarization, Quantity]
|
34
|
+
|
35
|
+
def _generate_binding(self) -> None:
|
36
|
+
"""
|
37
|
+
Prepares the keyword arguments for the C++ binding based on the scatterer's properties. This
|
38
|
+
involves evaluating material indices and organizing them into a dictionary for the C++ interface.
|
39
|
+
|
40
|
+
"""
|
41
|
+
self.mapping = {}
|
42
|
+
|
43
|
+
if not isinstance(self.polarization, BasePolarization):
|
44
|
+
self.polarization = Linear(self.polarization)
|
45
|
+
|
46
|
+
self.binding_kwargs = dict(
|
47
|
+
wavelength=self.wavelength,
|
48
|
+
jones_vector=self.polarization.element,
|
49
|
+
NA=self.NA,
|
50
|
+
optical_power=self.optical_power,
|
51
|
+
is_sequential=self.is_sequential
|
52
|
+
)
|
53
|
+
|
54
|
+
self.binding = CppGaussianSourceSet(
|
55
|
+
wavelength=self.wavelength.to('meter').magnitude,
|
56
|
+
jones_vector=self.polarization.element,
|
57
|
+
NA=self.NA.to('dimensionless').magnitude,
|
58
|
+
optical_power=self.optical_power.to('watt').magnitude,
|
59
|
+
is_sequential=self.is_sequential
|
60
|
+
)
|
@@ -0,0 +1,69 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
from typing import Union
|
4
|
+
from PyMieSim.single.source import PlaneWave as _ # noqa:F401 # Necessary for pybind11 binding initialization
|
5
|
+
import numpy
|
6
|
+
from pydantic.dataclasses import dataclass
|
7
|
+
from pydantic import field_validator
|
8
|
+
from PyMieSim.units import Quantity
|
9
|
+
from PyMieSim.experiment.source.base import BaseSource
|
10
|
+
from PyMieSim.polarization import BasePolarization, Linear
|
11
|
+
from PyMieSim.experiment.utils import config_dict, Sequential
|
12
|
+
from PyMieSim.binary.interface_sets import CppPlaneWaveSourceSet
|
13
|
+
|
14
|
+
|
15
|
+
@dataclass(config=config_dict)
|
16
|
+
class PlaneWave(BaseSource, Sequential):
|
17
|
+
"""
|
18
|
+
Represents a Plane Wave light source with a specified amplitude.
|
19
|
+
|
20
|
+
Inherits from BaseSource and specifies amplitude directly.
|
21
|
+
|
22
|
+
Parameters
|
23
|
+
----------
|
24
|
+
wavelength : Quantity
|
25
|
+
The wavelength(s) of the light source.
|
26
|
+
polarization : Union[UnitPolarizationAngle, Quantity]
|
27
|
+
Polarization state of the light field, if float is given it is assumed Linear polarization of angle theta.
|
28
|
+
amplitude : Quantity
|
29
|
+
The amplitude of the plane wave, in Volt/Meter.
|
30
|
+
"""
|
31
|
+
amplitude: Quantity
|
32
|
+
wavelength: Quantity
|
33
|
+
polarization: Union[BasePolarization, Quantity]
|
34
|
+
|
35
|
+
@field_validator('amplitude', mode='before')
|
36
|
+
def _validate_array(cls, value):
|
37
|
+
"""Ensure that arrays are properly converted to numpy arrays."""
|
38
|
+
if not isinstance(value, numpy.ndarray):
|
39
|
+
value = numpy.atleast_1d(value)
|
40
|
+
|
41
|
+
return value
|
42
|
+
|
43
|
+
def _generate_binding(self) -> None:
|
44
|
+
"""
|
45
|
+
Prepares the keyword arguments for the C++ binding based on the scatterer's properties. This
|
46
|
+
involves evaluating material indices and organizing them into a dictionary for the C++ interface.
|
47
|
+
|
48
|
+
Returns
|
49
|
+
-------
|
50
|
+
None
|
51
|
+
"""
|
52
|
+
self.mapping = {}
|
53
|
+
|
54
|
+
if not isinstance(self.polarization, BasePolarization):
|
55
|
+
self.polarization = Linear(self.polarization)
|
56
|
+
|
57
|
+
self.binding_kwargs = dict(
|
58
|
+
wavelength=self.wavelength,
|
59
|
+
jones_vector=self.polarization.element,
|
60
|
+
amplitude=self.amplitude,
|
61
|
+
is_sequential=self.is_sequential
|
62
|
+
)
|
63
|
+
|
64
|
+
self.binding = CppPlaneWaveSourceSet(
|
65
|
+
wavelength=self.wavelength.to('meter').magnitude,
|
66
|
+
jones_vector=self.polarization.element,
|
67
|
+
amplitude=self.amplitude.to('volt/meter').magnitude,
|
68
|
+
is_sequential=self.is_sequential
|
69
|
+
)
|
@@ -0,0 +1,132 @@
|
|
1
|
+
import numpy as np
|
2
|
+
from typing import Union, List, Dict, Optional, TypeVar
|
3
|
+
from PyMieSim.units import Quantity
|
4
|
+
from PyOptik.material.base_class import BaseMaterial
|
5
|
+
|
6
|
+
|
7
|
+
T = TypeVar('T')
|
8
|
+
|
9
|
+
from pydantic import ConfigDict
|
10
|
+
|
11
|
+
# Configuration dictionary for the Pydantic dataclass
|
12
|
+
config_dict = ConfigDict(
|
13
|
+
kw_only=True,
|
14
|
+
slots=True,
|
15
|
+
extra='forbid',
|
16
|
+
arbitrary_types_allowed=True
|
17
|
+
)
|
18
|
+
|
19
|
+
|
20
|
+
def broadcast_params(total_size: Optional[int] = None, **kwargs: Union[T, List[T]]) -> Dict[str, np.ndarray]:
|
21
|
+
"""
|
22
|
+
Broadcast scalar or single-element parameters to NumPy arrays of a given target size.
|
23
|
+
|
24
|
+
Each parameter is first converted to a 1D NumPy array. If a parameter is given as a scalar
|
25
|
+
or an array/list of length one, it will be repeated (broadcast) to match the target size.
|
26
|
+
The target size is determined by `total_size` if provided, or as the maximum size among all
|
27
|
+
parameters otherwise. If any parameter is provided as an array of length greater than one and
|
28
|
+
its length does not match the target size, a ValueError is raised.
|
29
|
+
|
30
|
+
Parameters
|
31
|
+
----------
|
32
|
+
total_size : int, optional
|
33
|
+
The explicit target size for broadcasting. Must be a positive integer if provided.
|
34
|
+
If not provided, the target size is determined as the maximum size among the input parameters.
|
35
|
+
**kwargs
|
36
|
+
Keyword arguments mapping parameter names to values, each of which may be a scalar or a list/array.
|
37
|
+
|
38
|
+
Returns
|
39
|
+
-------
|
40
|
+
dict
|
41
|
+
A dictionary with the same keys as the input parameters, where each value is a NumPy array
|
42
|
+
of length equal to the target size.
|
43
|
+
|
44
|
+
Raises
|
45
|
+
------
|
46
|
+
ValueError
|
47
|
+
If any parameter provided as a list/array has a length greater than one that does not match
|
48
|
+
the target size, or if total_size is provided and is less than the maximum size among the inputs.
|
49
|
+
AssertionError
|
50
|
+
If total_size is provided and is not a positive integer.
|
51
|
+
"""
|
52
|
+
if total_size is not None:
|
53
|
+
assert isinstance(total_size, int) and total_size > 0, "total_size must be a positive integer"
|
54
|
+
|
55
|
+
# Convert each parameter to a 1D NumPy array.
|
56
|
+
arrays = {name: np.atleast_1d(val) for name, val in kwargs.items()}
|
57
|
+
|
58
|
+
# Determine the target size.
|
59
|
+
target_size = total_size or max(arr.size for arr in arrays.values())
|
60
|
+
|
61
|
+
# Ensure no provided array of size > 1 mismatches the explicit target_size.
|
62
|
+
for name, arr in arrays.items():
|
63
|
+
if arr.size > 1 and arr.size != target_size:
|
64
|
+
raise ValueError(f"Inconsistent sizes: '{name}' has size {arr.size} but expected 1 or {target_size}.")
|
65
|
+
|
66
|
+
# Broadcast any array of size 1 to the target size.
|
67
|
+
for name, arr in arrays.items():
|
68
|
+
if arr.size == 1:
|
69
|
+
arrays[name] = np.full(target_size, arr.item())
|
70
|
+
if isinstance(arr, Quantity):
|
71
|
+
arrays[name] *= arr.units
|
72
|
+
|
73
|
+
return arrays
|
74
|
+
|
75
|
+
|
76
|
+
class Sequential:
|
77
|
+
is_sequential = False
|
78
|
+
|
79
|
+
@classmethod
|
80
|
+
def build_sequential(cls, total_size: Optional[int] = None, **kwargs) -> object:
|
81
|
+
"""
|
82
|
+
Broadcast scalar or single-element parameters to NumPy arrays of uniform length
|
83
|
+
and construct a new instance of the class.
|
84
|
+
|
85
|
+
Each parameter passed via keyword arguments may be provided as a scalar or as a list/array.
|
86
|
+
Scalars or single-element lists/arrays will be repeated to match the length of the longest
|
87
|
+
parameter. If any parameter is given as a list/array with more than one element, its length
|
88
|
+
must equal the maximum length among all parameters (or the explicit `total_size` if provided);
|
89
|
+
otherwise, a ValueError is raised.
|
90
|
+
|
91
|
+
Additionally, if a keyword argument 'source' is provided, it is removed from the
|
92
|
+
broadcasted parameters and passed directly to the class constructor.
|
93
|
+
|
94
|
+
Parameters
|
95
|
+
----------
|
96
|
+
total_size : int, optional
|
97
|
+
The explicit target size for broadcasting. Must be a positive integer if provided.
|
98
|
+
If not specified, the target size is determined by the maximum size among the input parameters.
|
99
|
+
**kwargs : dict
|
100
|
+
Arbitrary keyword arguments mapping parameter names to values. Each value may be a scalar
|
101
|
+
or a list/array of values. Examples include parameters such as `source`, `diameter`,
|
102
|
+
`property`, or `medium_property`.
|
103
|
+
|
104
|
+
Returns
|
105
|
+
-------
|
106
|
+
object
|
107
|
+
A new instance of the class with all provided parameters broadcasted as NumPy arrays
|
108
|
+
of equal length. If 'source' is provided, it is passed directly to the class constructor.
|
109
|
+
|
110
|
+
Raises
|
111
|
+
------
|
112
|
+
ValueError
|
113
|
+
If any parameter provided as a list/array has a length greater than one that does not
|
114
|
+
match the target size.
|
115
|
+
"""
|
116
|
+
is_material = any([
|
117
|
+
isinstance(v, BaseMaterial) for v in kwargs.values()
|
118
|
+
])
|
119
|
+
|
120
|
+
assert not is_material, "For the moment no Material can be defined material property for sequential computing, one should use refractive index property. See documentation online."
|
121
|
+
source = kwargs.pop("source", None)
|
122
|
+
kwargs = broadcast_params(total_size=total_size, **kwargs)
|
123
|
+
|
124
|
+
if source is not None:
|
125
|
+
instance = cls(source=source, **kwargs)
|
126
|
+
else:
|
127
|
+
instance = cls(**kwargs)
|
128
|
+
|
129
|
+
instance.is_sequential = True
|
130
|
+
|
131
|
+
return instance
|
132
|
+
|
PyMieSim/gui/__init__.py
ADDED
File without changes
|
PyMieSim/gui/helper.py
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
from PyMieSim.experiment.source.gaussian import Gaussian
|
2
|
+
from PyMieSim.experiment.scatterer.sphere import Sphere
|
3
|
+
from PyMieSim.experiment.detector.photodiode import Photodiode
|
4
|
+
from PyMieSim.experiment import Setup
|
5
|
+
from PyMieSim.units import nanometer, degree, milliwatt, AU, RIU
|
6
|
+
import numpy
|
7
|
+
|
8
|
+
|
9
|
+
length_units = nanometer
|
10
|
+
power_units = milliwatt
|
11
|
+
angle_units = degree
|
12
|
+
|
13
|
+
|
14
|
+
def parse_string_to_array_or_float(input_str):
|
15
|
+
"""
|
16
|
+
Parse a string to return either a numpy array or a float.
|
17
|
+
|
18
|
+
If the string is in the format 'start:end:count', return np.linspace(start, end, count).
|
19
|
+
If the string is a comma-separated list, return a numpy array.
|
20
|
+
If the string is a single numeric value, return it as a float.
|
21
|
+
"""
|
22
|
+
try:
|
23
|
+
if ":" in input_str:
|
24
|
+
start, end, count = map(float, input_str.split(":"))
|
25
|
+
return numpy.linspace(start, end, int(count))
|
26
|
+
elif "," in input_str:
|
27
|
+
return numpy.array(list(map(float, input_str.split(","))))
|
28
|
+
else:
|
29
|
+
return float(input_str)
|
30
|
+
except ValueError:
|
31
|
+
raise ValueError("Invalid input string format. Expected 'start:end:count', a comma-separated list, or a single numeric value.")
|
32
|
+
|
33
|
+
|
34
|
+
def interface(source_kwargs: dict, scatterer_kwargs: dict, detector_kwargs: dict, measure: str, **kwargs):
|
35
|
+
source = Gaussian(
|
36
|
+
wavelength=parse_string_to_array_or_float(source_kwargs['wavelength']) * length_units,
|
37
|
+
polarization=parse_string_to_array_or_float(source_kwargs['polarization']) * angle_units,
|
38
|
+
NA=parse_string_to_array_or_float(source_kwargs['NA']) * AU,
|
39
|
+
optical_power=parse_string_to_array_or_float(source_kwargs['optical_power']) * milliwatt
|
40
|
+
)
|
41
|
+
|
42
|
+
scatterer = Sphere(
|
43
|
+
diameter=parse_string_to_array_or_float(scatterer_kwargs['diameter']) * length_units,
|
44
|
+
property=parse_string_to_array_or_float(scatterer_kwargs['property']) * RIU,
|
45
|
+
medium_property=parse_string_to_array_or_float(scatterer_kwargs['medium_property']) * RIU,
|
46
|
+
source=source
|
47
|
+
)
|
48
|
+
|
49
|
+
detector = Photodiode(
|
50
|
+
NA=parse_string_to_array_or_float(detector_kwargs['NA']) * AU,
|
51
|
+
gamma_offset=parse_string_to_array_or_float(detector_kwargs['gamma_offset']) * angle_units,
|
52
|
+
phi_offset=parse_string_to_array_or_float(detector_kwargs['phi_offset']) * angle_units,
|
53
|
+
sampling=parse_string_to_array_or_float(detector_kwargs['sampling']) * AU,
|
54
|
+
)
|
55
|
+
|
56
|
+
setup = Setup(source=source, scatterer=scatterer, detector=detector)
|
57
|
+
|
58
|
+
dataframe = setup.get(measure, **kwargs)
|
59
|
+
|
60
|
+
return dataframe
|
@@ -0,0 +1,136 @@
|
|
1
|
+
from dash import Dash, html, dcc
|
2
|
+
import matplotlib.pyplot as plt
|
3
|
+
import io
|
4
|
+
import base64
|
5
|
+
import webbrowser
|
6
|
+
from PyMieSim.gui.section import SourceSection, ScattererSection, DetectorSection, MeasureSection
|
7
|
+
from PyMieSim.gui.helper import interface
|
8
|
+
|
9
|
+
dcc_store_id = "input-store"
|
10
|
+
|
11
|
+
class OpticalSetupGUI:
|
12
|
+
"""
|
13
|
+
A class to manage the overall GUI for the optical simulation interface.
|
14
|
+
|
15
|
+
This class integrates various sections, including Source, Scatterer, Detector, and Measure sections,
|
16
|
+
and sets up the layout and callbacks for the Dash application.
|
17
|
+
"""
|
18
|
+
|
19
|
+
def __init__(self):
|
20
|
+
"""
|
21
|
+
Initialize the Dash app and other settings.
|
22
|
+
|
23
|
+
This method sets up the Dash application, initializes the individual sections, and prepares the layout
|
24
|
+
and callbacks.
|
25
|
+
"""
|
26
|
+
plt.switch_backend('Agg')
|
27
|
+
self.app = Dash(__name__, external_stylesheets=["https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"])
|
28
|
+
|
29
|
+
self.source_section = SourceSection(self.app)
|
30
|
+
self.scatterer_section = ScattererSection(self.app)
|
31
|
+
self.detector_section = DetectorSection(self.app)
|
32
|
+
self.measure_section = MeasureSection(self.app, self.scatterer_section, self.source_section, self.detector_section)
|
33
|
+
|
34
|
+
self.setup_layout()
|
35
|
+
self.setup_callbacks()
|
36
|
+
|
37
|
+
def create_plot(self, measure: str, xaxis: str):
|
38
|
+
"""
|
39
|
+
Generate a matplotlib plot and return it as a base64 image.
|
40
|
+
|
41
|
+
Parameters
|
42
|
+
----------
|
43
|
+
measure : str
|
44
|
+
The type of measure to plot (e.g., Qsca, Qext).
|
45
|
+
xaxis : str
|
46
|
+
The parameter to plot on the x-axis.
|
47
|
+
|
48
|
+
Returns
|
49
|
+
-------
|
50
|
+
str
|
51
|
+
A base64-encoded string of the generated plot image.
|
52
|
+
"""
|
53
|
+
data = interface(
|
54
|
+
source_kwargs=self.source_section.data,
|
55
|
+
scatterer_kwargs=self.scatterer_section.data,
|
56
|
+
detector_kwargs=self.detector_section.data,
|
57
|
+
measure=measure
|
58
|
+
)
|
59
|
+
|
60
|
+
data.plot(x=xaxis)
|
61
|
+
|
62
|
+
buf = io.BytesIO()
|
63
|
+
plt.savefig(buf, format='png')
|
64
|
+
buf.seek(0)
|
65
|
+
encoded_image = base64.b64encode(buf.read()).decode('utf-8')
|
66
|
+
buf.close()
|
67
|
+
return f"data:image/png;base64,{encoded_image}"
|
68
|
+
|
69
|
+
def save_func(self, filename: str, measure: str):
|
70
|
+
"""
|
71
|
+
Save the simulation data to a CSV file.
|
72
|
+
|
73
|
+
Parameters
|
74
|
+
----------
|
75
|
+
filename : str
|
76
|
+
The name of the file to save the data.
|
77
|
+
measure : str
|
78
|
+
The type of measure to save.
|
79
|
+
xaxis : str
|
80
|
+
The parameter to include on the x-axis in the saved data.
|
81
|
+
"""
|
82
|
+
return interface(
|
83
|
+
source_kwargs=self.source_section.data,
|
84
|
+
scatterer_kwargs=self.scatterer_section.data,
|
85
|
+
detector_kwargs=self.detector_section.data,
|
86
|
+
measure=measure,
|
87
|
+
add_units=False
|
88
|
+
)
|
89
|
+
|
90
|
+
def setup_layout(self):
|
91
|
+
"""
|
92
|
+
Define the layout of the Dash app.
|
93
|
+
|
94
|
+
This method organizes the sections and visualization components in the application layout.
|
95
|
+
"""
|
96
|
+
self.app.layout = html.Div([
|
97
|
+
dcc.Store(id=dcc_store_id), # Store to save input values
|
98
|
+
html.Div([
|
99
|
+
html.H1("Optical Simulation Interface", style={'text-align': 'center'}),
|
100
|
+
*self.source_section.create(),
|
101
|
+
*self.scatterer_section.create(),
|
102
|
+
*self.detector_section.create(),
|
103
|
+
], style={'width': '40%', 'float': 'left', 'padding': '10px'}),
|
104
|
+
|
105
|
+
html.Div([
|
106
|
+
html.H1("Visualization", style={'text-align': 'center'}),
|
107
|
+
self.measure_section.create(),
|
108
|
+
html.Img(id="plot-image", style={'width': '100%', 'height': 'auto', 'margin-top': '20px'})
|
109
|
+
], style={'width': '55%', 'float': 'right', 'padding': '10px', 'border-left': '1px solid black'})
|
110
|
+
])
|
111
|
+
|
112
|
+
def setup_callbacks(self):
|
113
|
+
"""
|
114
|
+
Set up Dash callbacks for the GUI.
|
115
|
+
|
116
|
+
This method links user interactions with the relevant functions to update the plot and save data.
|
117
|
+
"""
|
118
|
+
self.measure_section.update_callbacks(self.create_plot, self.save_func)
|
119
|
+
|
120
|
+
def run(self, host: str = "0.0.0.0", port: str = "8050", open_browser: bool = False, debug: bool = False):
|
121
|
+
"""
|
122
|
+
Run the Dash app.
|
123
|
+
|
124
|
+
This method starts the Dash server and opens the application in the default web browser.
|
125
|
+
"""
|
126
|
+
if open_browser:
|
127
|
+
webbrowser.open(f"http://{host}:{port}/", new=2)
|
128
|
+
self.app.run_server(debug=debug)
|
129
|
+
|
130
|
+
else:
|
131
|
+
self.app.run_server(debug=debug, host=host, port=port)
|
132
|
+
|
133
|
+
|
134
|
+
if __name__ == '__main__':
|
135
|
+
_gui = OpticalSetupGUI()
|
136
|
+
_gui.run(host='0.0.0.0', port='8050', open_browser=False, debug=True)
|