FlowCyPy 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. FlowCyPy/__init__.py +13 -0
  2. FlowCyPy/_version.py +16 -0
  3. FlowCyPy/acquisition.py +652 -0
  4. FlowCyPy/classifier.py +208 -0
  5. FlowCyPy/coupling_mechanism/__init__.py +4 -0
  6. FlowCyPy/coupling_mechanism/empirical.py +47 -0
  7. FlowCyPy/coupling_mechanism/mie.py +207 -0
  8. FlowCyPy/coupling_mechanism/rayleigh.py +116 -0
  9. FlowCyPy/coupling_mechanism/uniform.py +40 -0
  10. FlowCyPy/coupling_mechanism.py +205 -0
  11. FlowCyPy/cytometer.py +314 -0
  12. FlowCyPy/detector.py +439 -0
  13. FlowCyPy/directories.py +36 -0
  14. FlowCyPy/distribution/__init__.py +16 -0
  15. FlowCyPy/distribution/base_class.py +79 -0
  16. FlowCyPy/distribution/delta.py +104 -0
  17. FlowCyPy/distribution/lognormal.py +124 -0
  18. FlowCyPy/distribution/normal.py +128 -0
  19. FlowCyPy/distribution/particle_size_distribution.py +132 -0
  20. FlowCyPy/distribution/uniform.py +117 -0
  21. FlowCyPy/distribution/weibull.py +115 -0
  22. FlowCyPy/flow_cell.py +198 -0
  23. FlowCyPy/helper.py +81 -0
  24. FlowCyPy/logger.py +136 -0
  25. FlowCyPy/noises.py +34 -0
  26. FlowCyPy/particle_count.py +127 -0
  27. FlowCyPy/peak_locator/__init__.py +4 -0
  28. FlowCyPy/peak_locator/base_class.py +163 -0
  29. FlowCyPy/peak_locator/basic.py +108 -0
  30. FlowCyPy/peak_locator/derivative.py +143 -0
  31. FlowCyPy/peak_locator/moving_average.py +166 -0
  32. FlowCyPy/physical_constant.py +19 -0
  33. FlowCyPy/plottings.py +269 -0
  34. FlowCyPy/population.py +136 -0
  35. FlowCyPy/populations_instances.py +65 -0
  36. FlowCyPy/scatterer_collection.py +306 -0
  37. FlowCyPy/signal_digitizer.py +90 -0
  38. FlowCyPy/source.py +249 -0
  39. FlowCyPy/units.py +30 -0
  40. FlowCyPy/utils.py +191 -0
  41. FlowCyPy-0.7.0.dist-info/LICENSE +21 -0
  42. FlowCyPy-0.7.0.dist-info/METADATA +252 -0
  43. FlowCyPy-0.7.0.dist-info/RECORD +45 -0
  44. FlowCyPy-0.7.0.dist-info/WHEEL +5 -0
  45. FlowCyPy-0.7.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,306 @@
1
+ from typing import List, Optional, Union
2
+ from MPSPlots.styles import mps
3
+ import pandas as pd
4
+ from FlowCyPy.units import Quantity, RIU, particle, liter
5
+ from FlowCyPy.population import Population
6
+ from FlowCyPy.utils import PropertiesReport
7
+ import matplotlib.patches as mpatches
8
+ from typing import Optional, List
9
+ from pint_pandas import PintArray
10
+ import seaborn as sns
11
+ import matplotlib.pyplot as plt
12
+ import numpy as np
13
+ from matplotlib.colors import LinearSegmentedColormap
14
+ from enum import Enum
15
+ from FlowCyPy import units
16
+
17
+
18
+ class CouplingModel(Enum):
19
+ MIE = 'mie'
20
+ RAYLEIGH = 'rayleigh'
21
+ UNIFORM = 'uniform'
22
+
23
+
24
+ class ScattererCollection(PropertiesReport):
25
+ """
26
+ Defines and manages the size and refractive index distributions of scatterers (particles)
27
+ passing through a flow cytometer. This class generates random scatterer sizes and refractive
28
+ indices based on a list of provided distributions (e.g., Normal, LogNormal, Uniform, etc.).
29
+
30
+ """
31
+ def __init__(self, medium_refractive_index: Quantity = 1.0 * RIU, populations: List[Population] = None, coupling_model: Optional[CouplingModel] = CouplingModel.MIE):
32
+ """
33
+ Parameters
34
+ ----------
35
+ populations : List[Population]
36
+ A list of Population instances that define different scatterer populations.
37
+ coupling_model : Optional[CouplingModel], optional
38
+ The type of coupling factor to use (CouplingModel.MIE, CouplingModel.RAYLEIGH, CouplingModel.UNIFORM). Default is CouplingModel.MIE.
39
+ medium_refractive_index : float
40
+ The refractive index of the medium. Default is 1.0.
41
+ """
42
+ self.populations = populations or []
43
+ self.medium_refractive_index = medium_refractive_index
44
+ self.coupling_model = coupling_model
45
+ self.dataframe: pd.DataFrame = None
46
+
47
+ def get_population_ratios(self) -> list[float]:
48
+ total_concentration = sum([p.particle_count.value for p in self.populations])
49
+
50
+ return [(p.particle_count.value / total_concentration).magnitude for p in self.populations]
51
+
52
+ def get_population_dataframe(self, total_sampling: Optional[Quantity]) -> pd.DataFrame:
53
+ """
54
+ Generate a DataFrame by sampling particles from populations.
55
+
56
+ Parameters
57
+ ----------
58
+ total_sampling : Quantity, optional
59
+ Total number of samples to draw, distributed across populations based on their ratios.
60
+
61
+ Returns
62
+ -------
63
+ pd.DataFrame
64
+ A MultiIndex DataFrame containing the sampled data from all populations. The first
65
+ index level indicates the population name, and the second level indexes the sampled data.
66
+ """
67
+ ratios = self.get_population_ratios()
68
+
69
+ sampling_list = [int(ratio * total_sampling.magnitude) for ratio in ratios]
70
+
71
+ population_names = [p.name for p in self.populations]
72
+
73
+ # Create tuples for the MultiIndex
74
+ multi_index_tuples = [
75
+ (pop_name, idx) for pop_name, n in zip(population_names, sampling_list) for idx in range(n)
76
+ ]
77
+
78
+ # Create the MultiIndex
79
+ multi_index = pd.MultiIndex.from_tuples(multi_index_tuples, names=["Population", "Index"])
80
+
81
+ # Initialize an empty DataFrame with the MultiIndex
82
+ scatterer_dataframe = pd.DataFrame(index=multi_index)
83
+
84
+ self.fill_dataframe_with_sampling(scatterer_dataframe=scatterer_dataframe)
85
+
86
+ return scatterer_dataframe
87
+
88
+ def add_population(self, *population: Population) -> 'ScattererCollection':
89
+ """
90
+ Adds a population to the ScattererCollection instance with the specified attributes.
91
+
92
+ Parameters
93
+ ----------
94
+ name : str
95
+ The name of the population.
96
+ size : BaseDistribution
97
+ The size distribution of the population.
98
+ refractive_index : BaseDistribution
99
+ The refractive index distribution of the population.
100
+
101
+ Returns
102
+ -------
103
+ ScattererCollection
104
+ The ScattererCollection instance (to support chaining).
105
+
106
+ Raises
107
+ ------
108
+ ValueError
109
+ If the concentration does not have the expected dimensionality.
110
+ """
111
+ self.populations.extend(population)
112
+ return population
113
+
114
+ @property
115
+ def concentrations(self) -> List[Quantity]:
116
+ """
117
+ Gets the concentration of each population in the ScattererCollection instance.
118
+
119
+ Returns
120
+ -------
121
+ List[Quantity]
122
+ A list of concentrations for each population.
123
+ """
124
+ return [population.concentration for population in self.populations]
125
+
126
+ @concentrations.setter
127
+ def concentrations(self, values: Union[List[Quantity], Quantity]) -> None:
128
+ """
129
+ Sets the concentration of each population in the ScattererCollection instance.
130
+
131
+ Parameters
132
+ ----------
133
+ values : Union[List[Quantity], Quantity]
134
+ A list of concentrations to set for each population, or a single concentration value to set for all populations.
135
+
136
+ Raises
137
+ ------
138
+ ValueError
139
+ If the length of the values list does not match the number of populations or if any concentration has an incorrect dimensionality.
140
+ """
141
+ if isinstance(values, (list, tuple)):
142
+ if len(values) != len(self.populations):
143
+ raise ValueError("The length of the values list must match the number of populations.")
144
+
145
+ for value in values:
146
+ if value.dimensionality != (particle / liter).dimensionality:
147
+ raise ValueError(
148
+ f"Invalid concentration dimensionality: {value.dimensionality}. Expected dimensionality is 'particles per liter' or similar."
149
+ )
150
+
151
+ for population, value in zip(self.populations, values):
152
+ population.concentration = value
153
+ else:
154
+ if values.dimensionality != (particle / liter).dimensionality:
155
+ raise ValueError(
156
+ f"Invalid concentration dimensionality: {values.dimensionality}. Expected dimensionality is 'particles per liter' or similar."
157
+ )
158
+ for population in self.populations:
159
+ population.concentration = values
160
+
161
+ def dilute(self, factor: float) -> None:
162
+ """
163
+ Dilutes the populations in the flow cytometry system by a given factor.
164
+
165
+ Parameters
166
+ ----------
167
+ factor : float
168
+ The dilution factor to apply to each population. For example, a factor
169
+ of 0.5 reduces the population density by half.
170
+
171
+ Returns
172
+ -------
173
+ None
174
+ The method modifies the populations in place.
175
+
176
+ Notes
177
+ -----
178
+
179
+ - This method iterates over all populations in the system and applies the `dilute` method of each population.
180
+ - The specific implementation of how a population is diluted depends on the `dilute` method defined in the `population` object.
181
+
182
+ Examples
183
+ --------
184
+ Dilute all populations by 50%:
185
+ >>> system.dilute(0.5)
186
+ """
187
+ for population in self.populations:
188
+ population.dilute(factor)
189
+
190
+ def fill_dataframe_with_sampling(self, scatterer_dataframe: pd.DataFrame) -> None:
191
+ """
192
+ Fills a DataFrame with size and refractive index sampling data for each population.
193
+
194
+ Parameters
195
+ ----------
196
+ scatterer_dataframe : pd.DataFrame
197
+ A DataFrame indexed by population names (first level) and containing the
198
+ following columns: `'Size'`: To be filled with particle size data. `'RefractiveIndex'`:
199
+ To be filled with refractive index data. The DataFrame must already have the required structure.
200
+
201
+ Returns
202
+ -------
203
+ None
204
+ The method modifies the `scatterer_dataframe` in place, adding size and
205
+ refractive index sampling data.
206
+
207
+ After filling, the DataFrame will be populated with size and refractive index data.
208
+ """
209
+ for population in self.populations:
210
+ # Check if population.name is in the dataframe's index
211
+ if population.name not in scatterer_dataframe.index:
212
+ continue # Skip this iteration if population.name is not present
213
+
214
+ # Safely access the sub-dataframe and proceed
215
+ sub_dataframe = scatterer_dataframe.xs(population.name)
216
+ sampling = len(sub_dataframe) * units.particle
217
+
218
+ size, ri = population.generate_sampling(sampling)
219
+
220
+ scatterer_dataframe.loc[population.name, 'Size'] = PintArray(size, dtype=size.units)
221
+ scatterer_dataframe.loc[population.name, 'RefractiveIndex'] = PintArray(ri, dtype=ri.units)
222
+
223
+ def plot(self, n_points: int = 200) -> None:
224
+ """
225
+ Visualizes the joint and marginal distributions of size and refractive index
226
+ for multiple populations in the scatterer collection.
227
+
228
+ This method creates a joint plot using `sns.JointGrid`, where:
229
+
230
+ - The joint area displays filled contours representing the PDF values of size and refractive index.
231
+ - The marginal areas display the size and refractive index PDFs as filled plots.
232
+
233
+ Each population is plotted with a distinct color using a transparent colormap for the joint area.
234
+
235
+ Parameters
236
+ ----------
237
+ n_points : int, optional
238
+ The number of points used to compute the size and refractive index PDFs. Default is 100.
239
+ Increasing this value results in smoother distributions but increases computation time.
240
+
241
+ Notes
242
+ -----
243
+
244
+ - The joint area uses a transparent colormap, transitioning from fully transparent to fully opaque for better visualization of overlapping populations.
245
+ - Marginal plots show semi-transparent filled curves for clarity.
246
+
247
+ Example
248
+ -------
249
+ >>> scatterer_collection.plot(n_points=200)
250
+
251
+ """
252
+ def create_transparent_colormap(base_color):
253
+ """
254
+ Create a colormap that transitions from transparent to a specified base color.
255
+ """
256
+ return LinearSegmentedColormap.from_list(
257
+ "transparent_colormap",
258
+ [(base_color[0], base_color[1], base_color[2], 0), # Fully transparent
259
+ (base_color[0], base_color[1], base_color[2], 1)], # Fully opaque
260
+ N=256
261
+ )
262
+
263
+ # Create a JointGrid for visualization
264
+ with plt.style.context(mps):
265
+ grid = sns.JointGrid()
266
+
267
+ # Initialize a list to store legend handles
268
+ legend_handles = []
269
+
270
+ for index, population in enumerate(self.populations):
271
+ # Get size and refractive index PDFs
272
+ size, size_pdf = population.size.get_pdf(n_samples=n_points)
273
+ size_pdf /= size_pdf.max()
274
+ ri, ri_pdf = population.refractive_index.get_pdf(n_samples=n_points)
275
+ ri_pdf /= ri_pdf.max()
276
+
277
+ # Create a grid of size and ri values
278
+ X, Y = np.meshgrid(size, ri)
279
+ Z = np.outer(ri_pdf, size_pdf) # Compute the PDF values on the grid
280
+
281
+ # Create a transparent colormap for this population
282
+ base_color = sns.color_palette()[index]
283
+ transparent_cmap = create_transparent_colormap(base_color)
284
+
285
+ # Plot the joint area as a filled contour plot
286
+ grid.ax_joint.contourf(
287
+ X, Y, Z, levels=10, cmap=transparent_cmap, extend="min"
288
+ )
289
+
290
+ legend_handles.append(mpatches.Patch(color=base_color, label=population.name))
291
+
292
+
293
+ # Plot the marginal distributions
294
+ grid.ax_marg_x.fill_between(size.magnitude, size_pdf, color=f"C{index}", alpha=0.5)
295
+ grid.ax_marg_y.fill_betweenx(ri.magnitude, ri_pdf, color=f"C{index}", alpha=0.5)
296
+
297
+ # Set axis labels
298
+ grid.ax_joint.legend(handles=legend_handles, title="Populations")
299
+
300
+ grid.ax_joint.set_xlabel(f"Size [{size.units}]")
301
+ grid.ax_joint.set_ylabel(f"Refractive Index [{ri.units}]")
302
+
303
+ plt.tight_layout()
304
+
305
+ # Show the plot
306
+ plt.show()
@@ -0,0 +1,90 @@
1
+ from pydantic.dataclasses import dataclass
2
+ from pydantic import field_validator
3
+ from typing import Union, Tuple
4
+ import logging
5
+ import numpy as np
6
+ from FlowCyPy.units import Quantity, volt
7
+
8
+ config_dict = dict(
9
+ arbitrary_types_allowed=True,
10
+ kw_only=True,
11
+ slots=True,
12
+ extra='forbid'
13
+ )
14
+
15
+ @dataclass(config=config_dict)
16
+ class SignalDigitizer:
17
+ """
18
+ Handles signal digitization and saturation for a detector.
19
+
20
+ Attributes
21
+ ----------
22
+ sampling_freq : Quantity
23
+ The sampling frequency of the detector in hertz.
24
+ bit_depth : Union[int, str]
25
+ The number of discretization bins or bit-depth (e.g., '12bit').
26
+ saturation_levels : Union[str, Tuple[Quantity, Quantity]]
27
+ A tuple defining the lower and upper bounds for saturation, or 'auto' to set bounds dynamically.
28
+ """
29
+ sampling_freq: Quantity
30
+ bit_depth: Union[int, str] = '10bit'
31
+ saturation_levels: Union[str, Tuple[Quantity, Quantity], Quantity] = 'auto'
32
+
33
+ @property
34
+ def _bit_depth(self) -> int:
35
+ return self._process_bit_depth(self.bit_depth)
36
+
37
+ @property
38
+ def bandwidth(self) -> Quantity:
39
+ """
40
+ Automatically calculates the bandwidth based on the sampling frequency.
41
+
42
+ Returns
43
+ -------
44
+ Quantity
45
+ The bandwidth of the detector, which is half the sampling frequency (Nyquist limit).
46
+ """
47
+ return self.sampling_freq / 2
48
+
49
+ @staticmethod
50
+ def _process_bit_depth(bit_depth: Union[int, str]) -> int:
51
+ """
52
+ Converts bit-depth to the number of bins.
53
+
54
+ Parameters
55
+ ----------
56
+ bit_depth : Union[int, str]
57
+ The bit-depth as an integer or a string (e.g., '10bit').
58
+
59
+ Returns
60
+ -------
61
+ int
62
+ The number of bins corresponding to the bit-depth.
63
+ """
64
+ if isinstance(bit_depth, str):
65
+ bit_depth = int(bit_depth.rstrip('bit'))
66
+ bit_depth = 2 ** bit_depth
67
+
68
+ return bit_depth
69
+
70
+ @field_validator('sampling_freq')
71
+ def _validate_sampling_freq(cls, value):
72
+ """
73
+ Validates that the sampling frequency is provided in hertz.
74
+
75
+ Parameters
76
+ ----------
77
+ value : Quantity
78
+ The sampling frequency to validate.
79
+
80
+ Returns
81
+ -------
82
+ Quantity
83
+ The validated sampling frequency.
84
+
85
+ Raises:
86
+ ValueError: If the sampling frequency is not in hertz.
87
+ """
88
+ if not value.check('Hz'):
89
+ raise ValueError(f"sampling_freq must be in hertz, but got {value.units}")
90
+ return value
FlowCyPy/source.py ADDED
@@ -0,0 +1,249 @@
1
+
2
+ from typing import Optional
3
+ from pydantic.dataclasses import dataclass
4
+ from pydantic import field_validator
5
+ from FlowCyPy.units import meter, joule, particle, degree, volt, AU
6
+ from PyMieSim.units import Quantity
7
+ from FlowCyPy.utils import PropertiesReport
8
+ from FlowCyPy.physical_constant import PhysicalConstant
9
+ import numpy as np
10
+
11
+
12
+ config_dict = dict(
13
+ arbitrary_types_allowed=True,
14
+ kw_only=True,
15
+ slots=True,
16
+ extra='forbid'
17
+ )
18
+
19
+
20
+ class BaseBeam(PropertiesReport):
21
+ """
22
+ Mixin class providing unit validation for quantities used in optical sources.
23
+ """
24
+
25
+ def initialization(self):
26
+ """
27
+ Initialize beam waists based on the numerical apertures and calculate electric field amplitude at the focus.
28
+ """
29
+ self.frequency = PhysicalConstant.c / self.wavelength
30
+ self.photon_energy = (PhysicalConstant.h * self.frequency).to(joule) / particle
31
+
32
+ # Calculate amplitude at focus
33
+ self.amplitude = self.calculate_field_amplitude_at_focus()
34
+
35
+ def calculate_field_amplitude_at_focus(self) -> Quantity:
36
+ return NotImplementedError('This method should be implemneted by the derived class!')
37
+
38
+ @field_validator('wavelength', mode='plain')
39
+ def validate_wavelength(cls, value, field):
40
+ if not isinstance(value, Quantity):
41
+ raise ValueError(f"{value} must be a Quantity with distance units [<prefix>meter].")
42
+
43
+ if not value.check('meter'):
44
+ raise ValueError(f"{field} must be a Quantity with distance units [<prefix>meter], but got {value.units}.")
45
+
46
+ return value
47
+
48
+ @field_validator('polarization', mode='plain')
49
+ def validate_polarization(cls, value, field):
50
+ if not isinstance(value, Quantity):
51
+ raise ValueError(f"{value} must be a Quantity with angle units [<prefix>degree or <prefix>radian].")
52
+
53
+ if not value.check('degree'):
54
+ raise ValueError(f"{field} must be a Quantity with angle units [<prefix>degree or <prefix>radian], but got {value.units}.")
55
+
56
+ return value
57
+
58
+ @field_validator('optical_power', mode='plain')
59
+ def validate_optical_power(cls, value, field):
60
+ if not isinstance(value, Quantity):
61
+ raise ValueError(f"{value} must be a Quantity with power units [<prefix>watt].")
62
+
63
+ if not value.check('watt'):
64
+ raise ValueError(f"{field} must be a Quantity with power units [<prefix>watt], but got {value.units}.")
65
+
66
+ return value
67
+
68
+ @field_validator('numerical_aperture', 'numerical_aperture_x', 'numerical_aperture_y', mode='plain')
69
+ def validate_numerical_apertures(cls, value, field):
70
+ if not isinstance(value, Quantity):
71
+ raise ValueError(f"{value} must be a Quantity with arbitrary units [AU].")
72
+
73
+ if not value.check(AU):
74
+ raise ValueError(f"{field} must be a Quantity with arbitrary units [AU], but got {value.units}.")
75
+
76
+ if value.magnitude < 0 or value.magnitude > 1:
77
+ raise ValueError(f"{field} must be between 0 and 1, but got {value.magnitude}")
78
+ return value
79
+
80
+
81
+ @dataclass(config=config_dict)
82
+ class GaussianBeam(BaseBeam):
83
+ """
84
+ Represents a monochromatic Gaussian laser beam focused by a standard lens.
85
+
86
+ Parameters
87
+ ----------
88
+ optical_power : Quantity
89
+ The optical power of the laser (in watts).
90
+ wavelength : Quantity
91
+ The wavelength of the laser (in meters).
92
+ numerical_aperture : Quantity
93
+ The numerical aperture (NA) of the lens focusing the Gaussian beam (unitless).
94
+ polarization : Optional[Quantity]
95
+ The polarization of the laser source in degrees (default is 0 degrees).
96
+ RIN : Optional[float], optional
97
+ The Relative Intensity Noise (RIN) of the laser, specified as dB/Hz. Default is -120.0 dB/Hz, representing a pretty stable laser.
98
+
99
+ Attributes
100
+ ----------
101
+ waist : Quantity
102
+ The beam waist at the focus, calculated as `waist = wavelength / (pi * numerical_aperture)`.
103
+ frequency : Quantity
104
+ The frequency of the laser, calculated as `frequency = c / wavelength`.
105
+ photon_energy : Quantity
106
+ The energy of a single photon, calculated as `photon_energy = h * frequency`.
107
+ amplitude : Quantity
108
+ The electric field amplitude at the focus, derived from optical power, waist, and fundamental constants.
109
+
110
+ Notes
111
+ -----
112
+ RIN Interpretation:
113
+ - The Relative Intensity Noise (RIN) represents fluctuations in the laser's intensity relative to its mean optical power.
114
+ - For a Gaussian beam, the instantaneous intensity at any given time can be modeled as: I(t) = <I> * (1 + ΔI)
115
+
116
+ where:
117
+ - `<I>` is the mean intensity derived from the optical power.
118
+ - `ΔI` is the noise component, modeled as a Gaussian random variable with:
119
+ - Mean = 0 (fluctuations are around the mean intensity).
120
+ - Variance = RIN * <I>².
121
+
122
+ """
123
+ optical_power: Quantity
124
+ wavelength: Quantity
125
+ numerical_aperture: Quantity
126
+ polarization: Optional[Quantity] = 0 * degree
127
+ RIN: Optional[float] = -120.0
128
+
129
+ def __post_init__(self):
130
+ """
131
+ Initialize additional parameters like beam waist, frequency, photon energy,
132
+ and electric field amplitude at the focus.
133
+ """
134
+ self.initialization()
135
+
136
+ def calculate_field_amplitude_at_focus(self) -> Quantity:
137
+ r"""
138
+ Calculate the electric field amplitude (E0) at the focus for a Gaussian beam.
139
+
140
+ The electric field amplitude at the focus is given by:
141
+
142
+ .. math::
143
+ E_0 = \sqrt{\frac{2 P}{\pi \epsilon_0 c w_0^2}}
144
+
145
+ where:
146
+ - `P` is the optical power of the beam,
147
+ - `epsilon_0` is the permittivity of free space,
148
+ - `c` is the speed of light,
149
+ - `w_0` is the beam waist at the focus.
150
+
151
+ Returns
152
+ -------
153
+ Quantity
154
+ The electric field amplitude at the focus in volts per meter.
155
+ """
156
+ self.waist = self.wavelength / (PhysicalConstant.pi * self.numerical_aperture)
157
+
158
+ E0 = np.sqrt(2 * self.optical_power / (PhysicalConstant.pi * PhysicalConstant.epsilon_0 * PhysicalConstant.c * self.waist**2))
159
+
160
+ return E0.to(volt / meter)
161
+
162
+
163
+ @dataclass(config=config_dict)
164
+ class AstigmaticGaussianBeam(BaseBeam):
165
+ """
166
+ Represents an astigmatic Gaussian laser beam focused by a cylindrical lens system.
167
+
168
+ Parameters
169
+ ----------
170
+ optical_power : Quantity
171
+ The optical power of the laser (in watts).
172
+ wavelength : Quantity
173
+ The wavelength of the laser (in meters).
174
+ numerical_aperture_x : Quantity
175
+ The numerical aperture of the lens along the x-axis (unitless).
176
+ numerical_aperture_y : Quantity
177
+ The numerical aperture of the lens along the y-axis (unitless).
178
+ polarization : Optional[Quantity]
179
+ The polarization of the laser source in degrees (default is 0 degrees).
180
+ RIN : Optional[float], optional
181
+ The Relative Intensity Noise (RIN) of the laser, specified as a fractional value (e.g., 0.01 for 1% RIN).
182
+ Default is 0.0, representing a perfectly stable laser.
183
+
184
+ Attributes
185
+ ----------
186
+ waist_x : Quantity
187
+ The beam waist at the focus along the x-axis, calculated as `waist_x = wavelength / (pi * numerical_aperture_x)`.
188
+ waist_y : Quantity
189
+ The beam waist at the focus along the y-axis, calculated as `waist_y = wavelength / (pi * numerical_aperture_y)`.
190
+ amplitude : Quantity
191
+ The electric field amplitude at the focus, derived from optical power, waist_x, waist_y, and fundamental constants.
192
+
193
+ Notes
194
+ -----
195
+ RIN Interpretation:
196
+ - The Relative Intensity Noise (RIN) represents fluctuations in the laser's intensity relative to its mean optical power.
197
+ - For a Gaussian beam, the instantaneous intensity at any given time can be modeled as:
198
+
199
+ I(t) = <I> * (1 + ΔI)
200
+
201
+ where:
202
+ - `<I>` is the mean intensity derived from the optical power.
203
+ - `ΔI` is the noise component, modeled as a Gaussian random variable with:
204
+ - Mean = 0 (fluctuations are around the mean intensity).
205
+ - Variance = RIN * <I>².
206
+
207
+ """
208
+ optical_power: Quantity
209
+ wavelength: Quantity
210
+ numerical_aperture_x: Quantity
211
+ numerical_aperture_y: Quantity
212
+ polarization: Optional[Quantity] = 0 * degree
213
+ RIN: Optional[float] = 0.0
214
+
215
+ def __post_init__(self):
216
+ """
217
+ Initialize additional parameters like beam waist, frequency, photon energy,
218
+ and electric field amplitude at the focus.
219
+ """
220
+ self.initialization()
221
+
222
+ def calculate_field_amplitude_at_focus(self) -> Quantity:
223
+ """
224
+ Calculate the electric field amplitude (E0) at the focus for an astigmatic Gaussian beam.
225
+
226
+ The electric field amplitude at the focus is given by:
227
+
228
+ .. math::
229
+ E_0 = \\sqrt{\\frac{2 P}{\\pi \\epsilon_0 c w_{0x} w_{0y}}}
230
+
231
+ where:
232
+ - `P` is the optical power of the beam,
233
+ - `epsilon_0` is the permittivity of free space,
234
+ - `c` is the speed of light,
235
+ - `w_{0x}` is the beam waist at the focus along the x-axis,
236
+ - `w_{0y}` is the beam waist at the focus along the y-axis.
237
+
238
+ Returns
239
+ -------
240
+ Quantity
241
+ The electric field amplitude at the focus in volts per meter.
242
+ """
243
+ # Calculate waists based on numerical apertures
244
+ self.waist_x = (self.wavelength / (PhysicalConstant.pi * self.numerical_aperture_x))
245
+ self.waist_y = (self.wavelength / (PhysicalConstant.pi * self.numerical_aperture_y))
246
+
247
+ E0 = np.sqrt(2 * self.optical_power / (PhysicalConstant.pi * PhysicalConstant.epsilon_0 * PhysicalConstant.c * self.waist_x * self.waist_y))
248
+
249
+ return E0.to(volt / meter)
FlowCyPy/units.py ADDED
@@ -0,0 +1,30 @@
1
+ # Initialize a unit registry
2
+ from PyMieSim.units import ureg, Quantity # noqa F401
3
+ from PyMieSim.units import * # noqa F401
4
+
5
+ _scaled_units_str_list = [
6
+ 'watt', 'volt', 'meter', 'second', 'liter', 'hertz', 'ohm', 'ampere'
7
+ ]
8
+
9
+ for _units_str in _scaled_units_str_list:
10
+ for scale in ['nano', 'micro', 'milli', '', 'kilo', 'mega']:
11
+ _unit = scale + _units_str
12
+ globals()[_unit] = getattr(ureg, _unit)
13
+
14
+
15
+ joule = ureg.joule
16
+ coulomb = ureg.coulomb
17
+ power = ureg.watt.dimensionality
18
+ kelvin = ureg.kelvin
19
+ celsius = ureg.celsius
20
+ particle = ureg.particle
21
+ degree = ureg.degree
22
+ distance = ureg.meter.dimensionality
23
+ time = ureg.second.dimensionality
24
+ volume = ureg.liter.dimensionality
25
+ frequency = ureg.hertz.dimensionality
26
+ dB = ureg.dB
27
+
28
+ # Define a custom unit 'bit_bins'
29
+ ureg.define("bit_bins = [detector_resolution]")
30
+ bit_bins = ureg.bit_bins