Aerodynamic Particle Sizer (APS)
The APS is an instrument used for measuring aerodynamic particle size distributions in the micrometer range.
AeroViz.rawDataReader.script.APS.Reader
Bases: AbstractReader
APS (Aerodynamic Particle Sizer) Data Reader
A specialized reader for APS data files, which measure particle size distributions in the range of 542-1981 nm (aerodynamic diameter).
See docs/api/instruments/particle-sizers/APS.md for usage and
docs/guide/reader-reference.md for the
file layout, status codes and QC rules.
Attributes
STATUS_BITS
class-attribute
instance-attribute
STATUS_BITS = {1 << 0: 'Laser fault', 1 << 1: 'Total Flow out of range', 1 << 2: 'Sheath Flow out of range', 1 << 3: 'Excessive sample concentration', 1 << 4: 'Accumulator clipped', 1 << 5: 'Autocal failed', 1 << 6: 'Internal temperature < 10°C', 1 << 7: 'Internal temperature > 40°C', 1 << 8: 'Detector voltage out of range'}
DATE_FORMATS
class-attribute
instance-attribute
Methods:
__call__
Return the dN/dlogDp distribution; write S/V + a stats sidecar.
The parent pipeline produces the QC-applied, resampled dN/dlogDp frame
(diameters in µm as columns) and stamps df.attrs. We then write the
number / surface / volume distributions and a QC-aligned statistics file
next to the main output. Pass append_stats=True to also append the
statistics columns to the returned frame (default keeps it a clean PSD
matrix for psd_stats / merge_psd / SizeDist).
_raw_reader
Read and parse raw APS data files.
Returns all columns from the raw file. Column selection is deferred to _QC() and _process() stages.
Handles files with multiple concatenated headers (when multiple APS export files are merged into one). Header rows are identified and filtered out.
_QC
Perform quality control on APS data.
QC Rules Applied
- Status Error : Non-zero status flags indicate instrument error
- Insufficient : Less than 5 measurements per hour
- Invalid Number Conc : Total number concentration outside valid range (1-700 #/cm³)
_process
Return the QC'd dN/dlogDp size bins (plus QC_Flag).
The size distribution itself is the canonical APS product. Summary
statistics (total per size cut, GMD / GSD / mode) and the surface and
volume distributions are derived quantities — compute them on demand
with :func:AeroViz.psd_stats / :func:AeroViz.psd_distributions
rather than baking them into the reader output. This keeps the reader's
return type a plain dN/dlogDp DataFrame (diameters in µm as columns),
which is exactly what psd_stats / merge_psd / SizeDist consume.
Raw format
- File pattern:
*.txt, tab-delimited - Native frequency:
6minis the config fallback, but the file header carriesSample Time(115 s in the corpus) — not a whole number of minutes. The native grid follows the period detected per file, so scans are not lost to bin collisions;df.attrs['raw_freq']reports what was actually used. - Encoding: opened with
encoding='utf-8', errors='ignore' - Header layout: ~6 metadata lines (
Sample File,Sample Time,Density,Stokes Correction,Lower/Upper Channel Bound) then aSample #header row, found by scanning for that first cell (so files with several concatenated headers still locate it) - Time columns:
DateandStart Time - Size distribution data: columns 3–54 of the export (the under-range
<0.523column plus the 51 bins)
Parse recipe
- Transposed exports (
Sample #as a row rather than a column header) are rotated withset_index('Sample #').T; if that fails the reader raisesNotImplementedErrorwith the original exception attached and logs the first columns it saw. - Date from
Date+Start Time, formats%m/%d/%y %H:%M:%Sthen%m/%d/%Y %H:%M:%S; the winning format is logged, and a file matching neither raises with the list of known formats. - Size bins are the numeric column names in 0.5–20 (µm), converted to float
and rounded to 4 decimal places. Expected grid
(0.542, 19.81, 51 bins)(TSI 3321/3320 factory-fixed); a deviation warns loudly but does not reject — an 8-year × 4-station audit of 1 485 files showed zero drift, so a deviation means a firmware change, and concatenating it with other files would create NaN-poisoned columns. The under-range<0.523column is kept as metadata, not a bin. - The consumed index columns (
Date,Start Time,Sample #,Aerodynamic Diameter) are dropped; rows with an unparseable or duplicated timestamp are dropped; every other source column is kept through L1.
Export units
AIM writes the export unit on every row, in the Aerodynamic Diameter column
(e.g. dN/dlogDp, dN, dM, Raw Counts). Everything after parsing treats
the bins as dN/dlogDp, so each row is checked — on dated rows only, since a
merged export repeats its header mid-file — and converted where the
conversion is exact:
| Row unit | Handling |
|---|---|
dN/dlogDp |
as is |
dN |
÷ dlogDp of the bin (bin_widths of the grid) |
dS/dlogDp, dS |
÷ π·Dp² (µm²/cm³), and ÷ dlogDp for dS |
dV/dlogDp, dV |
÷ (π/6)·Dp³ (µm³/cm³), and ÷ dlogDp for dV |
dM/dlogDp, dM |
× 10³ ÷ (ρ·(π/6)·Dp³) — AIM mass is mg/m³, ρ is the header's Density (g/cm³) |
Raw Counts, %dN, anything else (incl. corrupted cells such as dN/dlggDp) |
row dropped — Raw Counts needs AIM's flow, dead-time and Stokes corrections, a percentage needs the total |
A file mixing units is converted row by row; one with no convertible row left
is skipped. Transposed (*M.txt-style) mass exports parse their bins as
strings and are made numeric before conversion. The mass factor was checked on
11 days of paired TCLab exports (mass vs number of the same scans): ratio
1.000. Skipped files and dropped rows are named in the log and in the
terminal summary.
Measurement parameters
The APS provides aerodynamic particle size distribution measurements:
| Parameter | Value | Description |
|---|---|---|
| Size range | 0.542–19.81 µm (51 bins) | Aerodynamic diameter range |
| Output | dN/dlogDp | Number concentration per size bin |
| Unit | #/cm³ | Particle number concentration |
Status & error codes
Column Status Flags, mode binary_string (see
status modes).
OK is '0000 0000 0000 0000' (16-bit binary, all zeros); any bit still set
after clearing the whitelist mask is an error. A missing Status Flags
column is logged by check_status_columns and leaves the rule inert.
Bit meanings (from the TSI RF command):
- bit
0(0000 0000 0000 0001) — Laser fault - bit
1(0000 0000 0000 0010) — Total Flow out of range - bit
2(0000 0000 0000 0100) — Sheath Flow out of range - bit
3(0000 0000 0000 1000) — Excessive sample concentration - bit
4(0000 0000 0001 0000) — Accumulator clipped (> 65535) - bit
5(0000 0000 0010 0000) — Autocal failed - bit
6(0000 0000 0100 0000) — Internal temperature < 10°C - bit
7(0000 0000 1000 0000) — Internal temperature > 40°C - bit
8(0000 0001 0000 0000) — Detector voltage out of range (±10% Vb) - bit
9(0000 0010 0000 0000) — Reserved (unused)
Status Condition Register
The Status Flags column is a bitfield: the instrument OR-sums every
active condition and reports the sum, so one value can mean several things at
once. Reader.STATUS_BITS carries this table, and it is what turns a raw status
into df.attrs['status_conditions'] — a named condition instead of a number.
| Bit | Decimal | Condition |
|---|---|---|
| 8 | 256 |
Detector voltage out of range |
| 7 | 128 |
Internal temperature > 40°C |
| 6 | 64 |
Internal temperature < 10°C |
| 5 | 32 |
Autocal failed |
| 4 | 16 |
Accumulator clipped |
| 3 | 8 |
Excessive sample concentration |
| 2 | 4 |
Sheath Flow out of range |
| 1 | 2 |
Total Flow out of range |
| 0 | 1 |
Laser fault |
The register is written as a space-grouped bit string
('0000 0000 0000 0001' is bit 0, not the number one thousand). Bit 9 is
reserved. Every non-whitelisted bit counts as an error — this table names
the conditions, it does not decide which of them matter.
To stop treating one condition as an error, whitelist its decimal value:
QC rules
Before the rules run, the total number concentration is computed as
Σ (dN/dlogDp × dlogDp) over the per-bin widths. A natural log was used here
until 2026-09-09 where the data is dN/dlog₁₀Dp, inflating every total by
ln 10 = 2.303×; that mattered more for APS than for SMPS because the range is
tight — a MAX_TOTAL_CONC of 700 was really 304 /cm³, so valid high-loading
scans were being rejected.
| Rule | Condition | Severity |
|---|---|---|
| Status Error | Status Flags has any non-whitelisted bit set (see above) |
error |
| Insufficient | an hour holds < 50 % of the scans it could have held at the detected frequency (edge hours scaled by coverage) | advisory (WARNING) — recorded, data kept |
| Invalid Number Conc | total < 1 or > 700 #/cm³ (MIN_TOTAL_CONC / MAX_TOTAL_CONC); a NaN total is also flagged |
error |
Output
The L2 frame holds the dN/dlogDp matrix only (diameters in µm as
columns) plus the QC bookkeeping; Status Flags and the other metadata
columns are dropped in _process. Statistics are not in the frame.
| Column | Unit | Description |
|---|---|---|
| Size bins (0.542–19.81 µm) | dN/dlogDp | Number concentration for each size |
At L3 the same sidecars as SMPS are written next to the main output (all four
by default; choose a subset with size_dist_outputs=):
{prefix}_dNdlogDp.csv, {prefix}_dSdlogDp.csv (π·d²·dN),
{prefix}_dVdlogDp.csv (π·d³/6·dN) and {prefix}_stats.csv (from
psd_stats, QC-aligned; a failure there is logged and never fails the read).
Pass append_stats=True to also append the statistics columns to the
returned frame; the default keeps it a clean PSD matrix for psd_stats /
merge_psd / SizeDist.
df.attrs['size_dist_unit'] is always 'dN/dlogDp', checked by SizeDist,
psd_stats and merge_psd before they compute (see
Size Distribution);
df.attrs['source_units'] is {export unit: [files]} for the files that held
other units, and absent when none did.
Files written per read are listed in RawDataReader Reference §1.
Notes
- Measures aerodynamic particle diameter directly
- Complementary to SMPS for larger particle sizes
- Size range approximately 0.5–20 μm
- Logarithmic bin spacing in size distribution
- Counting efficiency: the APS under-counts at both ends — 85–99 % for solid particles, but falling from 75 % at 0.8 µm to 25 % at 10 µm for droplets (Volckens & Peters 2005). Not corrected for; see Counting Efficiency.