SACGF / SACGF/variantgrid

Specimen tissue as a UBERON ontology term, matched from LIMS text

Open
#1,747 2 comments 0 reactions 1 assignee Assigned to @davmlaw View on GitHub
Dominant language
Python
Stars
30
Forks
3
Avg merge
9h 28m
Merged PRs (30d)
42

Description

🤖 Written by Claude

Split out of #1706, where the comment "there's currently no way to create tissue, either" turned out to
be bigger than that issue's scope.

`Specimen.tissue` is a FK to a local `Tissue` table (`patients/models.py:315`) that has no rows on any
deployment — no seed data, no importer path (`patients/import_records.py:360` is commented out), no API
field (`SpecimenSerializer` omits it), and no creation path outside the Django admin. So the specimen
page renders a tissue `` that is always empty.

The fix is to stop owning the vocabulary: make it a **UBERON** term in the `ontology` app, imported in
the standard manner alongside the DOID import already queued there, and keep the raw string the LIMS
sent beside the resolved term.

**Why UBERON**

* GA4GH Phenopackets recommends it for `Biosample.sampled_tissue` — the same field.
* CC BY 3.0, so it ships without a licence negotiation. (SNOMED CT's specimen hierarchy is the
LIMS-native vocabulary and arguably a better fit, but it needs an Affiliate licence plus registration
— free for Australia via the NCTS, not redistributable, and each non-Australian deployment would need
its own. The design below lets a deployment that holds SNOMED map its own codes into the same field.)
* OBO/OWL, which `ontology_import` already parses with `pronto`, so the loader is the same shape as
`load_hpo`.
* Covers what accessioning sends: blood `UBERON:0000178`, bone marrow `UBERON:0002371`, buccal mucosa
`UBERON:0006956`, saliva `UBERON:0001836`, skin of body `UBERON:0002097`, plus every organ a solid
tumour block comes from.

**Take what the LIMS gives**

LIMS send `FFPE`, `EDTA blood`, `BM aspirate`, `Buccal` and local codes, and a specimen with an
unrecognised tissue string is still a perfectly good specimen. So the text is the record and the term is
the interpretation: keep `tissue_text` as sent, resolve to a nullable UBERON `tissue_term`, and carry a
match status so an unresolved value reads as unresolved. Two precedents to follow —
`ExtractionMatchMixin` (`patients/models.py:478`), already in this app and already this shape, and
`ConditionText`/`ConditionTextMatch`, which keys normalized text per lab so mapping a string once
resolves every future specimen that carries it.

Preservation and processing (FFPE, fresh frozen, EDTA tube) are a separate axis from anatomy —
phenopackets puts them on `sample_processing`/`sample_type`. Out of scope; those values sit in the raw
text until someone needs that axis modelled.

**Design**: [`claude/plans/1706_remaining_work_plan.md`](https://github.com/SACGF/variantgrid/blob/master/claude/plans/1706_remaining_work_plan.md)
§B — the ontology comparison, the `ontology` app integration points (including keeping UBERON out of
`CONDITION_ONTOLOGIES` and out of `OntologyVersion`), the matching model, and where tissue gets set
(specimen page, patient CSV column, API field). Moving to its own plan doc under this issue's number.

Related: #1707 (the API field), and the DOID import this rides with.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.