Summary
fgumi extract --extract-umis-from-read-names currently hard-codes a single read-name layout: it splits the name on :, requires at least 8 fields, and takes the UMI from the last field. This is the standard Illumina shape (instrument:run:flowcell:lane:tile:x:y:UMI) and nothing else. Any other demultiplexer that encodes UMIs in the read name — with a different field count, more than one UMI field, or a "missing UMI" sentinel — either silently yields no UMI or extracts the wrong one.
This proposes replacing the fixed rule with a small, declarative read-name structure: the read-name analog of fgbio/fgumi read-structures, but the unit is a delimited field rather than a base. The user describes where the UMIs are; extract stays format-agnostic. Named fast-path presets can be layered on later as thin aliases that expand to a structure string, so we get generality now without foreclosing terse shortcuts.
Motivation
The concrete driver is UMI-bearing reads from non-Illumina demultiplexers whose read names place the UMI outside the last field, or use two UMI fields (5′ and 3′), or use a sentinel such as * for an unclassified UMI. For example, a name of the form …:<field>:<umi5p>:<umi3p> carries two trailing UMI fields, each of which may be *. Today extract -n would take only the final field and treat * as literal UMI bases.
We deliberately want extract to encode no vendor-specific format. The layout should be supplied at runtime, so fgumi remains a neutral, general-purpose tool and can be pointed at any read-name convention without baking a specific one into the codebase.
Current behavior (for reference)
src/lib/commands/extract.rs, extract_read_name_and_umi:
- split the name (up to the first space) on
:
- if
parts.len() >= 8, take parts.last() as the UMI; normalize + → -
- otherwise return no UMI
Limitations:
- single UMI only (last field)
- fixed
>= 8 field guard; both under- and over-counting silently produce no UMI
- no notion of a "missing UMI" sentinel
- delimiter is fixed to
:
Proposed solution
A new option that describes the name as an ordered list of delimited-field segments:
--read-name-structure <STRUCTURE> # e.g. '+S 2M'
--read-name-delimiter <CHAR> # default ':'
--read-name-missing <TOKEN> # optional sentinel meaning "field present but UMI unclassified", e.g. '*'
Structure grammar (mirrors read-structures, over fields instead of bases):
- a whitespace-optional sequence of
<count><op> segments
- ops:
S = skip field(s), M = UMI field(s)
- exactly one segment may use
+ instead of a count, meaning "however many fields are left over"; unlike read-structures (where + is always terminal), + may appear anywhere so the fixed part can anchor to the end of the name — this matters because leading fields are the ones that drift or get stripped across pipeline stages, while trailing UMI fields are stable
Semantics:
- split the name (truncated at the first space) on
--read-name-delimiter
- solve the single
+ segment from the actual field count, then map fields to segments left-to-right
- collect
M fields in order; a field equal to --read-name-missing contributes no UMI
- the resulting UMI(s) join with
- into RX, consistent with how seq-derived UMIs are already combined
- if UMI segments are ALSO present in the sequence read-structures, name-UMIs are prepended, then
-, then seq-UMIs — unchanged from today
Mismatch handling: if the field count cannot satisfy the fixed segments, fail with a clear error naming the offending read (fail-fast, matching fgbio ExtractUmisFromBam), rather than the current silent drop. A lenient "skip non-conforming names" mode can be a follow-up if needed.
Examples
Two trailing UMI fields with a * sentinel (skip everything before them):
fgumi extract -i r1.fq.gz -o out.bam \
--read-name-structure '+S 2M' \
--read-name-missing '*'
# name '…:<field>:12:34' -> RX = 12-34
# name '…:<field>:*:34' -> RX = 34
Standard Illumina 8-field name, UMI in the last field:
fgumi extract -i r1.fq.gz -o out.bam --read-name-structure '+S 1M'
Interior UMI (not trailing), e.g. two UMI fields followed by a trailing checksum field:
--read-name-structure '4S 2M +S'
Back-compat
Keep --extract-umis-from-read-names / -n working exactly as it does today (the >= 8 guard + last-field rule). It can either remain a standalone code path or be reimplemented as sugar that preserves the guard. The new --read-name-structure is the general mechanism; the two are mutually exclusive.
Extensibility: fast-path presets (future)
Because a preset is just a structure string plus a delimiter and sentinel, named shortcuts can be added later as pure data with no new parsing logic, e.g.:
--read-name-preset <name> == --read-name-structure '…' --read-name-delimiter … --read-name-missing …
This keeps the door open for terse per-vendor shortcuts without encoding any single format as the primitive.
Open questions
- Missing-UMI representation. Drop missing
M fields from RX (proposed), or retain a placeholder to preserve 5′/3′ positional identity for duplex? Dropping is simpler; retaining preserves strand position. Preference?
- All-missing case. If every
M field equals the sentinel, set no RX (matches today's no-UMI outcome) — agree?
+-anywhere vs terminal-only. Is relaxing read-structures' "+ is terminal" rule acceptable here, or would a dedicated right-anchor syntax be clearer?
- Canonicalization. Leave any UMI reordering (e.g. lexical-min of the two UMIs) to
group/correct downstream (proposed), rather than in extract?
Out of scope
- UMI sequence extraction from bases (already handled by read-structures)
- any vendor-specific format detection or auto-sniffing
- changes to
group/correct/consensus behavior
Summary
fgumi extract --extract-umis-from-read-namescurrently hard-codes a single read-name layout: it splits the name on:, requires at least 8 fields, and takes the UMI from the last field. This is the standard Illumina shape (instrument:run:flowcell:lane:tile:x:y:UMI) and nothing else. Any other demultiplexer that encodes UMIs in the read name — with a different field count, more than one UMI field, or a "missing UMI" sentinel — either silently yields no UMI or extracts the wrong one.This proposes replacing the fixed rule with a small, declarative read-name structure: the read-name analog of fgbio/fgumi read-structures, but the unit is a delimited field rather than a base. The user describes where the UMIs are;
extractstays format-agnostic. Named fast-path presets can be layered on later as thin aliases that expand to a structure string, so we get generality now without foreclosing terse shortcuts.Motivation
The concrete driver is UMI-bearing reads from non-Illumina demultiplexers whose read names place the UMI outside the last field, or use two UMI fields (5′ and 3′), or use a sentinel such as
*for an unclassified UMI. For example, a name of the form…:<field>:<umi5p>:<umi3p>carries two trailing UMI fields, each of which may be*. Todayextract -nwould take only the final field and treat*as literal UMI bases.We deliberately want
extractto encode no vendor-specific format. The layout should be supplied at runtime, so fgumi remains a neutral, general-purpose tool and can be pointed at any read-name convention without baking a specific one into the codebase.Current behavior (for reference)
src/lib/commands/extract.rs,extract_read_name_and_umi::parts.len() >= 8, takeparts.last()as the UMI; normalize+→-Limitations:
>= 8field guard; both under- and over-counting silently produce no UMI:Proposed solution
A new option that describes the name as an ordered list of delimited-field segments:
Structure grammar (mirrors read-structures, over fields instead of bases):
<count><op>segmentsS= skip field(s),M= UMI field(s)+instead of a count, meaning "however many fields are left over"; unlike read-structures (where+is always terminal),+may appear anywhere so the fixed part can anchor to the end of the name — this matters because leading fields are the ones that drift or get stripped across pipeline stages, while trailing UMI fields are stableSemantics:
--read-name-delimiter+segment from the actual field count, then map fields to segments left-to-rightMfields in order; a field equal to--read-name-missingcontributes no UMI-intoRX, consistent with how seq-derived UMIs are already combined-, then seq-UMIs — unchanged from todayMismatch handling: if the field count cannot satisfy the fixed segments, fail with a clear error naming the offending read (fail-fast, matching fgbio
ExtractUmisFromBam), rather than the current silent drop. A lenient "skip non-conforming names" mode can be a follow-up if needed.Examples
Two trailing UMI fields with a
*sentinel (skip everything before them):Standard Illumina 8-field name, UMI in the last field:
Interior UMI (not trailing), e.g. two UMI fields followed by a trailing checksum field:
Back-compat
Keep
--extract-umis-from-read-names/-nworking exactly as it does today (the>= 8guard + last-field rule). It can either remain a standalone code path or be reimplemented as sugar that preserves the guard. The new--read-name-structureis the general mechanism; the two are mutually exclusive.Extensibility: fast-path presets (future)
Because a preset is just a structure string plus a delimiter and sentinel, named shortcuts can be added later as pure data with no new parsing logic, e.g.:
This keeps the door open for terse per-vendor shortcuts without encoding any single format as the primitive.
Open questions
Mfields fromRX(proposed), or retain a placeholder to preserve 5′/3′ positional identity for duplex? Dropping is simpler; retaining preserves strand position. Preference?Mfield equals the sentinel, set noRX(matches today's no-UMI outcome) — agree?+-anywhere vs terminal-only. Is relaxing read-structures' "+is terminal" rule acceptable here, or would a dedicated right-anchor syntax be clearer?group/correctdownstream (proposed), rather than inextract?Out of scope
group/correct/consensus behavior