Annotations and scores¶
FiberHMM writes its calls into the BAM in two forms: the fibertools-style
legacy tags (ns/nl, as/al) that every Fiber-seq tool reads, and the
Molecular-annotation
tags (MA, AQ, AN) that carry every call type with its quality bytes.
This page explains what they mean; BAM tags lists
every tag FiberHMM writes.
Legacy tags¶
| Tag | Content |
|---|---|
ns / nl |
nucleosome starts and lengths (0-based, molecular frame) |
as / al |
MSP starts and lengths |
nq |
one byte per nucleosome (below) |
aq |
one byte per MSP: mean HMM posterior P(accessible) over the MSP ×255 (high = confidently accessible), only from fiberhmm-apply --scores |
TF calls are not in ns/nl by default, only in MA/AQ. For a tool
that reads only the legacy tags, --downstream-compat writes TF calls into
ns/nl next to the nucleosomes (sorted by start; entries shorter than
--unify-threshold, 90 bp, are TFs) and writes no MA/AQ/AN. This loses
the per-TF quality bytes.
MA and AQ¶
MA:Z:<read_length>;nuc.QQQ:s1-l1,s2-l2,...;msp.:s1-l1,...;tf.QQQ:s1-l1,...
AQ:B:C,<bytes of every annotation in MA order>
Each group is name, a strand field (always . here) and a quality
specification: one Q per quality byte each annotation carries. Intervals
are start-length with a 1-based start. AQ concatenates the quality bytes
of every annotation in MA order; groups without Qs contribute none.
The groups fiberhmm-call writes:
| Group | Bytes | Meaning |
|---|---|---|
nuc.QQQ |
nq, el, er |
nucleosomes after nucleosome recall (the default) |
nuc.Q |
nq |
nucleosomes without nucleosome recall (--no-recall-nucs, and fiberhmm-recall-tfs) |
msp. |
none | methylase-sensitive patches |
tf.QQQ |
tq, el, er |
TF / Pol II and other sub-nucleosomal footprints |
Later commands add their own groups (ddda_mcg, ddda_ucg, deam+/deam-,
nuc_sr, tf_sr, tf_consensus, tf_recaller); a re-call keeps groups it
does not regenerate. The header's
MA-TYPES line lists the group names a
BAM may contain.
Quality bytes¶
tq: TF evidence¶
tq is the log-likelihood ratio of the footprint (protected against
accessible) under the emission table. Every 23 tq points are a factor of
ten in likelihood ratio.
tq |
LLR | Likelihood ratio |
|---|---|---|
| 50 | 5 nats | about 150 : 1 (the per-interval cost every preset calls at) |
| 100 | 10 nats | about 22,000 : 1 |
| 255 | ≥ 25.5 nats | saturated |
It is continuous evidence, not a posterior probability or an FDR, and its
scale is specific to the model version. Keep the calls and filter on tq
downstream (for example fiberhmm-extract --min-tq 100).
el, er: edge sharpness¶
The recaller places each edge conservatively, just past the last unmarked target. The true edge could extend up to the next mark. The edge bytes encode that ambiguity:
el = round(255 × max(0, 1 − left_ambiguity_bp / 30))
er = round(255 × max(0, 1 − right_ambiguity_bp / 30))
255 means a mark sits right at the edge (the size is exact); 0 means the
bracketing mark is 30 bp or more away (the size is a lower bound). For
example, el, er ≥ 230 means at most 3 bp of ambiguity at either edge. For
DddA radial nucleosomes, the edge is the posterior median and el/er
encode the width of its central 90% interval on the same 30 bp scale.
nq: nucleosome evidence¶
With nucleosome recall (the default), nq is the LLR ×10 of the retained
configuration, like tq; 0 means unresolved (for example a DddA nucleosome
with no radial dyad, or an unresolved boundary). Without nucleosome recall
it is the HMM posterior mean ×255 when --scores was given, the input's nq
when re-calling, or 0.
For DddA radial recall, nq scores the topology-changing linker
configuration (a split or an inward edge), not the easier question of
protected DNA against open linker, so it does not saturate on every
nucleosome.
Reading the output in Python¶
import pysam
from fiberhmm.io.ma_tags import flip_intervals_to_seq, parse_aq_array, parse_ma_tag
bam = pysam.AlignmentFile("calls.bam", "rb", check_sq=False)
for read in bam:
if not read.has_tag("MA"):
continue
parsed = parse_ma_tag(read.get_tag("MA"))
qual_specs = [group[2] for group in parsed["raw_types"]]
n_per_group = [len(group[3]) for group in parsed["raw_types"]]
per_annotation = parse_aq_array(read.get_tag("AQ"), qual_specs, n_per_group)
# parsed["nuc"], parsed["msp"], parsed["tf"]: [(start, length), ...],
# 0-based, molecular frame.
# per_annotation: one list of quality bytes per annotation, in MA order:
# [nq, el, er] per nuc.QQQ call, [] per msp, [tq, el, er] per tf call.
starts, lengths = zip(*parsed["tf"]) if parsed["tf"] else ((), ())
tf_query = flip_intervals_to_seq(starts, lengths, read) # query frame
The legacy tags hold the same nucleosome and MSP calls:
ns, nl = list(read.get_tag("ns")), list(read.get_tag("nl"))
as_, al = list(read.get_tag("as")), list(read.get_tag("al"))
Circular molecules¶
With fiberhmm-call -r/--circular (plasmids, mitochondrial DNA), each read
is tiled three times internally so features can cross the arbitrary read
origin, and calls are projected back onto the molecule. A feature that wraps
is written as two valid intervals, one at each end of the read, sharing an
AN name:
Tools that ignore AN still see two valid linear intervals. FiberBrowser and
fiberhmm-extract --circular-groups use AN to rebuild the wrapped feature.
Legacy tags are split the same way but carry no shared identity.
fiberhmm-recall-nucs handles linear reads only; for circular molecules use
fiberhmm-call -r.
Stale tags¶
Records a run skips (filtered, outside the called alignment set, no
observations) are written without the previous run's call tags, so a re-call
never leaves an old call next to a new one. DddA island groups (ddda_mcg, ddda_ucg) are
kept.