FlowCyPy 0.5.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 (44) hide show
  1. FlowCyPy/__init__.py +15 -0
  2. FlowCyPy/_version.py +16 -0
  3. FlowCyPy/classifier.py +196 -0
  4. FlowCyPy/coupling_mechanism/__init__.py +4 -0
  5. FlowCyPy/coupling_mechanism/empirical.py +47 -0
  6. FlowCyPy/coupling_mechanism/mie.py +205 -0
  7. FlowCyPy/coupling_mechanism/rayleigh.py +115 -0
  8. FlowCyPy/coupling_mechanism/uniform.py +39 -0
  9. FlowCyPy/cytometer.py +198 -0
  10. FlowCyPy/detector.py +616 -0
  11. FlowCyPy/directories.py +36 -0
  12. FlowCyPy/distribution/__init__.py +16 -0
  13. FlowCyPy/distribution/base_class.py +59 -0
  14. FlowCyPy/distribution/delta.py +86 -0
  15. FlowCyPy/distribution/lognormal.py +94 -0
  16. FlowCyPy/distribution/normal.py +95 -0
  17. FlowCyPy/distribution/particle_size_distribution.py +110 -0
  18. FlowCyPy/distribution/uniform.py +96 -0
  19. FlowCyPy/distribution/weibull.py +80 -0
  20. FlowCyPy/event_correlator.py +244 -0
  21. FlowCyPy/flow_cell.py +122 -0
  22. FlowCyPy/helper.py +85 -0
  23. FlowCyPy/logger.py +322 -0
  24. FlowCyPy/noises.py +29 -0
  25. FlowCyPy/particle_count.py +102 -0
  26. FlowCyPy/peak_locator/__init__.py +4 -0
  27. FlowCyPy/peak_locator/base_class.py +163 -0
  28. FlowCyPy/peak_locator/basic.py +108 -0
  29. FlowCyPy/peak_locator/derivative.py +143 -0
  30. FlowCyPy/peak_locator/moving_average.py +114 -0
  31. FlowCyPy/physical_constant.py +19 -0
  32. FlowCyPy/plottings.py +270 -0
  33. FlowCyPy/population.py +239 -0
  34. FlowCyPy/populations_instances.py +49 -0
  35. FlowCyPy/report.py +236 -0
  36. FlowCyPy/scatterer.py +373 -0
  37. FlowCyPy/source.py +249 -0
  38. FlowCyPy/units.py +26 -0
  39. FlowCyPy/utils.py +191 -0
  40. FlowCyPy-0.5.0.dist-info/LICENSE +21 -0
  41. FlowCyPy-0.5.0.dist-info/METADATA +252 -0
  42. FlowCyPy-0.5.0.dist-info/RECORD +44 -0
  43. FlowCyPy-0.5.0.dist-info/WHEEL +5 -0
  44. FlowCyPy-0.5.0.dist-info/top_level.txt +1 -0
FlowCyPy/scatterer.py ADDED
@@ -0,0 +1,373 @@
1
+ from typing import List, Optional, Union
2
+ import matplotlib.pyplot as plt
3
+ from MPSPlots.styles import mps
4
+ import seaborn as sns
5
+ import pandas as pd
6
+ import numpy
7
+ from FlowCyPy.units import Quantity, RIU, particle, liter
8
+ from FlowCyPy.flow_cell import FlowCell
9
+ from FlowCyPy.population import Population
10
+ from FlowCyPy.utils import PropertiesReport
11
+ from FlowCyPy.distribution import Base as BaseDistribution
12
+ from FlowCyPy.logger import ScattererLogger
13
+ from FlowCyPy.particle_count import ParticleCount
14
+ from enum import Enum
15
+ from pint_pandas import PintType, PintArray
16
+
17
+
18
+ class CouplingModel(Enum):
19
+ MIE = 'mie'
20
+ RAYLEIGH = 'rayleigh'
21
+ UNIFORM = 'uniform'
22
+
23
+
24
+ class Scatterer(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
+
46
+ self.flow_cell: FlowCell = None
47
+ self.n_events: int = None
48
+ self.dataframe: pd.DataFrame = None
49
+
50
+ def initialize(self, flow_cell: FlowCell, size_units: str = 'micrometer') -> None:
51
+ """
52
+ Initializes particle size, refractive index, and medium refractive index distributions.
53
+
54
+ Parameters
55
+ ----------
56
+ flow_cell : FlowCell
57
+ An instance of the FlowCell class that describes the flow cell being used.
58
+
59
+ """
60
+ self.flow_cell = flow_cell
61
+
62
+ for population in self.populations:
63
+ population.initialize(flow_cell=self.flow_cell)
64
+ population.dataframe.Size = population.dataframe.Size.pint.to(size_units)
65
+
66
+ if len(self.populations) != 0:
67
+ self.dataframe = pd.concat(
68
+ [population.dataframe for population in self.populations],
69
+ axis=0,
70
+ keys=[population.name for population in self.populations],
71
+ )
72
+ self.dataframe.index.names = ['Population', 'Index']
73
+
74
+ else:
75
+ dtypes = {
76
+ 'Time': PintType('second'), # Time column with seconds unit
77
+ 'Position': PintType('meter'), # Position column with meters unit
78
+ 'Size': PintType('meter'), # Size column with micrometers unit
79
+ 'RefractiveIndex': PintType('meter') # Dimensionless unit for refractive index
80
+ }
81
+
82
+ multi_index = pd.MultiIndex.from_tuples([], names=["Population", "Index"])
83
+
84
+ # Create an empty DataFrame with specified column types and a multi-index
85
+ self.dataframe = pd.DataFrame(
86
+ {col: pd.Series(dtype=dtype) for col, dtype in dtypes.items()},
87
+ index=multi_index
88
+ )
89
+
90
+ self.n_events = len(self.dataframe)
91
+
92
+ def distribute_time_linearly(self, sequential_population: bool = False) -> None:
93
+ """
94
+ Distributes particle arrival times linearly across the total runtime of the flow cell.
95
+
96
+ Optionally randomizes the order of times for all populations to simulate non-sequential particle arrivals.
97
+
98
+ Parameters
99
+ ----------
100
+ sequential_population : bool, optional
101
+ If `True`, organize the order of arrival times across all populations (default is `False`).
102
+
103
+ """
104
+ # Generate linearly spaced time values across the flow cell runtime
105
+ linear_spacing = numpy.linspace(0, self.flow_cell.run_time, self.n_events)
106
+
107
+ # Optionally randomize the linear spacing
108
+ if not sequential_population:
109
+ numpy.random.shuffle(linear_spacing)
110
+
111
+ # Assign the linearly spaced or randomized times to the scatterer DataFrame
112
+ self.dataframe.Time = PintArray(linear_spacing, dtype=self.dataframe.Time.pint.units)
113
+
114
+ def plot(self, ax: Optional[plt.Axes] = None, show: bool = True, alpha: float = 0.8, bandwidth_adjust: float = 1, log_plot: bool = False, color_palette: Optional[Union[str, dict]] = None) -> None:
115
+ """
116
+ Visualizes the joint distribution of scatterer sizes and refractive indices using a Seaborn jointplot.
117
+
118
+ Parameters
119
+ ----------
120
+ ax : matplotlib.axes.Axes, optional
121
+ Existing matplotlib axes to plot on. If `None`, a new figure and axes are created. Default is `None`.
122
+ show : bool, optional
123
+ If `True`, displays the plot after creation. Default is `True`.
124
+ alpha : float, optional
125
+ Transparency level for the scatter plot points, ranging from 0 (fully transparent) to 1 (fully opaque). Default is 0.8.
126
+ bandwidth_adjust : float, optional
127
+ Bandwidth adjustment factor for the kernel density estimate of the marginal distributions. Higher values produce smoother density estimates. Default is 1.
128
+ log_plot : bool, optional
129
+ If `True`, applies a logarithmic scale to both axes of the joint plot and their marginal distributions. Default is `False`.
130
+ color_palette : str or dict, optional
131
+ The color palette to use for the hue in the scatterplot. Can be a seaborn palette name
132
+ (e.g., 'viridis', 'coolwarm') or a dictionary mapping hue levels to specific colors. Default is None.
133
+
134
+ Returns
135
+ -------
136
+ None
137
+ This function does not return any value. It either displays the plot (if `show=True`) or simply creates it for later use.
138
+
139
+ Notes
140
+ -----
141
+ This method resets the index of the internal dataframe and extracts units from the 'Size' column.
142
+ The plot uses the specified matplotlib style (`mps`) for consistent styling.
143
+
144
+ """
145
+ df_reset = self.dataframe.reset_index()
146
+
147
+ if len(df_reset.Time) == 1:
148
+ return
149
+
150
+ x_unit = df_reset['Size'].pint.units
151
+
152
+ with plt.style.context(mps):
153
+ g = sns.jointplot(data=df_reset, x='Size', y='RefractiveIndex',
154
+ hue='Population', palette=color_palette, kind='scatter',
155
+ alpha=alpha, marginal_kws=dict(bw_adjust=bandwidth_adjust)
156
+ )
157
+
158
+ g.ax_joint.set_xlabel(f"Size [{x_unit}]")
159
+
160
+ if log_plot:
161
+ g.ax_joint.set_xscale('log')
162
+ g.ax_joint.set_yscale('log')
163
+ g.ax_marg_x.set_xscale('log')
164
+ g.ax_marg_y.set_yscale('log')
165
+
166
+ plt.tight_layout()
167
+
168
+ if show:
169
+ plt.show()
170
+
171
+ def print_properties(self) -> None:
172
+ """
173
+ Prints specific properties of the Scatterer instance, such as coupling factor and medium refractive index.
174
+
175
+ """
176
+ min_delta_position = abs(self.dataframe['Time'].diff()).min().to_compact()
177
+ mean_delta_position = self.dataframe['Time'].diff().mean().to_compact()
178
+
179
+ _dict = {
180
+ 'coupling factor': self.coupling_model.value,
181
+ 'medium refractive index': self.medium_refractive_index,
182
+ 'minimum time between events': min_delta_position,
183
+ 'average time between events': mean_delta_position
184
+ }
185
+
186
+ super(Scatterer, self).print_properties(**_dict)
187
+
188
+ for population in self.populations:
189
+ population._log_properties()
190
+
191
+ def _log_properties(self) -> None:
192
+ """
193
+ Logs key properties of the Scatterer instance in a formatted table, including its name,
194
+ refractive index, size, concentration, and number of events.
195
+
196
+ The results are displayed in a formatted table using `tabulate` for clarity.
197
+ """
198
+ # Gather properties
199
+ logger = ScattererLogger(self)
200
+
201
+ logger.log_properties(table_format="fancy_grid")
202
+
203
+ def add_population(self, population: Population, particle_count: ParticleCount) -> 'Scatterer':
204
+ """
205
+ Adds a population to the Scatterer instance with the specified attributes.
206
+
207
+ Parameters
208
+ ----------
209
+ name : str
210
+ The name of the population.
211
+ size : BaseDistribution
212
+ The size distribution of the population.
213
+ refractive_index : BaseDistribution
214
+ The refractive index distribution of the population.
215
+ particle_count : ParticleCount
216
+ The concentration or number of particle of the population. Must have the dimensionality of 'particles per liter'.
217
+
218
+ Returns
219
+ -------
220
+ Scatterer
221
+ The Scatterer instance (to support chaining).
222
+
223
+ Raises
224
+ ------
225
+ ValueError
226
+ If the concentration does not have the expected dimensionality.
227
+ """
228
+ population.particle_count = ParticleCount(particle_count)
229
+
230
+ self.populations.append(population)
231
+ return population
232
+
233
+ def _add_population(self, name: str, size: BaseDistribution, refractive_index: BaseDistribution, concentration: Quantity) -> 'Scatterer':
234
+ """
235
+ Adds a population to the Scatterer instance with the specified attributes.
236
+
237
+ Parameters
238
+ ----------
239
+ name : str
240
+ The name of the population.
241
+ size : BaseDistribution
242
+ The size distribution of the population.
243
+ refractive_index : BaseDistribution
244
+ The refractive index distribution of the population.
245
+ concentration : Quantity
246
+ The concentration of the population. Must have the dimensionality of 'particles per liter'.
247
+
248
+ Returns
249
+ -------
250
+ Scatterer
251
+ The Scatterer instance (to support chaining).
252
+
253
+ Raises
254
+ ------
255
+ ValueError
256
+ If the concentration does not have the expected dimensionality.
257
+ """
258
+ if concentration.dimensionality != (particle / liter).dimensionality:
259
+ raise ValueError(
260
+ f"Invalid concentration dimensionality: {concentration.dimensionality}. Expected dimensionality is 'particles per liter' or similar."
261
+ )
262
+
263
+ population = Population(
264
+ name=name,
265
+ size=size,
266
+ refractive_index=refractive_index,
267
+ concentration=concentration
268
+ )
269
+
270
+ self.populations.append(population)
271
+ return population
272
+
273
+ def remove_population(self, name: str) -> 'Scatterer':
274
+ """
275
+ Removes a population from the Scatterer instance by name.
276
+
277
+ Parameters
278
+ ----------
279
+ name : str
280
+ The name of the population to remove.
281
+
282
+ Returns
283
+ -------
284
+ Scatterer
285
+ The Scatterer instance (to support chaining).
286
+
287
+ Raises
288
+ ------
289
+ ValueError
290
+ If the population with the specified name does not exist.
291
+ """
292
+ population_names = [p.name for p in self.populations]
293
+ if name not in population_names:
294
+ raise ValueError(f"Population '{name}' not found in Scatterer.")
295
+
296
+ self.populations = [p for p in self.populations if p.name != name]
297
+ return self
298
+
299
+ def add_to_ax(self, *axes) -> None:
300
+ """
301
+ Adds vertical lines representing events for each population to the provided axes.
302
+
303
+ Parameters
304
+ ----------
305
+ *axes : matplotlib.axes.Axes
306
+ One or more matplotlib axes to which the vertical lines will be added.
307
+
308
+ Returns
309
+ -------
310
+ None
311
+ """
312
+ vlines_color_palette = plt.get_cmap('Set2')
313
+
314
+ for index, (population_name, group) in enumerate(self.dataframe.groupby(level=0)):
315
+ vlines_color = vlines_color_palette(index % 8)
316
+ x = group['Time']
317
+ units = x.max().to_compact().units
318
+ x.pint.values = x.pint.to(units)
319
+ for ax in axes:
320
+ ax.vlines(x=x, ymin=0, ymax=1, transform=ax.get_xaxis_transform(), color=vlines_color, lw=2.5, linestyle='--', label=population_name)
321
+
322
+ ax.set_xlabel(f'Time [{units}]')
323
+
324
+ @property
325
+ def concentrations(self) -> List[Quantity]:
326
+ """
327
+ Gets the concentration of each population in the Scatterer instance.
328
+
329
+ Returns
330
+ -------
331
+ List[Quantity]
332
+ A list of concentrations for each population.
333
+ """
334
+ return [population.concentration for population in self.populations]
335
+
336
+ @concentrations.setter
337
+ def concentrations(self, values: Union[List[Quantity], Quantity]) -> None:
338
+ """
339
+ Sets the concentration of each population in the Scatterer instance.
340
+
341
+ Parameters
342
+ ----------
343
+ values : Union[List[Quantity], Quantity]
344
+ A list of concentrations to set for each population, or a single concentration value to set for all populations.
345
+
346
+ Raises
347
+ ------
348
+ ValueError
349
+ If the length of the values list does not match the number of populations or if any concentration has an incorrect dimensionality.
350
+ """
351
+ if isinstance(values, (list, tuple)):
352
+ if len(values) != len(self.populations):
353
+ raise ValueError("The length of the values list must match the number of populations.")
354
+
355
+ for value in values:
356
+ if value.dimensionality != (particle / liter).dimensionality:
357
+ raise ValueError(
358
+ f"Invalid concentration dimensionality: {value.dimensionality}. Expected dimensionality is 'particles per liter' or similar."
359
+ )
360
+
361
+ for population, value in zip(self.populations, values):
362
+ population.concentration = value
363
+ else:
364
+ if values.dimensionality != (particle / liter).dimensionality:
365
+ raise ValueError(
366
+ f"Invalid concentration dimensionality: {values.dimensionality}. Expected dimensionality is 'particles per liter' or similar."
367
+ )
368
+ for population in self.populations:
369
+ population.concentration = values
370
+
371
+ def dilute(self, factor: float) -> None:
372
+ for population in self.populations:
373
+ population.concentration /= factor
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,26 @@
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