RawDataReader Reference
What this page is. The single reference for how
RawDataReaderworks under the hood: the pipeline stages and the files they write, the QC machinery, how an instrument's status register is judged, the fullQC_Flagvocabulary, and one table per instrument giving its raw format, status semantics and QC rules.You do not need it to use the reader — start with RawDataReader Usage — and the level model that frames everything here is Data Levels (L0–L3). Come here when a file will not parse, when
Status Errorfires on every row (or never fires), when you want to know exactly what a flag means, or when you are adding a reader or a rule.
1. Pipeline at a glance
RawDataReader(inst, path, start=…, end=…, mean_freq=…, qc=True)
│
▼
┌─ L1 _raw_reader ───────────────────────────────────────────────────┐
│ one parser per instrument, per file · every source column kept │
│ native frequency detected per file · scan groups partitioned │
│ stray dates flagged · rows snapped to the native grid │
└─────────────────────────────────────────────────────────────────────┘
│ _read_{inst}_raw.pkl / .csv ← cache
▼
┌─ L2 _QC → _process ──────────────────────────────────────────────┐
│ QCFlagBuilder evaluates every QCRule → QC_Flag ("Valid" | names) │
│ → QC_Invalid (verdict) │
│ _process derives abs / AAE / eBC / sca_550 / SAE / │
│ Volatile_Fraction, and may add late rules (Invalid AAE) │
└─────────────────────────────────────────────────────────────────────┘
│ _read_{inst}_qc.pkl / .csv ← cache
▼ ─────────────────── cache boundary ───────────────────
┌─ L3 __call__ ──────────────────────────────────────────────────────┐
│ place on the requested range (fill_missing) · apply outlier.json │
│ QC_Invalid rows → NaN · drop QC_Flag + QC_Invalid │
│ _generate_report (rates, timeline) · resample(mean_freq) · _stamp │
└─────────────────────────────────────────────────────────────────────┘
│ output_{inst}.csv · report.json · SMPS/APS sidecars · {inst}.log
▼
returned DataFrame (+ df.attrs)
Everything above the cache boundary is skipped on a cache hit; everything below
it runs on every call, which is why a cached read still honours the current
start / end / fill_missing / mean_freq. The step-by-step version, with
the invariants each step must respect, is in
Data Levels §3.
Files written
{inst}_outputs/ # next to the raw files; output_dir= overrides
# folder and file stems are lower-case (ae33_outputs/output_ae33.csv);
# only the log keeps the reader's case (AE33.log)
├── _read_{inst}_raw.pkl / .csv # L1 — parsed, all columns, native grid
├── _read_{inst}_qc.pkl / .csv # L2 — + QC_Flag, QC_Invalid (flag and value side by side)
├── output_{inst}.csv # L3 — what the call returns
├── output_{inst}_dNdlogDp.csv # SMPS / APS only: the three weightings
├── output_{inst}_dSdlogDp.csv
├── output_{inst}_dVdlogDp.csv
├── output_{inst}_stats.csv # SMPS / APS only: psd_stats output, QC-aligned
├── output_{inst}_element_reliability.csv # Xact only: per-element quantitative / semi / below-detection
├── report.json # rates per period + up/down timeline
└── {inst}.log # parse warnings, dropped files, QC summary
output_prefix= renames the output_{inst} stem; save_pkl,
save_intermediate_csv, save_report switch the respective files off.
What a column goes through
| Stage | AE33 example | APS example |
|---|---|---|
_raw_reader (L1) |
BC1–BC7, ATN, Flow, Status, … | size bins (0.5–20 µm) + metadata |
_QC (L2) |
+ QC_Flag, QC_Invalid |
+ QC_Flag, QC_Invalid |
_process (L2) |
+ abs_370…abs_950, abs_550, AAE, eBC | keeps the size bins only |
__call__ (L3) |
invalid rows → NaN; both QC columns dropped | same |
| returned | BC, abs, AAE, eBC, metadata | dN/dlogDp matrix (statistics in _stats.csv, or appended with append_stats=True) |
_raw_reader deliberately keeps all source columns so instrument metadata
(flow, temperature, RH, pressure, status) survives to L2; the readers that
narrow the frame do so in _process, never at L1.
2. QC machinery
Rules and the builder
Every reader declares its checks as QCRules and hands them to a
QCFlagBuilder (core/qc.py):
QCRule QCFlagBuilder
name "Invalid BC" rules[]
condition df -> bool mask add_rule() / add_rules()
description "0–20 000 ng/m³" apply(df) -> QC_Flag, QC_Invalid
severity 'error'|'warning' get_summary()
apply evaluates every rule as a vectorised mask, writes QC_Flag = "Valid" or
the comma-joined names of every rule that fired, and sets QC_Invalid = True
where at least one error-severity rule fired. A rule that raises is caught,
warned about, and treated as all-False. Nothing is NaN'd or dropped here —
masking is L3's job, on the verdict alone (Data Levels
R2).
_process may add further rules after derived columns exist (Invalid AAE
needs AAE), via update_qc_flag, which appends to an existing non-Valid
flag and raises the verdict if the new rule is invalidating.
The generic filters the rules are built from — n_sigma, iqr,
time_aware_rolling_iqr, time_aware_std_QC, bidirectional_trend_std_QC,
spike_detection, hourly_completeness_QC, filter_error_status — are
documented in the QualityControl API.
Severity — not every flag is fatal
| Severity | Effect at L3 | Ships as |
|---|---|---|
error (default) |
row masked to NaN | every rule not listed below |
warning (advisory) |
flag recorded, measurement kept | Insufficient (every reader that has it), OCEC Below MDL, Xact Upscale Warning, Xact High Uncertainty |
The test is what the flag is about. Insufficient says an hour is thinly
covered — a statement about whether an average over that hour is
representative, not about whether the readings are real. A value below a
detection limit, or a vendor notice the instrument itself classes as a warning,
is a real measurement, and because masking is per row, invalidating it would
delete every other species at that timestamp too.
Reclassify per run without editing a reader:
# tighten an advisory flag back into an invalidating one
RawDataReader('SMPS', path, flag_severity={'Insufficient': 'error'})
# demote one — the closest thing to switching a check off
RawDataReader('AE33', path, flag_severity={'Invalid AAE': 'warning'})
Demoting does not stop the rule running: it still fires and still lands in
QC_Flag; the row just keeps its values. Nor does qc=False: on a fresh parse
_QC and _process run regardless and the L2 cache is written either way —
qc=False only changes what is returned, the parsed L1 frame with no masking,
no outlier.json and no report (mean_freq is still honoured).
A key naming no rule of the instrument being read raises ValueError listing
that instrument's flags, so a typo cannot silently do nothing.
Lifecycle of one flag
_QC QCRule.condition(df) → mask → QC_Flag = "Below MDL, Status Error"
→ QC_Invalid = True (Status Error is 'error';
Below MDL alone leaves it False)
_process update_qc_flag(..., severity=...) → "…, Invalid AAE"
log " Status Error: 24312 (4.9%)" / " Below MDL: 1201 (2.4%) [advisory]"
report yield counts rows that survived the verdict, so an advisory-only row counts
__call__ QC_Invalid → whole row NaN ; QC_Flag + QC_Invalid dropped
The QC summary
AE33 QC Summary:
Status Error: 24312 (4.9%)
Invalid BC: 29265 (5.9%)
Insufficient: 105481 (21.1%) [advisory]
Invalid AAE: 25948 (5.2%)
Valid: 356025 (71.2%)
Usable: 461506 (92.3%)
Valid = passed every rule. Usable = no invalidating rule fired, i.e. what
survives into the output. They differ by exactly the rows carrying only advisory
flags. Rule counts overlap by design — a row can trip several.
Readers with a _process that reshapes the frame (AE33, AE43, BC1054, MA350,
SMPS, APS, Aurora, NEPH) store the summary in _QC and log it from _process
— the four aethalometers after adding Invalid AAE; TEOM, GRIMM, BAM1020,
OCEC, IGAC, Xact and EPA log it inside _QC. The same rows are also stamped into
df.attrs['qc_rules'] as {rule, count, percentage, severity, description},
so a consumer can explain an outage, not just measure it.
Where a judgement is recorded
| Channel | Written by | Granularity | Content |
|---|---|---|---|
QC_Flag column |
_QC / _process (L2) |
per row | "Valid", or every rule that fired, advisory ones included |
QC_Invalid column |
_QC / _process (L2) |
per row | boolean verdict — the only thing masking looks at |
_read_{inst}_qc.csv |
_save_data (L2) |
per row | the flagged frame before masking — flag, verdict and value side by side |
{inst}.log |
all levels | per run | parse warnings, skipped/dropped files, mixed-resolution and stray-date warnings, QC summary, rates |
report.json |
_generate_report (L3) |
per week / month + timeline | rates.weekly / rates.monthly (acquisition / yield / total), timeline of operational and down periods with reasons |
df.attrs |
_stamp (L3) |
per call | provenance, coverage_* vs requested_*, raw_freq, rates, qc_rules, status_conditions |
Both QC columns are dropped from the returned frame and from
output_{inst}.csv. To see the values behind a flag, open
_read_{inst}_qc.csv or call with qc=False.
Rates
All three are computed from the verdict on a 1 h resample; a period counts as valid when more than 50 % of its points pass the verdict (rows carrying only advisory flags count as passing).
| Rate | Definition |
|---|---|
| Acquisition | periods with data / expected periods |
| Yield | periods passing QC / periods with data |
| Total | periods passing QC / expected periods |
report.json always carries a weekly (W-MON) and a monthly (MS) breakdown;
a qc period string ('W', '2MS', …) adds a per-period breakdown to the
log; the overall rates land in
df.attrs['acquisition_rate' / 'yield_rate' / 'total_rate'].
process_timeline_report adds up/down periods and matches them against a
known_issues.yml named by the KNOWN_ISSUES_PATH environment variable.
3. How a status judgement is made
Every status check goes through one function —
QualityControl.filter_error_status — in one of four modes. The mode
decides how the raw value is interpreted and how the ignored_status_errors
whitelist is applied.
| Mode | Raw value looks like | Error when | Instruments | ignored_status_errors entries are… |
|---|---|---|---|---|
bitwise |
integer, OR-summed bits (0, 4, 536870912) |
any non-whitelisted code in ERROR_STATES matches bitwise, or the value equals a special_codes entry |
AE33, AE43, BC1054, MA350, TEOM | integer codes / bits removed from the error definition |
numeric |
flat integer code (0, 4, 16) |
status != ok_value and not NaN |
Aurora, NEPH | numeric codes treated as OK |
text |
free text, possibly comma-joined (Normal Scan, Low aerosol flow,Neutralizer not active) |
any token is neither ok_value nor whitelisted |
SMPS (six columns across two AIM dialects) | string tokens, matched per token |
binary_string |
space-grouped bit string (0000 0000 0000 0000) |
any non-whitelisted bit remains set after clearing the whitelist mask | APS | integer bit masks cleared before testing |
Xact is the exception: its ALARM column is matched by exact code (errors
100–110, warnings 200–203), not through filter_error_status.
Two behaviours to know:
- Empty means OK, always. In
textmode the sentinels'','nan'and'None'are never errors. ('None'arrives when a column is missing from some files of a multi-file concat: pandas fills withNone, whichastype(str)renders as'None'.) - A missing status column reads as OK — but says so.
filter_error_statusreturns all-Falsefor a column it cannot find, which is deliberate (mixed exports must not crash) but used to make a status rule completely inert with no signal. Every status reader now callscheck_status_columnsfirst and logs a warning naming what it looked for and what the file actually contains, so a new export dialect surfaces instead of silently passing every fault.
Entries that don't fit a reader's mode are skipped, so one whitelist can be passed to any reader safely:
RawDataReader('TEOM', path, ignored_status_errors=[536870912]) # bitwise: ignore "Dryer A"
RawDataReader('SMPS', path, ignored_status_errors=['Low aerosol flow']) # text: per token
RawDataReader('APS', path, ignored_status_errors=[1, 2]) # binary_string: clear bits 0,1
RawDataReader('Aurora', path, ignored_status_errors=[4, 16]) # numeric: extra OK codes
Which condition, not just a condition
Status Error is a boolean verdict — it cannot say which bit tripped it. A
reader that declares STATUS_BITS ({decimal: condition_name}) gets the
register decoded into df.attrs['status_conditions'] as
{code, name, count, percentage} per condition that actually fired, busiest
first, so a TEOM status of 8 is reported as Ambient RH & Temp sensor rather
than left as a number.
| Reader | Column | Encoding | Named conditions |
|---|---|---|---|
| AE33 / AE43 | Status |
int |
8 (3 = 1|2 stays unnamed) |
| BC1054 | Status |
int |
12 |
| MA350 | Status |
int |
14 |
| APS | Status Flags |
binary_string |
9 |
| TEOM | status |
int |
29 |
| Aurora / NEPH | Status / status |
numeric |
— flat codes, only 0 (OK) is defined |
| SMPS | six text columns | text |
— free-text tokens, not codes |
Each of these six readers' API pages carries the full table under
#### Status Condition Register, and a test parses it back out and compares
it with STATUS_BITS, so the documented table cannot drift from the code. percentage shares its denominator with qc_rules (the whole frame,
padding included); count is the absolute and the one to trust for a sparse
reader. status_conditions is absent from df.attrs where no table exists
(an incomplete map would read as "that condition never fired") or the status
column is missing, and [] where the register was clean. qc_rules is
likewise absent, not None, when qc=False.
4. QC_Flag vocabulary
Severity is error unless marked advisory.
| Flag | Meaning | Used by |
|---|---|---|
Valid |
passed every rule | all |
Status Error |
instrument status register reports a non-whitelisted condition | AE33, AE43, BC1054, MA350, SMPS, APS, Aurora, NEPH, TEOM |
Insufficient |
an hour holds < 50 % of the points it could have held, given how much of that hour the read covers — advisory | AE33, AE43, BC1054, MA350, SMPS, APS, Aurora, NEPH, TEOM, GRIMM |
Invalid BC |
any BC channel ≤ 0 or > 20 000 ng/m³ | AE33, AE43, BC1054, MA350 |
Invalid AAE |
AAE outside 0.7–3.0 (added in _process) |
AE33, AE43, BC1054, MA350 |
Invalid Number Conc |
total number concentration outside range (SMPS 10–1e6, APS 1–700 #/cm³; the SMPS bounds take min_total_conc= / max_total_conc= per run) |
SMPS, APS |
CPC Over-range |
any bin's dN/dlogDp exceeds what the attached CPC is rated to count, given the recorded sheath ratio — inert (logged) when the file names no known detector model or has no flow columns | SMPS |
DMA Water Ingress |
any bin ≥ 400 nm exceeds 4 000 dN/dlogDp | SMPS |
Truncated Scan |
counts only below 2 × the smallest bin, every bin above exactly zero (DMA ramp never completed) | SMPS |
Invalid Scat Value |
any scattering channel ≤ 0 or > 2 000 Mm⁻¹ | Aurora, NEPH |
Invalid Scat Rel |
B < G < R — inverted wavelength dependence | Aurora, NEPH |
No Data |
every measurement channel NaN | Aurora, NEPH, GRIMM |
High Noise |
TEOM noise ≥ 0.01 |
TEOM |
Non-positive |
PM_NV ≤ 0 or PM_Total ≤ 0 |
TEOM |
NV > Total |
PM_NV > PM_Total (physically impossible) |
TEOM |
Spike |
change > 3 × median absolute change, or a large up-down reversal | TEOM, BAM1020, OCEC |
Negative Conc |
any size channel is negative | GRIMM |
Invalid Conc |
PM ≤ 0 or > 500 µg/m³ | BAM1020 |
Invalid Carbon |
any carbon fraction ≤ −5 or > 100 µgC/m³ | OCEC |
Below MDL |
value at or below the method detection limit — advisory | OCEC |
Missing OC |
Thermal_OC or Optical_OC missing |
OCEC |
Above MR |
concentration above the instrument's stated measurement range | IGAC |
Mass Closure |
Σ aerosol ions > PM2.5 | IGAC |
Missing Main |
any of NH₄⁺ / SO₄²⁻ / NO₃⁻ missing | IGAC |
Ion Balance |
cation/anion ratio outside 1.5 × IQR | IGAC |
Calibration Mode |
SAMPLE_TYPE != 1 |
Xact |
Instrument Error |
ALARM in 100–110 |
Xact |
Upscale Warning |
ALARM in 200–203 — advisory (the instrument's own warning class) |
Xact |
Invalid Value |
element concentration outside 0–100 000 ng/m³ | Xact |
Internal Std Drift |
Nb outside ±20 % of its median | Xact |
High Uncertainty |
a normally-quantitative element reported above its detection limit with < 3σ confidence — advisory | Xact |
Negative |
any numeric column < 0 | EPA |
5. Per-instrument reference
Each instrument has one page that is the single home for its raw format and parse recipe, status column and error codes (with the decoded register table where one exists), QC rules with severities, and output columns:
| Family | Pages |
|---|---|
| Aethalometers / BC | AE33 · AE43 · BC1054 · MA350 |
| Particle sizers | SMPS · APS · GRIMM |
| Nephelometers | Aurora · NEPH |
| Mass | TEOM · BAM1020 |
| Chemistry | OCEC · IGAC · Xact |
| External | EPA |
The Supported Instruments page carries the catalogue and a rule-name table; §7 below is the one-line-per-instrument matrix.
Not readable through RawDataReader
Removed: VOC and Minion
Both were pre-aggregated, second-hand data — somebody else's processed
output, not an instrument's raw log — so there was no raw format for a reader to
parse and the readers only ran generic checks. They were withdrawn from
RawDataReader in v0.4.0; RawDataReader('VOC', …) / ('Minion', …) raise a
KeyError carrying migration advice.
Read them yourself and go straight to the analysis functions:
import pandas as pd
from AeroViz import voc_potentials
df = pd.read_csv('voc.csv', index_col=0, parse_dates=True,
na_values=('-', 'N.D.'))
df.columns = df.columns.str.strip()
out = voc_potentials(df) # validates species against support_voc.json
Minion arrives as a monthly report spreadsheet (read_excel), with a units row
at the top and site-specific invalid markers (維護校正, Nodata, 0L); the
handling depends on who produced that particular report, which is exactly why it
does not belong behind a general-purpose reader.
Q-ACSM — reader not implemented yet
A real instrument, but no reader exists: no sample export has been available
to write and test a parser against. It stays in meta (the 30-min native
frequency is known) and is registered in supported_instruments.pending, so
RawDataReader('Q-ACSM', …) raises a plain
NotImplementedError: Q-ACSM is a real instrument, but its reader is not
implemented yet — no sample export has been available to write and test a parser
against. Contribute one by adding AeroViz/rawDataReader/script/Q-ACSM.py with
_raw_reader and _QC ...
rather than an abstract-class TypeError. Removing the pending entry is all
that is needed once a reader lands.
6. Known limitations
Where the status/QC machinery is still doing less than it appears to:
| Instrument | Symptom | Status |
|---|---|---|
| MA350 | Status column presence never verified against a real export |
open — no fixture. check_status_columns will say so at runtime instead of passing everything |
| GRIMM | no concentration range check | open by choice: no sample corpus to calibrate one against, and a guessed threshold silently deletes good data |
SMPS CPC Over-range |
inert on files whose detector model is not in CPC_MAX_CONC, or that carry no flow columns |
open by design — logged, and cpc_max_conc= enables it per run |
| SMPS / APS counting efficiency | the lowest SMPS channels and both APS ends under-count | not corrected — recorded in Counting Efficiency so the bias is known |
Aurora S2 |
evidently a status bitfield, moves with the zero/span check | not decoded — no manual; kept as data |
7. Summary matrix
| Instrument | Pattern | Native | Status column | Mode | Rules | _process |
Main outputs |
|---|---|---|---|---|---|---|---|
| AE33 | …_AE33*.dat |
1 min | Status |
bitwise | 4 | ✅ | BC1–7, abs, AAE, eBC, Delta-C |
| AE43 | …_AE43*.dat |
1 min | Status |
bitwise | 4 | ✅ | BC1–7, abs, AAE, eBC + metadata |
| BC1054 | *.csv |
1 min | Status |
bitwise | 4 | ✅ | BC1–10, abs, AAE, eBC + metadata |
| MA350 | *.csv |
1 min | Status (unverified) |
bitwise | 4 | ✅ | BC1–5, abs, AAE, eBC + metadata |
| Aurora | *.csv |
1 min | S1 (renamed to Status) |
numeric | 5 | ✅ | sca_550, SAE + B/G/R, BB/BG/BR |
| NEPH | *.dat |
5 min | status (Y row f9) |
numeric | 5 | ✅ | sca_550, SAE + B/G/R, BB/BG/BR |
| SMPS | *.txt, *.csv |
6 min | 6 text columns (AIM 10.3 / 11.x) | text | 5 | ✅ | dN/dlogDp matrix (nm) |
| APS | *.txt |
6 min | Status Flags |
binary_string | 3 | ✅ | dN/dlogDp matrix (µm) |
| GRIMM | *.dat |
6 min | — | — | 3 | ❌ | size-channel concentrations |
| TEOM | *.csv |
6 min | status |
bitwise (32-bit) | 6 | ✅ | PM_Total, PM_NV, Volatile_Fraction |
| BAM1020 | *.csv |
1 h | — | — | 2 | ❌ | Conc (µg/m³) |
| OCEC | *LCRes.csv |
1 h | — | — | 4 | ❌ | thermal / optical OC & EC, OC1–4, PC |
| IGAC | *.csv |
1 h | — | — | 4 | ❌ | up to 14 ion / gas species |
| Xact | *.csv |
1 h | ALARM |
exact code | 6 | ❌ | element concentrations + _uncert |
| EPA | *.csv |
1 h | — | — | 1 | ❌ | air-quality reference data |
| Q-ACSM | *.csv |
30 min | — | — | reader pending | — | — |
Rules counts every QCRule, advisory ones and late ones (Invalid AAE)
included.
Three registry categories (config/supported_instruments.py):
- supported — every row above except Q-ACSM: a module in
script/plus ametaentry. - pending —
Q-ACSM: a real instrument, reader not written yet →NotImplementedError. - removed —
VOC,Minion: pre-aggregated second-hand data, no raw log to parse →KeyErrorwith migration advice, see above.