Skip to main content

icpms-qc — the QC check machine for ICP-MS batches

Point it at your batch export. Get back a report that says whether the data can be reported, and why.

pip install .
icpms-qc check my_batch.csv

That is the whole thing. No template to choose, no options to learn — it works out what your file is, reads it, and writes out/qc_report.html for you and out/qc_report.json for your scripts.

  read as: masshunter · masshunter_2row · paired with Count_12052024.csv
icpms-qc FAIL: Conc_12052024.csv
  [         PASS] cal_linearity
  [         FAIL] precision_rsd   - 76 failing row(s)
  [         WARN] instrument_flags - 1128 objection(s) from the instrument software
  [NOT_EVALUATED] ccv_recovery    - no CCV samples in batch
  report: out/qc_report.html

Exit code 0 = everything passed, 2 = QC failures, 1 = error. Drop it in a cron job.

Why

Your instrument software will tell you the CCV recovered at 87%. It will not tell you whether the batch is usable, which samples the failure reaches, or what to check first. Instruments produce numbers. They do not produce assurance. Somebody has to look at the run and say "I have reviewed this, it can be reported" — and in most labs that somebody is one experienced person, one batch at a time.

icpms-qc automates the reviewable part: 21 QC criteria evaluated, with the numbers behind every verdict, in a report you can hand to someone else.

What it reads

Agilent MassHunter batch exports (CSV), single-quad 7700/7800/7900 and triple-quad MS/MS (31 -> 47 P [O2], 8800/8900), collision and reaction cell modes
Thermo Element 2 / XR ASCII exports (.ASC) — point it at the run folder, since the Element writes a file per sample
Count + Conc pairs Conc_0816.csv with Count_0816.csv beside it is paired automatically
Agilent ICP Expert ICP-OES workbooks (.xlsx). Analytes are emission wavelengths rather than masses, and oes_line_agreement asks whether an element's lines agree — a line reading consistently high is a spectral interference, not scatter. Needs pip install 'icpms-qc[oes]'

Tested against 165 real MassHunter exports and 212 real Element files — 99%+ read without being told anything about them. Thermo Qtegra and PerkinElmer Syngistix are on the roadmap.

Something not working?

Pick whichever costs you least:

It could not read my export a short form — instrument, software version, and a fingerprint
A check got it wrong passed something bad, or failed something fine
yzoe236@gmail.com if you would rather not open an issue at all

You never need to send data. icpms-qc inspect yourfile.csv prints a layout fingerprint that is already safe to share — column headers and column kinds, never a single measurement, with free text outside lab vocabulary masked. Paste that.

And if a run produces warnings, the tool hands you a link with the form already filled in from what it just printed. The only thing left to type is your software version:

Something it could not read? One click, form already filled in:
  https://github.com/yzoe236/icpms-qc/issues/new?template=layout.yml&title=…&what=…

Every layout somebody sends is one more lab this works for out of the box. That is genuinely the most useful thing you can contribute.

Who this is for (and who it isn't)

Let's be direct about the landscape, because overselling a QC tool is its own kind of QC failure.

Vendor software already does the calculation well. Agilent's Intelligent Sequence ships templates for EPA 200.8, 6020 and ISO 17294 and evaluates ISTD recovery, calibration linearity, %RSD and blank levels during acquisition. A regulated lab running a validated LIMS (LabWare and its peers) already has batch QC enforcement, control charts, audit trails and scheduled compliance reporting — and changing tools there means re-validation and an audit. If that's you, icpms-qc is not worth your trouble, and you should not switch.

icpms-qc is for the labs in between: a shared instrument facility, a research group, a lab that owns an ICP-MS but no LIMS and no dedicated QC infrastructure — where the review is real work done by a person, and the record of it is a spreadsheet on someone's desktop.

What it gives you there:

Runs after the fact on the export, any time — re-review a two-year-old batch against today's limits
Rules are files you can read YAML rule packs in git; your QC policy becomes reviewable and diffable
Says what it could not check NOT_EVALUATED with a reason, never a silent pass
Shows the numbers every verdict comes with the rows behind it, so a reviewer can disagree
Machine-readable stable JSON sidecar for automation and AI agents
MIT yours to fork, audit and modify

It does not control the instrument, reprocess raw signal, or replace a validated workflow. And it does not certify anything — you remain the person who signs off.

Quick start

pip install .                      # from a clone; PyPI release pending name check
icpms-qc check my_batch_export.csv --rules epa6020b --template masshunter_quant_wide
# → out/qc_report.html  (human)  +  out/qc_report.json  (machines/agents)
# exit code: 0 = all pass, 2 = QC failures, 1 = error  (CI/automation friendly)

📖 docs/USAGE.md — full walkthrough: your own export, reading the report, adapting the rules to your lab, reference materials, automation.

Your export doesn't match a template?

Exports differ by vendor, software version, and report template, so the honest answer to "will it read my file?" used to be "after you write a YAML template." Now:

icpms-qc inspect my_export.csv                            # what does my layout look like?
icpms-qc template-from-header my_export.csv --id my_lab   # draft a template for it
icpms-qc template-from-header my_export.csv --id my_lab --accept

template-from-header extracts a layout fingerprint — column headers, per-column kind, categorical vocabularies — asks a language model to draft the template, then validates the draft by parsing your actual export and reports what it understood and what it did not. Nothing is written until you pass --accept.

Three things this deliberately does not do:

  • It never sends measurements. The fingerprint carries layout, not values; free text outside lab vocabulary is masked, so client sample names stay on your machine. Run icpms-qc inspect to see exactly what would be sent. (--include-names opts out, and tells you it did.)
  • The model never judges QC. It authors a template, once. After you accept it, the layout is a plain YAML file and every future run is deterministic — the same export produces the same report in three years, which is the point of a compliance tool.
  • It never guesses quietly. Ambiguous sample types are left unmapped and reported, because a loud NOT_EVALUATED is worth more than a confident wrong answer.

Needs the claude CLI or ANTHROPIC_API_KEY. Without either, icpms-qc inspect still works — hand the fingerprint to any model, or write the template yourself.

Quick start (continued)

No real data at hand? Generate a synthetic demo batch:

python tools/gen_synthetic_data.py demo_batch.csv
icpms-qc check demo_batch.csv --rules epa6020b
python tools/gen_synthetic_data.py bad_batch.csv --violations   # see a failing report

What v0.1 checks

Calibration linearity and low-standard recovery · ICV/CCV recovery · CCV cadence (every N analyses + end of run) · ICB/CCB and method blanks vs LOQ · detection limits implied by the run's own blank scatter (3σ/10σ, audited against the LOQ your pack declares) · internal-standard drift vs the ICAL reference · LCS recovery · certified reference material recovery, per certified element · duplicate RPD · MS/MSD recovery + RPD · serial-dilution agreement · required-QC presence. Each check reports PASS / FAIL / WARN / NOT_EVALUATED — a check that can't run says so loudly, because silence is how QC reports lie.

Precision is a first-class check, not an afterthought. A run can recover every standard perfectly and still be unusable because the signal would not sit still, and the accuracy checks cannot see that. precision_rsd reads the %RSD your export already carries — gated on signal level, because the RSD of a blank is counting noise rather than a finding. Validating against two real batches found icpms-qc silently reporting nothing while the instrument software itself had raised 1218 objections, all of them precision or calibration; that is the hole this closes.

It can check the number against the counts it came from. quant_crosscheck rebuilds the calibration from the standards in the same export, predicts each sample from its own raw intensities, and compares. The instrument software cannot raise this, because it is what produced the number — it applies the parameters it was given and does not doubt them. A dilution factor typed one digit wrong, an internal standard assigned to the wrong analyte, a stale calibration carried into a new batch: all produce confident, well-formatted, wrong results that every other QC check passes. It reports one thing only, and that restraint is the point. An export does not carry the regression weighting, the curve type, the excluded standards or the interference-correction equations, so individual samples differ for reasons that say nothing about correctness. What survives every one of those unknowns is a ratio: when several analytes are out by the same factor, something scaled the sample — a dilution, a unit, the wrong calibration. One analyte alone at a constant offset is per-mass arithmetic (an interference correction, typically) and is shown rather than blamed.

Counts and concentrations usually live in two files — Count_0816.csv beside Conc_0816.csv. Pass the companion with --counts and they are paired by sequence:

icpms-qc check Conc_0816.csv --counts Count_0816.csv

The instrument's own verdict is carried, not discarded. MassHunter writes its QC objections into the export. instrument_flags parses and reports them alongside icpms-qc's own conclusions — they are the vendor's thresholds, so they are reported rather than decisive (on_flag: fail makes them binding), and where the two disagree, that disagreement is exactly what a reviewer needs to see.

Non-detects are treated as results, not as missing data. <0.05 bounds the truth from above: a blank censored below its threshold passes on merit, one censored above it is reported as undecidable, and a standard that came back non-detect fails on its upper bound. Reading <0.05 as "no value" makes a clean blank and an unmeasured blank produce the same report.

Reference materials

A CRM certifies dozens of elements at dozens of different values, so it does not fit the single Level column an export carries. Certified values live in icpms_qc/configs/crm/*.yaml — one file per material, matched to samples by name, versioned in git next to the rule packs. See icpms_qc/configs/crm/README.md.

unit: ppb
match: { name_patterns: ['(?i)\bSRM\s*1640a\b'] }
certified:
  As: { value: 8.075, uncertainty: 0.070 }

Units are converted, never assumed; the certificate's own uncertainty is reported as context but never used as the pass/fail criterion — that window is a QC policy decision and belongs in the rule pack.

Design in one paragraph

Exports vary wildly across MassHunter versions and report templates, so parsing is template-driven: a small YAML mapping (icpms_qc/configs/*.template.yaml) turns your lab's export layout into a canonical batch model, and the QC engine only ever sees the canonical model. Adding an instrument or a new export layout = contributing a template file, not forking the engine. QC logic itself is a set of small, method-agnostic checks that rule packs (icpms_qc/configs/*.yaml) parameterize with your method's limits.

Status

v0.1 implemented: auto-detecting parser (MassHunter + Thermo Element), 20-check engine, CRM library, HTML + JSON reports, CLI — 131 tests green. The single most useful contribution: a redacted export from your lab (fake sample names, real column layout) + the software version that produced it.

Roadmap

  • v0.2 — batch history store (SQLite) + cross-batch trend charts (instrument drift, ISTD decay)
  • v0.2 — consensus and informational reference values. The CRM library holds certified values today. Geological materials (BHVO-2, BCR-2, NIST 610) also carry consensus and informational values whose provenance matters as much as the number, so a value needs to record which compilation it came from and how much weight it carries.
  • v0.2 — carry the instrument's own reject flag (MassHunter's Rjct column) into the report instead of ignoring it; an auditable tool should not drop the vendor's own verdict
  • v0.3 — Thermo Qtegra export support; PerkinElmer Syngistix export support
  • later — sequence-design linting before you press Run, LIMS-friendly outputs

Prior art

ICPHuntR (R, Lorenz Gfeller) reshapes MassHunter export tables into tidy data frames and ships a table of reference-material values. It stops where icpms-qc starts — it hands you a data frame to write your own analysis against, rather than a verdict and a report — but it hit the same export-side problems first, and three things here are better for having read it: triple-quad mass -> mass labels, collision-cell mode as a first-class field, and detection limits estimated from the post-calibration blanks. Independent tools converging on the same parsing headaches is reasonable evidence the headaches are real.

Contributing

Issues and PRs welcome. Never post client-identifiable data — synthetic or redacted fixtures only (tools/gen_synthetic_data.py shows the reference layout).

License

MIT © 2026 Linhan Li

Metadata

Release files for icpms-qc 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for icpms-qc 0.1.1
File Size Uploaded
icpms_qc-0.1.1.tar.gz 101.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for icpms-qc 0.1.1
File Interpreter ABI Platform
icpms_qc-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 199.0 kB

Release files / icpms_qc-0.1.1.tar.gz

Download URL icpms_qc-0.1.1.tar.gz
Size 101.7 kB
Tags Source
SHA-256 checksum
How to use checksums
b42f4200abec6eb1532339d9c447979b22a6f792672c4c1ba7e053ec99d04fd9
BLAKE2b-256 checksum
How to use checksums
1324a765889c8c53f494ffb43ebb1d0f4476b82a041b2ab6b3e3e570fd7f85c2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 15, 2026.

Transparency log

Release files / icpms_qc-0.1.1-py3-none-any.whl

Download URL icpms_qc-0.1.1-py3-none-any.whl
Size 97.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e62d7dc3c23cc5a1bfc26d1c76bc19e9563a361572a0a8eb93f0d169d7950f2e
BLAKE2b-256 checksum
How to use checksums
4796cad79baf31956a4cf30b3d26c4b37c8cf58279454f25e1ca77a58556ebf7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 15, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page