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.
- FlowCyPy/__init__.py +15 -0
- FlowCyPy/_version.py +16 -0
- FlowCyPy/classifier.py +196 -0
- FlowCyPy/coupling_mechanism/__init__.py +4 -0
- FlowCyPy/coupling_mechanism/empirical.py +47 -0
- FlowCyPy/coupling_mechanism/mie.py +205 -0
- FlowCyPy/coupling_mechanism/rayleigh.py +115 -0
- FlowCyPy/coupling_mechanism/uniform.py +39 -0
- FlowCyPy/cytometer.py +198 -0
- FlowCyPy/detector.py +616 -0
- FlowCyPy/directories.py +36 -0
- FlowCyPy/distribution/__init__.py +16 -0
- FlowCyPy/distribution/base_class.py +59 -0
- FlowCyPy/distribution/delta.py +86 -0
- FlowCyPy/distribution/lognormal.py +94 -0
- FlowCyPy/distribution/normal.py +95 -0
- FlowCyPy/distribution/particle_size_distribution.py +110 -0
- FlowCyPy/distribution/uniform.py +96 -0
- FlowCyPy/distribution/weibull.py +80 -0
- FlowCyPy/event_correlator.py +244 -0
- FlowCyPy/flow_cell.py +122 -0
- FlowCyPy/helper.py +85 -0
- FlowCyPy/logger.py +322 -0
- FlowCyPy/noises.py +29 -0
- FlowCyPy/particle_count.py +102 -0
- FlowCyPy/peak_locator/__init__.py +4 -0
- FlowCyPy/peak_locator/base_class.py +163 -0
- FlowCyPy/peak_locator/basic.py +108 -0
- FlowCyPy/peak_locator/derivative.py +143 -0
- FlowCyPy/peak_locator/moving_average.py +114 -0
- FlowCyPy/physical_constant.py +19 -0
- FlowCyPy/plottings.py +270 -0
- FlowCyPy/population.py +239 -0
- FlowCyPy/populations_instances.py +49 -0
- FlowCyPy/report.py +236 -0
- FlowCyPy/scatterer.py +373 -0
- FlowCyPy/source.py +249 -0
- FlowCyPy/units.py +26 -0
- FlowCyPy/utils.py +191 -0
- FlowCyPy-0.5.0.dist-info/LICENSE +21 -0
- FlowCyPy-0.5.0.dist-info/METADATA +252 -0
- FlowCyPy-0.5.0.dist-info/RECORD +44 -0
- FlowCyPy-0.5.0.dist-info/WHEEL +5 -0
- 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
|