"""
Result components for particle sizer IDSs.
:py:class:`Result` bundles the technique-specific result blocks
(:py:class:`DLSResult`, :py:class:`ELSResult`, :py:class:`SLSResult`,
:py:class:`LaserDiffractionResult`) alongside the size distributions
(:py:class:`SizeDistribution`) that DLS and Laser Diffraction share. A
consuming IDS includes only the blocks relevant to the techniques it
supports.
"""
from enum import Enum
from typing import List
from ts_ids_core.annotations import (
NullableNumber,
NullableString,
UUIDForeignKey,
UUIDPrimaryKey,
)
from ts_ids_core.base.ids_element import IdsElement
from ts_ids_core.base.ids_field import IdsField
from ts_ids_core.schema import RawValueUnit
[docs]
class SizeDistributionWeighting(str, Enum):
"""
Statistical weighting of a size distribution. The same physical
distribution can be expressed weighted by scattered intensity, by
particle volume/mass, or by particle number; the three differ
substantially for polydisperse samples, so the weighting must be
preserved to interpret the distribution correctly.
"""
INTENSITY = "intensity"
VOLUME = "volume"
MASS = "mass"
NUMBER = "number"
[docs]
class PolydispersityIndexDefinition(str, Enum):
"""
Algorithm used to compute the polydispersity index (PDI). The two
definitions are **not** interchangeable, so the definition must be
recorded alongside the value.
* :py:attr:`ISO_22412` -- µ₂/Γ̄² from the cumulants fit (Zetasizer).
* :py:attr:`PERCENT_PD_OVER_100` -- (%Pd/100)² (DynaPro).
"""
ISO_22412 = "iso_22412"
PERCENT_PD_OVER_100 = "percent_pd_over_100"
[docs]
class ResultAnalysisPass(str, Enum):
"""
Zetasizer-specific analysis pass. The Zetasizer reports two ``Result``
entries per measurement, one for each pass. DynaPro and Mastersizer set
this to null.
"""
FILTERED = "filtered"
UNFILTERED = "unfiltered"
# ---------------------------------------------------------------------------
# Size distributions (shared across DLS and Laser Diffraction)
# ---------------------------------------------------------------------------
[docs]
class SizeDistributionPeak(IdsElement):
"""A single peak within a size distribution."""
mean: RawValueUnit = IdsField(description="Mean size of the peak.")
mode: RawValueUnit = IdsField(description="Modal (most frequent) size of the peak.")
standard_deviation: RawValueUnit = IdsField(
description="Standard deviation (width) of the peak."
)
area: RawValueUnit = IdsField(description="Area under the peak.")
radius: RawValueUnit = IdsField(description="Radius corresponding to the peak.")
polydispersity: RawValueUnit = IdsField(description="Polydispersity of the peak.")
molecular_weight: RawValueUnit = IdsField(
description="Molecular weight estimated for the peak."
)
diffusion_coefficient: RawValueUnit = IdsField(
description="Diffusion coefficient corresponding to the peak."
)
percent_intensity: RawValueUnit = IdsField(
description="Contribution of the peak to the intensity-weighted distribution, as a percentage."
)
percent_mass: RawValueUnit = IdsField(
description="Contribution of the peak to the mass/volume-weighted distribution, as a percentage."
)
percent_number: RawValueUnit = IdsField(
description="Contribution of the peak to the number-weighted distribution, as a percentage."
)
[docs]
class SizeDistribution(IdsElement):
"""
A size distribution result for a given weighting, summarized by its
percentile statistics and constituent peaks.
"""
weighting: NullableString = IdsField(
description="Statistical weighting of the distribution.",
json_schema_extra={
"example_values": [w.value for w in SizeDistributionWeighting]
},
)
source: NullableString = IdsField(
description="Analysis/source that produced the distribution, e.g. 'cumulants' or 'distribution analysis'."
)
d10: RawValueUnit = IdsField(
description="10th percentile size: 10% of the distribution lies below this size."
)
d50: RawValueUnit = IdsField(description="50th percentile (median) size.")
d90: RawValueUnit = IdsField(
description="90th percentile size: 90% of the distribution lies below this size."
)
span: RawValueUnit = IdsField(description="Distribution width, (d90 - d10) / d50.")
peaks: List[SizeDistributionPeak] = IdsField(
description="Individual peaks that make up the distribution."
)
# ---------------------------------------------------------------------------
# DLS-specific results
# ---------------------------------------------------------------------------
[docs]
class ZAverage(IdsElement):
"""
Intensity-weighted harmonic mean hydrodynamic size from the cumulants
fit. The Zetasizer reports diameter natively; DynaPro reports radius
natively.
"""
diameter: RawValueUnit = IdsField(description="Z-average hydrodynamic diameter.")
radius: RawValueUnit = IdsField(description="Z-average hydrodynamic radius.")
[docs]
class PolydispersityIndex(IdsElement):
"""
Polydispersity index (PDI) from the cumulants fit: <0.1 indicates a
monodisperse sample, >0.4 a broad or aggregated one. The computation
differs between instruments -- always check ``definition``.
"""
value: NullableNumber = IdsField(description="Numeric PDI value.")
unit: NullableString = IdsField(
description="Unit of the PDI value (typically dimensionless)."
)
definition: NullableString = IdsField(
description="Algorithm used to compute the PDI. The definitions are not interchangeable.",
json_schema_extra={
"example_values": [d.value for d in PolydispersityIndexDefinition]
},
)
[docs]
class CumulantFit(IdsElement):
"""
Cumulant analysis fit parameters from the DLS autocorrelation function
(ACF). Regularized NNLS deconvolution yields amplitude, decay time, and
diffusion coefficient from the fitted ACF decay.
"""
amplitude: RawValueUnit = IdsField(
description="Fitted amplitude (intercept) of the ACF."
)
decay_time: RawValueUnit = IdsField(
description="Characteristic decay time of the ACF."
)
diffusion_coefficient: RawValueUnit = IdsField(
description="Translational diffusion coefficient from the fit."
)
radius: RawValueUnit = IdsField(description="Hydrodynamic radius from the fit.")
molecular_weight: RawValueUnit = IdsField(
description="Molecular weight estimated from the fit."
)
viscosity: RawValueUnit = IdsField(description="Viscosity used in the fit.")
q: RawValueUnit = IdsField(
description="Scattering vector magnitude used in the fit."
)
polydispersity: RawValueUnit = IdsField(description="Polydispersity from the fit.")
polydispersity_index: RawValueUnit = IdsField(
description="Polydispersity index from the fit."
)
polydispersity_percent: RawValueUnit = IdsField(
description="Polydispersity expressed as a percentage."
)
baseline: RawValueUnit = IdsField(description="Fitted baseline of the ACF.")
sos_error: RawValueUnit = IdsField(description="Sum-of-squares error of the fit.")
lambda_: RawValueUnit = IdsField(
alias="lambda",
description="Regularization parameter used in the deconvolution.",
)
sigma: RawValueUnit = IdsField(
description="Standard deviation (width) parameter of the fit."
)
first_cumulant: RawValueUnit = IdsField(
description="First cumulant (mean decay rate)."
)
second_cumulant: RawValueUnit = IdsField(
description="Second cumulant (variance of the decay rate)."
)
measured_baseline: RawValueUnit = IdsField(
description="Measured (as opposed to fitted) baseline."
)
fit_error: RawValueUnit = IdsField(description="Overall error metric of the fit.")
order_of_fit: NullableNumber = IdsField(
description="Order of the cumulant polynomial fit."
)
[docs]
class RegularizedFitPeak(IdsElement):
"""A single peak from regularized NNLS deconvolution of the DLS ACF."""
dist: NullableNumber = IdsField(
description="Distribution index the peak belongs to."
)
peak_index: NullableNumber = IdsField(
description="Index of the peak within the distribution."
)
radius: RawValueUnit = IdsField(description="Hydrodynamic radius of the peak.")
diffusion_coefficient: RawValueUnit = IdsField(
description="Diffusion coefficient of the peak."
)
molecular_weight: RawValueUnit = IdsField(
description="Molecular weight estimated for the peak."
)
polydispersity: RawValueUnit = IdsField(description="Polydispersity of the peak.")
first_index: NullableNumber = IdsField(
description="Index of the first ACF grid point contributing to the peak."
)
number_of_points: NullableNumber = IdsField(
description="Number of ACF grid points contributing to the peak."
)
decay_time: RawValueUnit = IdsField(
description="Characteristic decay time of the peak."
)
percent_intensity: RawValueUnit = IdsField(
description="Contribution of the peak to the intensity-weighted distribution, as a percentage."
)
percent_mass: RawValueUnit = IdsField(
description="Contribution of the peak to the mass/volume-weighted distribution, as a percentage."
)
percent_number: RawValueUnit = IdsField(
description="Contribution of the peak to the number-weighted distribution, as a percentage."
)
[docs]
class DLSAcquisition(IdsElement):
"""A single DLS acquisition within a measurement, with its own fit results."""
index: NullableNumber = IdsField(
description="Index of the acquisition within the measurement."
)
cumulant_fit: CumulantFit = IdsField(
description="Cumulant fit for this acquisition."
)
regularized_fit_peaks: List[RegularizedFitPeak] = IdsField(
description="Peaks from the regularized fit of this acquisition."
)
[docs]
class InstrumentReading(IdsElement):
"""Average instrument reading over all acquisitions in a measurement."""
intensity: RawValueUnit = IdsField(description="Average scattered light intensity.")
temperature: RawValueUnit = IdsField(description="Average measured temperature.")
time: RawValueUnit = IdsField(description="Average acquisition time.")
[docs]
class DerivedAnalysis(IdsElement):
"""
Derived concentration-dependent analysis from DLS (DynaPro SLS
companion), computed across a concentration series.
"""
kd: RawValueUnit = IdsField(
description="Diffusion interaction parameter (k_D) from the concentration series."
)
a2: RawValueUnit = IdsField(
description="Second virial coefficient (A2) from the concentration series."
)
tagg: RawValueUnit = IdsField(
description="Aggregation onset temperature (T_agg), when a thermal ramp is run."
)
[docs]
class DLSResult(IdsElement):
"""Dynamic Light Scattering (DLS) specific results."""
z_average: ZAverage = IdsField(
description=(
"Intensity-weighted harmonic mean hydrodynamic size from the "
"cumulants fit. Zetasizer reports diameter natively; DynaPro "
"reports radius natively."
)
)
polydispersity_index: PolydispersityIndex = IdsField(
description=(
"PDI from the cumulants fit. <0.1 monodisperse, >0.4 "
"broad/aggregated. Definition differs between instruments -- "
"check the ``definition`` field."
)
)
diffusion_coefficient: RawValueUnit = IdsField(
description="Translational diffusion coefficient from the cumulants fit."
)
intercept: RawValueUnit = IdsField(
description="ACF y-intercept at tau=0. Data quality indicator; ideal = 1.0."
)
mean_count_rate: RawValueUnit = IdsField(
description="Photon count rate in kcps. Signal strength indicator."
)
derived_mean_count_rate: RawValueUnit = IdsField(
description="Derived (attenuation-corrected) mean count rate."
)
in_range: RawValueUnit = IdsField(
description="Fraction of the signal within the configured size range."
)
quality_indicator: NullableString = IdsField(
description="Overall data-quality verdict reported by the instrument, e.g. 'good' or a warning message."
)
number_of_runs: NullableNumber = IdsField(
description="Number of sub-runs/acquisitions averaged into this result."
)
run_retention: RawValueUnit = IdsField(
description="Fraction of sub-runs retained after quality filtering."
)
particle_concentration: RawValueUnit = IdsField(
description="Estimated particle concentration."
)
cumulant_fit: CumulantFit = IdsField(
description="Cumulant fit for the averaged measurement."
)
regularized_fit_peaks: List[RegularizedFitPeak] = IdsField(
description="Peaks from the regularized fit of the averaged measurement."
)
acquisitions: List[DLSAcquisition] = IdsField(
description="Per-acquisition results within the measurement."
)
average_instrument_reading: InstrumentReading = IdsField(
description="Average instrument reading over all acquisitions."
)
derived: DerivedAnalysis = IdsField(
description="Derived concentration-dependent analysis (DynaPro SLS companion)."
)
# ---------------------------------------------------------------------------
# ELS-specific results
# ---------------------------------------------------------------------------
[docs]
class ZetaDistributionPeak(IdsElement):
"""A single peak in a zeta potential distribution."""
mean: RawValueUnit = IdsField(description="Mean zeta potential of the peak.")
area: RawValueUnit = IdsField(
description="Area (relative contribution) of the peak."
)
[docs]
class ZetaDistribution(IdsElement):
"""Zeta potential distribution, expressed as a set of peaks."""
peaks: List[ZetaDistributionPeak] = IdsField(
description="Peaks that make up the zeta potential distribution."
)
[docs]
class ELSResult(IdsElement):
"""Electrophoretic Light Scattering (ELS) / zeta potential results."""
potential: RawValueUnit = IdsField(description="Zeta potential.")
zeta_deviation: RawValueUnit = IdsField(
description="Standard deviation (width) of the zeta potential distribution."
)
mobility: RawValueUnit = IdsField(description="Electrophoretic mobility.")
model: NullableString = IdsField(
description="Model used to convert mobility to zeta potential, e.g. 'Smoluchowski' or 'Huckel'."
)
conductivity: RawValueUnit = IdsField(description="Conductivity of the dispersant.")
mean_count_rate: RawValueUnit = IdsField(
description="Photon count rate in kcps during the ELS measurement."
)
quality_factor: RawValueUnit = IdsField(
description="Data quality factor reported for the measurement."
)
effective_voltage: RawValueUnit = IdsField(
description="Effective voltage applied across the cell."
)
measured_voltage: RawValueUnit = IdsField(
description="Measured voltage across the cell."
)
measured_current: RawValueUnit = IdsField(
description="Measured current through the cell."
)
distribution: ZetaDistribution = IdsField(
description="Zeta potential distribution."
)
# ---------------------------------------------------------------------------
# SLS-specific results (DynaPro only, simultaneous with DLS)
# ---------------------------------------------------------------------------
[docs]
class SLSResult(IdsElement):
"""Static Light Scattering (SLS) results (DynaPro only, simultaneous with DLS)."""
molar_mass: RawValueUnit = IdsField(
description="Molar mass from the static scattering intensity."
)
particle_concentration: RawValueUnit = IdsField(
description="Particle concentration used in the SLS calculation."
)
error: NullableString = IdsField(
description="Error/warning message for the SLS calculation, when present."
)
# ---------------------------------------------------------------------------
# Laser Diffraction-specific results
# ---------------------------------------------------------------------------
[docs]
class AdditionalStatistic(IdsElement):
"""An additional Dxx percentile beyond the standard D10/D50/D90."""
name: NullableString = IdsField(
description="Name of the percentile, e.g. 'D[v,0.25]'."
)
value: NullableNumber = IdsField(description="Numeric value of the percentile.")
unit: NullableString = IdsField(description="Unit of the value, e.g. 'Micrometer'.")
[docs]
class LaserDiffractionResult(IdsElement):
"""Laser Diffraction specific results."""
d10: RawValueUnit = IdsField(
description="10th percentile size: 10% of the volume distribution lies below this size."
)
d50: RawValueUnit = IdsField(description="50th percentile (median) size.")
d90: RawValueUnit = IdsField(
description="90th percentile size: 90% of the volume distribution lies below this size."
)
span: RawValueUnit = IdsField(description="Distribution width, (d90 - d10) / d50.")
volume_weighted_mean: RawValueUnit = IdsField(
description="D[4,3] - volume-weighted mean diameter."
)
surface_weighted_mean: RawValueUnit = IdsField(
description="D[3,2] - surface-weighted mean diameter (Sauter mean)."
)
obscuration: RawValueUnit = IdsField(
description="Obscuration (fraction of light attenuated) during the measurement."
)
concentration: RawValueUnit = IdsField(
description="Sample concentration in the measurement zone."
)
weighted_residual: RawValueUnit = IdsField(
description="Weighted residual of the model fit to the diffraction pattern; a goodness-of-fit indicator."
)
uniformity: RawValueUnit = IdsField(
description="Uniformity of the size distribution."
)
specific_surface_area: RawValueUnit = IdsField(
description="Specific surface area computed from the volume distribution."
)
additional_statistics: List[AdditionalStatistic] = IdsField(
description="Additional Dxx percentiles beyond D10/D50/D90."
)
# ---------------------------------------------------------------------------
# Top-level Result
# ---------------------------------------------------------------------------
[docs]
class Result(IdsElement):
"""
A measurement result: the fields common to every technique (size
distributions plus the foreign keys linking it back to its run, method,
sample, and originating curve).
The technique-specific result blocks are *not* included here. As with
:py:class:`Method <ts_ids_components.particle_sizer.method.Method>`, a
vendor-specific IDS composes only the blocks its instrument produces by
inheriting from this base plus the relevant nested mixin classes -- e.g.
``class MyResult(Result, Result.DynamicLightScattering, Result.ElectrophoreticLightScattering)``
for a DLS/ELS instrument.
"""
[docs]
class DynamicLightScattering(IdsElement):
"""Adds the Dynamic Light Scattering result block."""
dynamic_light_scattering: DLSResult = IdsField(
description="Dynamic Light Scattering results."
)
[docs]
class ElectrophoreticLightScattering(IdsElement):
"""Adds the Electrophoretic Light Scattering (zeta potential) result block."""
electrophoretic_light_scattering: ELSResult = IdsField(
description="Electrophoretic Light Scattering / zeta potential results."
)
[docs]
class StaticLightScattering(IdsElement):
"""Adds the Static Light Scattering result block."""
static_light_scattering: SLSResult = IdsField(
description="Static Light Scattering results."
)
[docs]
class LaserDiffraction(IdsElement):
"""Adds the Laser Diffraction result block."""
laser_diffraction: LaserDiffractionResult = IdsField(
description="Laser Diffraction results."
)
pk: UUIDPrimaryKey = IdsField(description="Primary key for this result.")
analysis_pass: NullableString = IdsField(
description=(
"Zetasizer-specific analysis pass. Two ``Result`` entries per "
"measurement (one per pass). DynaPro and Mastersizer set null."
),
json_schema_extra={"example_values": [p.value for p in ResultAnalysisPass]},
)
size_distributions: List[SizeDistribution] = IdsField(
description="Size distributions for this result, one per weighting (shared across DLS and Laser Diffraction)."
)
fk_run: UUIDForeignKey = IdsField(
primary_key="/properties/runs/items/properties/pk",
description="Foreign key to the run that produced this result.",
)
fk_method: UUIDForeignKey = IdsField(
primary_key="/properties/methods/items/properties/pk",
description="Foreign key to the method used for this result.",
)
fk_sample: UUIDForeignKey = IdsField(
primary_key="/properties/samples/items/properties/pk",
description=(
"Foreign key to the sample. Essential for plate-reader "
"instruments where one run produces results for many samples."
),
)
fk_curve: UUIDForeignKey = IdsField(
primary_key="/properties/curves/items/properties/pk",
description=(
"Foreign key to the raw curve (in the ``curves`` array) this "
"result was derived from. See :py:class:`CurveFile "
"<ts_ids_components.particle_sizer.curve.CurveFile>`. When a "
"result has no corresponding curve, point this at a placeholder "
"curve entry so the foreign key is always satisfied."
),
)