Skip to content

feat(chemistry): ingest isotope results (BDMS-1461) - #1002

Open
ksmuczynski wants to merge 10 commits into
stagingfrom
feat/isotope-results-ingest-BDMS-1461
Open

ksmuczynski wants to merge 10 commits into
stagingfrom
feat/isotope-results-ingest-BDMS-1461

Conversation

@ksmuczynski

@ksmuczynski ksmuczynski commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The IsotopeResults tab of the chemistry field spreadsheet now loads into NMA_MinorTraceChemistry, through two new commands, oco water-chemistry isotope-upload and oco water-chemistry isotope-sync-sheet. Each result attaches to a sample the field-sheet or LIMS ingest already recorded.

Why

This PR addresses the following problem / context:

  • The spreadsheet's IsotopeResults tab had no ingest path. The LIMS ingest only reads lab exports from Bonnie's lab, and the field-sheet ingest intentionally skips the IsotopeResults tab.
  • Isotope results come from various labs and arrive weeks or months after the sample info, so they need to load on their own schedule.

How

Implementation summary - the following was changed / added / removed:

  • Added services/chemistry_isotopes.py, which turns each value cell into one NMA_MinorTraceChemistry row and attaches it to its sample by field sample ID.
  • Added the two commands to the CLI. isotope-upload reads a downloaded copy of the spreadsheet, and isotope-sync-sheet reads it from Google Drive. Both take the field-sheet commands' options: dry run, failed rows, loadable copy and the raw-zone flags.
  • Added a warning that names any column on the tab the ingest doesn't read.
  • Added LEGACY_UNITS to services/legacy_chemistry.py.
  • Moved the archive-then-read step into services/ingest_raw_zone.py, and the report header and footer into CLI helpers, so both ingests share them.
  • Added docs/chemistry-isotope-ingest.md, and pointed the field-sheet doc, the LIMS runbook and CLAUDE.md to it.
  • Added docs/chemistry-isotope-ingest-walkthrough.md, a step-by-step guide for whoever runs the ingest.

Notes

Any special considerations, workarounds, or follow-up work to note?

  • The field-sheet ingest behaves and reports exactly as before.
  • Load sample info first. The isotope ingest never creates a sample, and never writes the sample's nma_WCLab_ID, which the LIMS ingest needs empty to adopt a sample.
  • ogc_minor_chemistry_wells shows one value per well and analyte, so where two labs report H2r, only one appears there. That layer also has no tritium column. Both are left for later.
  • Verified. 274 tests pass across the chemistry, raw-zone and CLI files. Against a local copy of production, with the field sheet's sample info loaded, a dry run would load 311 results for 60 samples. Its 36 failed rows are sheet data problems: samples not yet recorded, a missing well (WL-0433), two IDs without a letter, and two analysis dates before collection.

Refs BDMS-1461

The isotope ingest has to archive what it reads before mapping it, the
same way the field sheet does. That step was a private helper tied to
the field sheet's dataset, so the isotope ingest had no way to call it.

The step now lives in ingest_raw_zone and takes the dataset as an
argument. The field sheet behaves exactly as before.

Refs BDMS-1461
The isotope report needs the same top and bottom as the field-sheet
report. Both show the outcome, the database, the source and the
archive, and both list where the failed row workbook and loadable
copy went. Two copies of that code would drift apart.

The field-sheet report now gets both parts from two helpers. Its
output is unchanged.

Refs BDMS-1461
Isotope results use four unit abbreviations that the legacy tables
already hold, pm, pmc, TU and yrs. Nothing in the code said what they
mean, and each ingest spells its units its own way.

The lexicon's unit category doesn't fit. It belongs to the Observation
model, where unit is a foreign key. The NMA_* units columns are plain
text, so a lexicon term there would check nothing. The lexicon also
writes units differently, deg C where the field sheet writes °C.

LEGACY_UNITS goes in services/legacy_chemistry.py instead, beside
LEGACY_ANALYTE_NAMES. A test checks that every unit the isotope
ingest writes is in it, so a new abbreviation can't appear without
its meaning recorded.

Refs BDMS-1461
The IsotopeResults tab of the field spreadsheet had no way into the
database. The LIMS ingest can't read it. It handles one lab's export
and records every result as NMBGMR, while this tab holds results from
NMBGMR, Beta Analytic and the University of Miami Tritium Laboratory,
with qualifiers and uncertainties.

The ingest writes to NMA_MinorTraceChemistry. Legacy isotopes already
live in that table, and /chemistry/results and the OGC views already
read from it.

Choices worth knowing:

- Each result attaches to a sample that already exists, found by its
  field sample ID, SamplePointID. The ingest never creates a sample.
  The tab has no collection date, and the other ingests can't match a
  sample without one.
- Sample, analyte and lab together identify a result. Beta Analytic
  and NMBGMR both report H2r and O18r for 33 samples, and the ingest
  keeps both.
- The ingest skips a result it already stored unchanged, so a re-run
  is safe. A changed result stops the run. It may be a re-analysis,
  and a person decides which value stands.
- The lab's sample id goes on each result, not on the sample. The
  sample has one lab id slot, and the LIMS ingest only adopts a sample
  whose slot is empty.
- The ingest leaves nma_GlobalID and nma_chemistry_sample_info_uuid
  empty. Those columns hold NM_Aquifer's values only.
- Labs come from a fixed list. A misspelled lab would otherwise store
  a duplicate instead of matching.
- The ingest refuses a result analysed before its sample was
  collected.

Like the other chemistry ingests, one failed row stops the whole run
and nothing gets written.

Refs BDMS-1461
Isotope results come back weeks or months after the sample info and
field parameters. Loading the tab with sync-sheet or field-upload
would make the crew's data wait on the slowest lab.

The tab gets its own commands instead. isotope-upload reads a
downloaded copy and isotope-sync-sheet reads Drive. Both take the
field sheet commands' options and name the database in the report.

Refs BDMS-1461
The isotope ingest is new, so each rule gets a test against the test
database:

- each measurement becomes one result, with its qualifier and
  uncertainty
- lab ids are stored as text, without a trailing .0
- each kind of bad row is refused
- three labs on one sample load side by side, each with its own lab id
- the sample record is left alone, and legacy audit columns stay empty
- a re-run skips, and a changed value or qualifier stops the run
- a missing, ambiguous or unknown-well sample fails its row
- a result analysed before collection fails its row
- conflicting rows fail, and a repeated row loads once
- dry run, both reports, archive and replay
- the LIMS ingest still adopts the sample afterwards

The last test guards the lab id rule. If the isotope ingest filled the
sample's lab id, the LIMS ingest would refuse that sample.

The raw_zone fixture moves to conftest, since the field-sheet and
isotope tests both use it now.

Refs BDMS-1461
The new commands get the same checks as field-upload. The report lists
skips and empty rows, names the database, and writes the failed row
workbook and the loadable copy.

Refs BDMS-1461
The field-sheet doc, the LIMS runbook and CLAUDE.md all said the
IsotopeResults tab had no ingest. They now point to a new doc,
docs/chemistry-isotope-ingest.md.

The new doc covers the commands, the column mapping, how results find
their sample, re-runs, failures and known gaps. The biggest gap is the
ogc_minor_chemistry_wells layer, which shows one value per well and
analyte. Where two labs report H2r, only one appears.

Refs BDMS-1461
The isotope ingest reads only the measurement columns it knows. If a
lab starts reporting something new and someone adds a column for it,
those results would never load, and the report gave no sign of it.

The report now names any column on the tab that the ingest doesn't
read, under WARNINGS. The run still loads everything else.

Refs BDMS-1461
Whoever loads isotope results may not be an engineer. The field sheet
has a step-by-step guide for them, but the isotope ingest only had a
reference doc written for whoever maintains the code.

The new guide walks through checking the tab, loading the sample info
first, the practice run, fixing failed rows, the real run, and what to
do when a stored result changes. It links to the field sheet guide for
setup instead of repeating it, so the two can't drift apart. The field
sheet guide and the reference doc now point to it, and the reference
doc lists the unread-column warning among its known gaps.

Refs BDMS-1461
@ksmuczynski ksmuczynski self-assigned this Oct 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Coverage

✅ 81.68% total — gate is 75%.

Coverage for the Python files changed in this PR
Name Stmts Miss Cover Missing
cli/cli.py 639 272 57% 63, 67, 71-75, 89, 310-312, 343, 347, 358-361, 364-366, 400-505, 553-609, 667-668, 704-710, 715-717, 724-725, 770-771, 824, 878-887, 917-919, 998-1051, 1066, 1074-1075, 1137, 1251-1305, 1533-1587, 1662-1664, 1693-1705, 1776-1805, 1893
services/chemistry_field_params.py 360 18 95% 269, 273, 352, 535, 584-585, 661-664, 877-896
services/chemistry_isotopes.py 309 19 94% 212, 222, 330-331, 419, 424, 621, 728, 752-768, 789
services/ingest_raw_zone.py 114 2 98% 170, 229
services/legacy_chemistry.py 20 0 100%
TOTAL 1442 311 78%

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant