gain.genomic_resources.statistics package
Submodules
gain.genomic_resources.statistics.alleles module
Allele-content statistics for allele-score resources.
Vocabulary per CONTEXT.md and ADR 0020. Where the coverage statistic
answers where a score holds data, this one answers what its rows are:
how many alleles a resource carries, how those alleles distribute
over the five allele classes, and what the classes with structure
look like inside – the 4x4 ref->alt matrix for substitutions, the exact
length maps for the two anchored classes, and the complex grid for
the rest.
An allele score counts no covered positions at all (gain#1118). Its
rows collapse to points, so the span union
gain.genomic_resources.statistics.coverage computes never applied
to the kind – which is why it is deliberately absent from that
statistic’s kinds – and the DISTINCT-position count this module used to
keep in its place answered a question the page never asked. What an
allele score’s rows ARE is the whole of what this statistic says.
Raw counts only. Anything needing a denominator is computed at render time, as the coverage statistic’s fractions are.
Laid out like its coverage twin: the per-region accumulator, the
resource-wide statistic, the fold that merges a scan’s regions into one,
and the write. The scan wiring that feeds it is in
implementations/genomic_scores_impl/scan.py.
- gain.genomic_resources.statistics.alleles.ALLELE_INSERTION_LENGTHS_IMAGE_FILE = 'statistics/allele_insertion_lengths.png'
The global images the statistics build renders beside the file. One each, never per chromosome (ADR 0020): the per-chromosome numbers are stored as data and rolled up for the picture.
- class gain.genomic_resources.statistics.alleles.AlleleChromosomeRow(chrom: str, allele_count: int, shares: dict[str, ClassShare] | None)[source]
Bases:
NamedTupleOne chromosome’s allele counts, as the info page renders them.
The chromosome is a FIELD rather than a mapping key, so a row is self-contained exactly as the coverage and fragment rows are: the template reads a row, never a pair it has to keep together.
- allele_count: int
Alias for field number 1
- chrom: str
Alias for field number 0
- shares: dict[str, ClassShare] | None
What this chromosome’s alleles are MADE OF, one entry per class.
Keyed by
CLASS_NAMES, andNonefor a chromosome with no alleles to take a share of – the row then renders empty cells carrying no sort key, exactly as the Coverage table’s row with no resolvable denominator does.percentages_over()owns that rule; the difference is only where it is asked. Coverage resolves a denominator per ROW and so does this, while the classes total that used to sit below the table resolved one for the whole table.
- class gain.genomic_resources.statistics.alleles.AlleleCounts(allele_count: int, class_counts: dict[str, int], substitution_matrix: dict[tuple[str, str], int] | None, insertion_lengths: IndelLengths | None = None, deletion_lengths: IndelLengths | None = None, complex_grid: dict[tuple[int, int], int] | None = None)[source]
Bases:
NamedTupleOne chromosome’s – or a whole resource’s – allele counts.
class_countsis keyed by the class names inCLASS_NAMESand sums toallele_count: every row classifies,otherabsorbing what does not parse as alleles.substitution_matrixis keyed by the ref/alt cells inMATRIX_CELLS, upper-cased, and sums to thesubstitutionclass count: a cell holds the rows whose pair classifies as a substitution of that cell, and nothing else lands in any cell. It isNone– data unknown, distinct from a matrix of zeros – when restored from a file written before the matrix existed.- allele_count: int
Alias for field number 0
- class_counts: dict[str, int]
Alias for field number 1
- complex_grid: dict[tuple[int, int], int] | None
Alias for field number 5
- deletion_lengths: IndelLengths | None
Alias for field number 4
- display() AlleleDisplay | None[source]
This entry’s render payload,
Noneif EVERY group is unknown.One seam for the whole Alleles section, with the groups independently optional inside it: collapsing the payload the moment any single group is missing would hide the groups a file does carry, and the rollout guarantees no particular combination. A genuinely empty group still renders – zeros for the matrix, an empty grid for the complex cells – which is why “unknown” and “empty” are different answers here.
Everything derived is computed here, at render time, and never stored: transitions are the four
A<->G/C<->Tcells and transversions the eight remaining OFF-DIAGONAL cells – never “substitutions minus transitions”, which would silently count the diagonal’s identity rows as transversions.
- insertion_lengths: IndelLengths | None
Alias for field number 3
- substitution_matrix: dict[tuple[str, str], int] | None
Alias for field number 2
- class gain.genomic_resources.statistics.alleles.AlleleDisplay(substitution_matrix: dict[tuple[str, str], int] | None, transitions: int | None, transversions: int | None, ts_tv: float | None, substitution_percentages: dict[tuple[str, str], str] | None, insertion_lengths: IndelLengths | None = None, deletion_lengths: IndelLengths | None = None, complex_grid: dict[tuple[int, int], int] | None = None)[source]
Bases:
NamedTupleThe Alleles section’s render payload, one field per stored group.
Raw cells come from the stored statistic; the derived numbers are computed by
AlleleCounts.display()– which builds this – and never stored, as the coverage display derives its fractions.Every group is INDEPENDENTLY optional, because the statistics roll out lazily and a resource may have been rebuilt under any one of them: a file written between gain#778 and gain#779 carries a matrix and no lengths, and must render its matrix rather than losing the whole section. Each of the page’s sections therefore asks for its own group, and this payload exists at all whenever ANY group is known.
- complex_grid: dict[tuple[int, int], int] | None
Alias for field number 7
- property complex_grid_renders_as_table: bool
Whether the complex cells render as a table, not a heatmap.
Falsewithout a grid, which the page never asks: it gates on the grid itself first, as it does for the matrix.
- complex_rows() list[tuple[str, str, int, str]][source]
The occupied complex cells as table rows, most alleles first.
A row is reference length, alternative length, alleles and the share of the complex class – the lengths labelled as the heatmap’s axes label them, so a clamped cell reads
≥64in both. Empty without a grid; the page gates on the grid itself rather than reading this to find out.The shares come from
percentages_over(), the one rule the Alleles section writes a share by, so a rare cell reads<0.01%and a cell that is all but the whole class reads>99.99%, here exactly as they do in the classes column. Its denominator is the grid’s own total, which the TOTAL clamp makes exactly the complex class count: every complex row lands in one cell, so these SHARES sum to 100% – the rendered strings, as in the matrix, round off it.That denominator is zero only when no cell is occupied, and then there are no rows to carry a share anyway – so the helper’s “no percentage at all” answer and this method’s empty result are the same answer, and it is returned as one.
- deletion_lengths: IndelLengths | None
Alias for field number 6
- indel_rows() list[IndelStatisticsRow][source]
The indel statistics table: one row per known group.
Global only. Per-chromosome indel statistics would add eight columns to the Alleles table to answer a question nobody asks; the comparison these exist for is between the two GROUPS – is the deletion tail longer than the insertion tail – and that wants them side by side.
A group the file does not carry is left out rather than shown empty; the page says “not computed” for it, as it does for every other unknown group.
- insertion_lengths: IndelLengths | None
The three gain#779 groups,
Nonewhen the file predates them. An empty grid is KNOWN and empty, which is not the same thing.
- matrix_rows() list[tuple[str, list[MatrixCell]]][source]
The matrix as table rows: reference base, then a cell per alt.
Empty without a matrix; the page gates on the matrix itself rather than reading this to find out. This only pairs each stored count with the share
AlleleCounts.display()already computed for it – nothing is derived here.
- property nucleotides: tuple[str, ...]
The matrix’s axis labels, in stored order.
- substitution_matrix: dict[tuple[str, str], int] | None
Nonewhen the file predates the matrix (gain#778).
- substitution_percentages: dict[tuple[str, str], str] | None
Each cell’s share of the substitution class, formatted by
percentages_over(). The denominator is read off the matrix rather than taken fromclass_counts: the two are equal byAlleleCounts’s invariant, and dividing the cells by their own total is what makes the sixteen SHARES come to exactly 100%. The sixteen rendered strings do not: two decimals round independently, and the floor and ceiling round further still.Nonewith no matrix, and with no substitutions to take a share of.
- transitions: int | None
The four
A<->G/C<->Tcells;Nonewith no matrix.
- transversions: int | None
The eight off-diagonal cells that are not transitions; the diagonal’s identity pairs are neither.
Nonewith no matrix.
- ts_tv: float | None
Nonewhen there are no transversions – the template renders “not applicable” rather than dividing – and with no matrix.
- class gain.genomic_resources.statistics.alleles.AlleleSectionDisplay(rows: list[AlleleChromosomeRow], class_counts: dict[str, int], class_percentages: dict[str, str] | None, detail: AlleleDisplay | None)[source]
Bases:
NamedTupleThe Alleles section’s render payload: the table and its totals.
Built in the implementation layer, as
CoverageDisplayandFragmentDisplayare, so the template renders fields off an inert record rather than calling methods on the statistic itself.- property allele_count: int
The table’s total, summed off the rows it shows.
Derived rather than stored, as
CoverageDisplay. global_coveredandFragmentDisplay.global_fragmentsare: the counts have one source, so a total cannot drift from the rows under it.
- class_counts: dict[str, int]
Alias for field number 1
- property class_names: tuple[str, ...]
The class columns, in the order ADR 0020 states them.
Reached for THROUGH the payload rather than as a template global, because the template layer deliberately does not import
gain.genomic_resources– which is whynatural_chromosome_keylives ingain.utilsinstead. The constant itself is what this returns; the property is the seam that keeps the import out of the template environment.
- class_percentages: dict[str, str] | None
Each class name’s share of
allele_count, orNone.Computed HERE rather than on
AlleleDisplaybecause the two numbers a share needs – the class counts and the allele total – are stored fields that every file carries, while that payload collapses when every OPTIONAL group is unknown. Computing them there dropped this column for a file written before the matrix (gain#777) whose shares were perfectly resolvable.Nonewhen there are no alleles to take a share of, which drops the column;percentages_over()owns that rule.
- detail: AlleleDisplay | None
matrix, indel lengths, complex grid.
Nonewhen the file carries none of them; each group inside it stays independently optional, asAlleleDisplayexplains.- Type:
The optional groups
- rows: list[AlleleChromosomeRow]
Alias for field number 0
- class gain.genomic_resources.statistics.alleles.AlleleStatistics[source]
Bases:
RegionFoldedStatistic[RegionAlleles]A resource’s allele content, per chromosome and global.
Folds
RegionAllelesthe way the base class does, and serializes to the resource’sALLELE_STATISTICS_FILEas raw counts.- by_chromosome() dict[str, AlleleCounts][source]
The per-chromosome counts, in natural chromosome order.
Ordered here rather than downstream because this order REACHES the info page:
build_allele_section_display()turns these entries into the Alleles table’s rows as they come, and does not re-sort them the way its coverage and fragment siblings sort theirs. What it replaces is not arrival order but the plain string sortregions_in_genomic_order()applies before the fold – exactly the order iossifovlab/gain#983 calls wrong.serialize()reads it too, so the order reachesstatistics/alleles.json. Nothing downstream reads that file positionally:deserialize()and_total()are both order-blind, andcalc_statistics_hashhashes the config and the source files, never the statistics content.
- static deserialize(content: str) AlleleStatistics[source]
Create a statistic from serialized data.
- global_counts() AlleleCounts[source]
The roll-up over every chromosome.
Off the regions directly rather than through
by_chromosome():_totalis order-blind, so the ordering that accessor does would be paid and thrown away.
- serialize() str[source]
Return a serialized version of this statistic.
- gain.genomic_resources.statistics.alleles.CLASS_NAMES: tuple[str, ...] = ('substitution', 'insertion', 'deletion', 'complex', 'other')
The five class names, in the order ADR 0020 states them – which is the order
AlleleClassdeclares them in, so this reads that order rather than restating it. The serialized class map is written in it, so two builds of one resource produce byte-identical JSON however the rows arrived.
- gain.genomic_resources.statistics.alleles.COMPLEX_GRID_TABLE_MAX_CELLS = 32
How many occupied cells the complex grid may hold and still render as a table of those cells rather than as the heatmap (gain#989).
A judgement call, not a derivation: 32 rows is about where a table stops being scannable, and a 64x64 grid starts having enough lit cells to show shape. Unlike
COMPLEX_LENGTH_CLAMPthis is a RENDERING choice and no part of the stored format.RAISING it is free: a resource built under the old value keeps a PNG its page no longer references, which is the leftover image
save_allele_statistics()already documents. LOWERING it needs the resources rebuilt with--force: a grid between the two values would start asking for a PNG that was never written, and a plainrepo-statswill not notice – the statistics hash covers the table config, the score definitions and the data files, none of which a constant here moves.
- gain.genomic_resources.statistics.alleles.COMPLEX_LENGTH_CLAMP = 64
The longest allele length the complex grid resolves exactly. A length at or above it lands in the grid’s top row or column, which therefore reads “this many bases or more”. The clamp is TOTAL – every complex row lands in exactly one cell, so the grid’s total is the complex class count and no overflow counter is needed.
Exact lengths rather than the shared log2 ladder (gain#779): that ladder’s first bin is exactly length 1, which no complex pair can have – a 1->1 pair is a substitution – and its second bin is {2, 3}, which would put a 2->3 complex in the same cell as a 3bp MNV and empty the diagonal of its meaning. Part of the stored format: it must not change once resources carry grids built from it.
- class gain.genomic_resources.statistics.alleles.ClassShare(alleles: int, percentage: str, fraction: float)[source]
Bases:
NamedTupleOne class’s slice of one chromosome’s alleles.
Three renderings of one count, because the cell shows one, sorts on another and titles itself with the third:
percentageis the text,fractionis the sort key – the NUMBER, so that a column sorts by size rather than by the string<0.01%– andallelesis the exact count the hover title carries.The count is what the global classes table used to show as a column of its own (gain#1118 removed it). Keeping it here is what lets a reader recover the exact number the share rounds off, per chromosome rather than only for the resource.
- alleles: int
Alias for field number 0
- fraction: float
Alias for field number 2
- percentage: str
Alias for field number 1
- gain.genomic_resources.statistics.alleles.MATRIX_CELLS: tuple[tuple[str, str], ...] = (('A', 'A'), ('A', 'C'), ('A', 'G'), ('A', 'T'), ('C', 'A'), ('C', 'C'), ('C', 'G'), ('C', 'T'), ('G', 'A'), ('G', 'C'), ('G', 'G'), ('G', 'T'), ('T', 'A'), ('T', 'C'), ('T', 'G'), ('T', 'T'))
The sixteen ref->alt cells of the substitution matrix, row-major in
NUCLEOTIDESorder. The identity pairs are cells like any other: ADR 0020 classifiesA>Aas a substitution.
- class gain.genomic_resources.statistics.alleles.MatrixCell(alleles: int, percentage: str | None)[source]
Bases:
NamedTupleOne substitution-matrix cell as the page renders it.
allelesis the stored count – spelled as the column it sits under rather thancount, which atuplealready means something else by. The percentage is its share of the substitution class,Nonewhen no denominator resolves, which the page renders as no second line rather than as0.00%.- alleles: int
Alias for field number 0
- percentage: str | None
Alias for field number 1
- gain.genomic_resources.statistics.alleles.NUCLEOTIDES: tuple[str, ...] = ('A', 'C', 'G', 'T')
The nucleotides an allele may be written with, in the order the classifier’s alphabet states them. The substitution matrix’s cells are keyed and serialized in this one order, so two builds of one resource produce byte-identical JSON however the rows arrived.
- class gain.genomic_resources.statistics.alleles.RegionAlleles(chrom: str, start: int | None, end: int | None)[source]
Bases:
objectThe allele content of one scanned region, accumulated row by row.
Counts each ROW as an allele – duplicate
(chrom, pos, ref, alt)rows are legitimate per-transcript data and each is one allele.A region owns the rows whose point falls inside it, which is what makes the statistic chunk-invariant: the regions of a contig partition it, so a position carries rows in exactly one of them and no merge can double-count it. A row’s optional
pos_endtakes no part – an allele’s value stands for its ref/alt pair, not for the bases such a column may reach over – so ownership is the sharedclip_span()asked about the point, and gain#636’s edge is answered there rather than again here.- add_allele(pos: int, ref: str | None, alt: str | None) None[source]
Fold one row, read as the point at
pos.
- add_allele_batch(pos_begin: ndarray, reference: ndarray, alternative: ndarray) None[source]
Fold a batch of column arrays, the counting vectorized.
The same rule
add_allele()applies row by row. Ownership is vectorized outright; the classification cannot be – a class is a property of one ref/alt PAIR, and an array statement of it would be a second spelling ofclassify_allele()– so instead each DISTINCT pair in the batch is classified once and its multiplicity added. Same function, same answer, called once per pair rather than once per row: a real allele score is overwhelmingly substitutions, so a 100,000-row batch usually holds a handful of distinct pairs, and this is ~7x the row-by-row fold over whole-genome data. (A batch of entirely distinct pairs pays a small tally overhead instead.)The nucleotide arrays are RAW, as
AlleleScore.fetch_region_allele_arrays()yields them, so this path hands the classifier exactly the strings the per-record path does.
- add_record(record: tuple[Any, ...]) None[source]
Fold one raw record.
The nucleotides come off the RAW record: the region-values transform an allele score applies collapses a row to the point it sits at and drops them (
region_values_from_records).
- counts() AlleleCounts[source]
This region’s counts, class map keyed by class name.
- classmethod frozen(chrom: str, allele_count: int, class_counts: dict[str, int], *, substitution_matrix: dict[tuple[str, str], int] | None = None, insertion_lengths: IndelLengths | None = None, deletion_lengths: IndelLengths | None = None, complex_grid: dict[tuple[int, int], int] | None = None) RegionAlleles[source]
A region restored from serialized counts, with no scan state.
substitution_matrixisNonefor a file written before the matrix existed – data unknown, not a matrix of zeros.
- merge(other: RegionAlleles) None[source]
Fold the adjacent region to the right into this one.
It is the adjacency – asserted by
refuse_unmergeable– that lets the counts simply add: a row belongs to exactly one of two adjacent regions, so none is counted twice.
- gain.genomic_resources.statistics.alleles.allele_arrays_folded_into(score: AlleleScore, chrom: str, start: int, end: int, score_ids: list[str], *, batch_size: int, alleles: RegionAlleles) Generator[tuple[ndarray, ndarray, dict[str, ndarray]], None, None][source]
The bulk read, nucleotides folded off it, validated as usual.
The array twin of
records_folded_into(), and the same shape: a transducer that folds each batch and yields it onward. What it yields is the batch’s[:3]slice – a plainRecordArrays– because the scan’s array door (validate_record_arrays(), ADR 0008) unpacks three names and raises on the five anAlleleRecordArrayscarries.Folding on the way IN is what lets the nucleotides reach this statistic without the door having to carry them: nothing downstream ever sees the widened batch, so nothing has to pair the two back up.
That the fold precedes the door’s verdict is unobservable: a region the door refuses raises out of the scan, and its accumulator is discarded with the failed task rather than merged.
- gain.genomic_resources.statistics.alleles.build_allele_section_display(statistics: AlleleStatistics) AlleleSectionDisplay[source]
Turn the stored allele counts into the section’s payload.
The rows are built over
AlleleStatistics.by_chromosome()rather than replacing it: that accessor already orders the chromosomes naturally (gain#983) andserializereads it too, so the ordering has one owner.One walk of the regions serves both the rows and the roll-up, as
AlleleStatistics.serialize()does it:_totalis order-blind, so folding the ordered entries it already has costs nothing overAlleleStatistics.global_counts(), which would rebuild every entry a second time.
- gain.genomic_resources.statistics.alleles.merge_region_alleles(resource_id: str, regions: Iterable[RegionAlleles | None]) AlleleStatistics | None[source]
Fold the regions’ counts, or
Nonefor a kind that has none.
- gain.genomic_resources.statistics.alleles.percentages_over(counts: Mapping[K, int], total: int) dict[K, str] | None[source]
Each count as a percentage of
total,Nonewithout one.The one place the ALLELES section writes a share of a count, so the classes column, the substitution matrix’s cells and gain#989’s complex table all say the same thing the same way. How each cell is written – the floor at
<0.01%, the ceiling at>99.99%, and the two exact answers neither may swallow – ispercentage_of(), shared with the Coverage table on the same page (gain#1057).What this adds is the MAP contract around a missing denominator: a zero total has no percentage, and the answer is
Nonefor the WHOLE map rather than per cell, because the denominator is a property of the table. The page then drops the column instead of printing a row of nothing. Coverage resolves a denominator per row and so degrades one row at a time – the same rule per cell, a different answer to not having one.
- gain.genomic_resources.statistics.alleles.plot_complex_grid(outfile: IO, grid: dict[tuple[int, int], int]) None[source]
Render the complex
(len_ref, len_alt)cells as a heatmap.Drawn over the FULL clamped square rather than only the occupied cells, so the diagonal – where the MNVs sit – is visible as a diagonal and an empty region reads as empty rather than as a missing axis. The axes are exact lengths, which is the whole point of the cell scheme (gain#779): a 2->3 complex sits one cell off the diagonal from a 3bp MNV, and a binned axis would have hidden that.
Counts span orders of magnitude on real scores, so the COLOUR is log-scaled – the same choice the length histograms make on their count axis – with empty cells left as the background rather than coloured as a genuine zero.
- gain.genomic_resources.statistics.alleles.records_folded_into(records: Iterator[tuple[Any, ...]], alleles: RegionAlleles) Generator[tuple[Any, ...], None, None][source]
Yield a record stream through, folding each row into
alleles.A transducer over the very stream the per-record scan is already pulling: this statistic rides that one read rather than re-reading the region, exactly as the coverage statistic rides it.
- gain.genomic_resources.statistics.alleles.region_alleles_for(score: GenomicScore, chrom: str, start: int | None, end: int | None) RegionAlleles | None[source]
A region accumulator for an allele score,
Nonefor other kinds.Gated on the BUILT SCORE CLASS rather than the resource type string.
Until 2026.8.5 that was load-bearing:
allele_scoreand the deprecatednp_scoreboth built anAlleleScorewhileequivalent_resource_typesaliased neither to the other, so a gate written on type strings skippednp_scoresilently (gain#777).np_scoreis gone (gain#920) and only one spelling reaches here now, but the class gate stays: it is the property this statistic actually depends on – that the score reads alleles – and a type-string gate would have to be revisited by the next spelling that builds anAlleleScore.
- gain.genomic_resources.statistics.alleles.save_allele_statistics(resource: GenomicResource, statistics: AlleleStatistics | None) None[source]
Write the statistics into the resource, with their global images.
Laid out like
save_and_plot_coverage: the file first, then one image per group that has something to draw. A group the resource publishes nothing for writes no image – the info page’s section is what says whether that is “not computed” or “genuinely none”.An EMPTY group is skipped just as an unknown one is, in both twins. Every group here applies to every allele score, so plotting the empty ones would put an all-zero deletion histogram on each of the many scores that carry only substitutions – and a logarithmic count axis cannot draw one at all. What skipping costs is a previous build’s image left behind when a group empties out, and nothing links the leftover: the page reads the stored counts, not the directory.
- gain.genomic_resources.statistics.alleles.serves_allele_arrays(score: GenomicScore, score_ids: list[str]) bool[source]
Whether the bulk read can hand this score’s rows their nucleotides.
Asked BEFORE a path is chosen rather than caught after: a region whose backend will not serve the ref/alt arrays must fall back to the per-record scan, which reads the nucleotides off the record, rather than produce a statistic with no class data.
Ask it of an OPEN score. On an unopened one the answer is merely conservative – a table naming its key columns nowhere but inside its own data file cannot be known to have them until that header is read – and a spurious
Falsecosts the whole region the bulk scan for no gain in correctness.
gain.genomic_resources.statistics.base_statistic module
- class gain.genomic_resources.statistics.base_statistic.MergeableRegion(*args, **kwargs)[source]
Bases:
ScannedRegion,ProtocolA scanned region that can absorb its neighbour to the right.
What
RegionFoldedStatisticneeds of the accumulators it holds, and no more: where the region was scanned (fromScannedRegion) and how to fold the next one in. Whether that fold adds counts, unions spans or sums histograms is the region’s own business.- merge(other: Self) None[source]
Fold the adjacent region to the right into this one.
- gain.genomic_resources.statistics.base_statistic.NON_NUMERIC_ERRORS = (<class 'TypeError'>, <class 'ValueError'>)
What a numeric accumulator’s nan skip raises for a value it cannot read as a number – the complaints
non_numeric_error()re-words, stated once so the twins that catch them cannot drift apart.TypeErrorisnp.isnanon text orDecimal;ValueErrorisnp.isnanon a SEQUENCE, which answers with an array whose coercion to theboolthe skip needs is what raises. Catching only the first let the second escape every statistics pass’s per-score containment and abort the whole build (gain#1337). A one-element sequence is the exception: its array coerces, andas_python_number()refuses it below the skip instead.
- gain.genomic_resources.statistics.base_statistic.PYTHON_NUMBER_TYPES = (<class 'float'>, <class 'int'>)
What one value folded into a numeric accumulator may be, once numpy’s own scalars have been normalized to the Python value they hold.
boolrides in throughint, which it subclasses – a bool score under a number histogram really does produce a 0/1 histogram.Shared by
NumberHistogramandMinMaxValuefor the reasonnon_numeric_error()below is: the twins fold the same values, so the allow-list is stated once (gain#1338, gain#1358). It lives here rather than with the histogram becausehistogram.pyimportsmin_max.pyand not the reverse.Named for Python’s types to keep it distinct from
NUMBER_HISTOGRAM_VALUE_TYPES: that one is declared scorevalue_typeSTRINGS, this one is the type of a single folded value.Public rather than folded into
as_python_number(), because each twin tests it INLINE before calling that:add_valueruns per value of every record, a Python float is what the scan hands it, and a function call on that path costs measurably more than oneisinstanceagainst a hoisted tuple (gain#1358 measured +8% against +3%). Hoisted rather than written as a tuple literal at each check, because a tuple of names is rebuilt on every call: 1Misinstancecalls, best of 5 (gain#1338), 112 ns/call for a 3-member literal against 52 ns hoisted.floatfirst becauseisinstancetests a tuple in order.
- class gain.genomic_resources.statistics.base_statistic.RegionFoldedStatistic(statistic_id: str, description: str)[source]
Bases:
Statistic,GenericA statistic accumulated one region per scanned window.
The coverage, allele and fragment statistics are all built this way: the scan produces one region per window, and the statistic keeps one region per chromosome, folding each new window into the region already held for its contig. Distinct chromosomes accumulate side by side; same-chromosome windows merge, which is where the regions’ adjacency rule applies.
Subclasses supply their id and description, their readers over the folded regions, and their own serialization. Everything about the FOLD itself lives here.
- add_value(value: Any) None[source]
Add a value to the statistic.
- fold_region(region: R) None[source]
Fold one region in, keyed by its chromosome.
- merge(other: Statistic) None[source]
Fold another statistics object’s regions into this one.
For statistics holding LIVE regions (the scan’s own): two deserialized statistics carry no extents, so same-chromosome regions from two files refuse to merge as non-adjacent.
Gated on the CONCRETE type rather than on this base: all three kinds fold regions identically, so a base-type gate would happily fold allele counts into a coverage statistic.
type(self)makes that gate asymmetric where the three named-class checks it replaced were symmetric, which nothing observes: none of the three is subclassed.
- class gain.genomic_resources.statistics.base_statistic.ScannedRegion(*args, **kwargs)[source]
Bases:
ProtocolWhat every per-region accumulator shows the merge step.
A statistic accumulated per scanned REGION – coverage, allele content and fragments alike – carries the extent it was scanned over, because the merge is only sound over a contig’s non-overlapping windows. What it accumulates INSIDE that extent is the statistic’s own business and is deliberately absent here.
- chrom: str
- end: int | None
- start: int | None
- class gain.genomic_resources.statistics.base_statistic.Statistic(statistic_id: str, description: str)[source]
Bases:
objectBase class genomic resource statistics.
Statistics are generated using task graphs and aggregate values from a large amount of data. Each statistic should have a clearly defined single unit of data to process (for example, a nucleotide in a reference genome).
- abstractmethod add_value(value: Any) None[source]
Add a value to the statistic.
- description: str
- abstractmethod static deserialize(content: str) Statistic[source]
Create a statistic from serialized data.
- finish() None[source]
Perform final calculations for the statistic.
This step is optional.
This is called when resource iteration is complete.
Can also be used when creating more complex resources via deserialization.
- abstractmethod merge(other: Statistic) None[source]
Merge the values from another statistic in place.
- abstractmethod serialize() str[source]
Return a serialized version of this statistic.
- statistic_id: str
- gain.genomic_resources.statistics.base_statistic.as_python_number(value: Any, what: str) float | int[source]
The Python number a value that is NOT already one folds as.
The slow path of the numeric contract
NumberHistogramandMinMaxValueshare, reached only by a valuenp.isnanaccepted and the caller’s inlinePYTHON_NUMBER_TYPEStest refused – text andDecimalnever get here, and neither does a Python number.numpy’s own scalars fold as the Python value they hold, which is what an enumerated allow-list checked BEFORE normalizing got wrong:
np.float32is not afloat(onlynp.float64subclasses it) andnp.bool_is not annp.integer, so all of them were refused as non-numeric even thoughadd_batchfolds them and a gene score’s column really does arrive as one (gain#1338).item()rather than a wider allow-list: widening alone would not make the histogram’s two arms agree, because numpy 2 keepsnp.float32 - <python float>in float32 and that picks a different bin at the edges – the witness is intest_add_batch_matches_add_value_loop_float32_at_bin_edges.Anything else is refused:
np.isnanaccepting a value does not make it one a reducer can fold. A complex or a 0-d array passes it, andmin()would order either without complaint and leave the extremum holding it (gain#1358). The refusal names what the CALLER handed over, not what it was normalized to, so a nullified score’s reason does not report anp.complex128as a plaincomplex.
- gain.genomic_resources.statistics.base_statistic.non_numeric_error(value: Any, what: str) TypeError[source]
The one wording of a numeric accumulator’s refusal.
whatnames the accumulator in the message; the rule is one – the same shape asrefuse_unmergeable(), and here for the same reason:NumberHistogramandMinMaxValueare twins that fold the same values under the same numeric contract, and a reader of a nullified score’s reason should not be able to tell which of them produced it.Returned rather than raised, so a caller can raise it
fromthe numpy error it is re-wording – one ofNON_NUMERIC_ERRORS.
- gain.genomic_resources.statistics.base_statistic.refuse_unmergeable(what: str, left: ScannedRegion, right: ScannedRegion) None[source]
Refuse a pair that is not adjacent-and-in-order on one contig.
Region statistics are only ever produced over a contig’s non-overlapping windows, so anything else reaching a merge is a wiring error – and it is exactly the adjacency that lets the per-region counts simply add. Refusing it loudly is the difference between a failed build and a silently wrong statistic.
whatnames the statistic in the message; the rule is one.
- gain.genomic_resources.statistics.base_statistic.regions_in_genomic_order(regions: Iterable[R | None]) list[R][source]
The scan’s regions in genomic order, kinds carrying none dropped.
Ordered here rather than trusting the order the task arguments arrived in: the merge asserts adjacency, and a correct set of regions handed over shuffled would fail that assertion.
gain.genomic_resources.statistics.chart_style module
Where the style shared by the statistics charts is set.
histogram draws the number and categorical histograms; in this
package, alleles draws the complex-allele grid and length_histogram
the length ladder. The charts agree on their label font and figure size
so they read as one set on the resource page, and this module is where
that agreement lives – a leaf that imports nothing from gain, so both
tiers reach it with a module-level import. test_architecture keeps
it one and says why (gain#1486).
- gain.genomic_resources.statistics.chart_style.CHART_FIGSIZE = (15, 10)
Figure size, in inches, shared by the statistics charts – all but the complex-allele grid, which is square and keeps its own.
- gain.genomic_resources.statistics.chart_style.CHART_LABEL_FONT_SIZE = 20
Label font size shared by the statistics charts.
gain.genomic_resources.statistics.coverage module
Covered-position statistics for tabular genomic scores.
Vocabulary per CONTEXT.md and ADR 0020: a covered position is a
position spanned by at least one table row — value-blind, union semantics.
A segment is a maximal run of touching-or-overlapping rows carrying
equal values (the whole scanned score tuple, NA equal to NA, floats exact).
The whole of this statistic lives here: the per-region accumulator and
the resource-wide statistic, the fold that merges a scan’s regions into
one, the write, and the render payload the info page reads. Its allele
twin is laid out the same way in
gain.genomic_resources.statistics.alleles; the scan wiring that
feeds either is in implementations/genomic_scores_impl/scan.py.
- class gain.genomic_resources.statistics.coverage.CoverageDisplay(rows: list[CoverageRow], global_length: int | None, uncovered: UncoveredContigs | None, segment_lengths: list[int] | None)[source]
Bases:
NamedTupleThe Coverage section’s render payload, shares resolved.
Raw counts come from the stored statistic; shares are computed at render time and never stored.
global_lengthis the denominator the section answers what part of the reference genome has values against: the WHOLE resolved reference, including contigs the score never touched (gain#1041). It isNoneunless every covered chromosome resolved a length – a covered contig the reference does not list is proof the reference is the wrong one, and a global percent over a partial denominator would be misleading.- property global_covered: int
- property global_fraction: float | None
The whole score’s share of the reference, as a number.
- global_length: int | None
Alias for field number 1
- property global_percent: str | None
The whole score’s share, as the page writes it.
The same rule the rows are written through, over the same two integers: a reference all but entirely covered reads
>99.99%here exactly as one of its chromosomes does above (gain#1057).
- property global_segments: int | None
The segment total, or
Nonewhen any row lacks segments.All-or-nothing like the stored statistic: a global over a partial set would silently understate.
- property has_fractions: bool
Whether the section renders a
Covered %column at all.The summary rows carry percentages too, so this cannot be read off
rowsalone: a score with no values ANYWHERE has every contig rolled up and no rows left, and a resolved global fraction of 0.0 would be computed and then dropped for want of a column to print it in.
- property has_segments: bool
- rows: list[CoverageRow]
Alias for field number 0
- segment_lengths: list[int] | None
The global segment-length histogram, or
Noneif unknown.The section’s image is drawn from these counts, so the page decides whether to show it from the same counts the plotter refuses to draw – a proxy such as the segment total could disagree with what was actually written.
- uncovered: UncoveredContigs | None
The untouched part of the reference, or
Nonewhen unknowable.None– rather than a zero roll-up – wheneverglobal_lengthis: “these contigs have no values” is a claim about the resolved reference being the right one, and it is not made under a denominator already known to be wrong.
- class gain.genomic_resources.statistics.coverage.CoverageRow(chrom: str, covered: int, length: int | None, segments: int | None)[source]
Bases:
NamedTupleOne chromosome’s rendered coverage: raw counts, share derived.
lengthis the denominator resolved for this chromosome, orNonewhen none was – the row then renders its raw count only.segmentsisNonewhen the stored statistic carries no segment data for the resource (an old file, or a kind that publishes none).The share is held as the two INTEGERS it is a share of rather than as either rendering of it, because the page needs both and they must not disagree: the cell sorts on
fractionand displayspercent, and the boundariespercentrespects – covered none of it, covered all of it – are decided on the counts (gain#1057).- chrom: str
Alias for field number 0
- covered: int
Alias for field number 1
- property fraction: float | None
The share as a number, for the cell’s sort key.
- length: int | None
Alias for field number 2
- property percent: str | None
The share as the page writes it,
Nonewithout a length.
- segments: int | None
Alias for field number 3
- class gain.genomic_resources.statistics.coverage.CoverageStatistics[source]
Bases:
RegionFoldedStatistic[RegionCoverage]A resource’s covered positions, per chromosome and global.
Folds
RegionCoveragethe way the base class does, and serializes to the resource’sstatistics/coverage.jsonas raw counts. Fractions are deliberately not computed here: they need chromosome lengths, which belong to a reference genome resolved at render time.- covered_by_chromosome() dict[str, int][source]
- covered_global() int[source]
- static deserialize(content: str) CoverageStatistics[source]
Create a statistic from serialized data.
- segment_lengths_by_chromosome() dict[str, list[int]][source]
Per-chromosome length histograms – the read API for the per-chromosome data the statistics file stores (rendered consumers use the global roll-up; gain#776 reads these).
- segment_lengths_global() list[int] | None[source]
The bin-wise sum of the per-chromosome length histograms.
- segments_by_chromosome() dict[str, int][source]
- segments_global() int | None[source]
- serialize() str[source]
Return a serialized version of this statistic.
- class gain.genomic_resources.statistics.coverage.RegionCoverage(chrom: str, start: int | None, end: int | None, *, publishes_segments: bool = True)[source]
Bases:
objectCoverage of one scanned region, accumulated row by row.
Consumes
[begin, end]spans in non-decreasingbeginorder – the order the scan validators guarantee – and counts each position once. The rows it is fed are pairwise disjoint: since gain#1127 the only coverage-scanned kinds are position scores, whose validators refuse a row beginning at or before its predecessor’s end (adjacent rows are legal and common, and the segment algebra depends on that). So the scan hands over each row at its FULL extent, unclipped – disjoint spans cannot double-count a position, the union stays additive across parallel regions, and the segment runs are measured at their true length rather than the region’s (gain#1175 retired the clip that overlapping rows once needed).- add_interval(begin: int, end: int, values: tuple) None[source]
Fold one row span into the coverage and its run bookkeeping.
The union first – a running maximum over the right edge, so a row is counted once whatever it overlaps – then the runs: the row joins the open run while it touches or overlaps it and carries equal values, and closes it otherwise. Refuses a region that publishes no segments – see
_refuse_without_segments().
- add_interval_batch(left: ndarray, right: ndarray, cells: list[ndarray]) None[source]
Fold a batch of row spans, collapsed into runs.
The vectorized statement of the rule
add_interval()applies row by row — it lives HERE, beside that rule, so the equality algebra has one home: rows collapse into a run while they touch or overlap the positions covered so far and every column compares equal, nan equal to nan (ADR 0020), and each run costs oneadd_interval()rather than one per row.The touching test reads the running maximum end, which is exact for a position score (whose validators refuse overlap, so the previous row IS the running maximum).
left/rightare the rows’ own full extents (see the class docstring) andcellsis one kept column per scanned score, all equally long.
- classmethod frozen(chrom: str, covered: int, segments: tuple[int, list[int]] | None) RegionCoverage[source]
A region restored from serialized counts, with no scan state.
segmentsofNonemarks that data unknown – the file predates it, or carries foreign bins – and is the one way a region comes to publish no segments.
- merge(other: RegionCoverage) None[source]
Fold the adjacent region to the right into this one.
Refuses a pair that is not adjacent-and-in-order on one chromosome – see
refuse_unmergeable, which states that rule for this statistic and its two twins alike.
- property publishes_segments: bool
Whether this region has segment numbers to answer with.
The one predicate behind both
segment_summary()’sNoneand the accessors’ refusal, so the two gates cannot drift apart. False only for a regionfrozen()from a statistics file that carried no segment data.
- property segment_count: int
How many segments the region holds.
Refuses a region that publishes none – see
_refuse_without_segments().
- segment_length_histogram() list[int][source]
Counts of segment lengths on the fixed log2 bins.
Finalizes the still-open bookkeeping: the first and the open run are folded in on top of the interior counts, so the histogram totals exactly
segment_count. Refuses a region that publishes none – see_refuse_without_segments().
- segment_summary() tuple[int, list[int]] | None[source]
Segment count and length histogram, or
Noneif unknown.Unknown means the region was deserialized from a statistics file that predates segment-length histograms. This is the ASKING form of the gate the count and histogram accessors refuse through –
Nonehere, an exception there, because a caller that asks may not know and one that reaches straight for a number has asserted it does.
- class gain.genomic_resources.statistics.coverage.UncoveredContigs(contigs: int, length: int)[source]
Bases:
NamedTupleThe contigs of the reference that carry no values at all.
One roll-up rather than a row each: a reference genome routinely carries hundreds of contigs a score never touches (alts, decoys, an unplaced scaffold), and per-contig zero rows would bury the contigs that do have values. The count and the base pairs are what the global fraction is measured against but has nothing to show for.
Membership is zero covered positions, not absence from the stored statistic. The two differ by backend and by nothing else – a bigWig scan visits every header contig and stores a 0 for the empty ones, a tabix scan visits only contigs the index lists – so rolling up by absence would render the same data two ways.
- contigs: int
Alias for field number 0
- length: int
Alias for field number 1
- property percent: str
none of it.
Written through the same rule as every other cell in the column rather than as a formatted constant, so the row cannot drift from its neighbours (gain#1057). Membership is zero covered positions, so this is the rule’s exact zero by construction.
Unguarded, and returning
strrather thanstr | None, whereCoverageRow.percentandCoverageDisplay.global_percentboth check their denominator first: those hold anint | None, because a denominator is what may fail to resolve, while a roll-up exists only where one DID – and_plausible_lengths()has already dropped every contig that could contribute a zero tolength.- Type:
The share of these contigs that is covered
- gain.genomic_resources.statistics.coverage.accumulate_coverage(arrays: tuple[ndarray, ndarray, dict[str, ndarray]], coverage: RegionCoverage, region: tuple[str, int | None, int | None]) None[source]
Fold one batch of column arrays into the region’s coverage.
Rides
owned_records_mask(), the record partition every other statistic reads: a region owns the rows whosepos_beginfalls inside it and measures them whole. That is exact for coverage because the rows are pairwise disjoint (seeRegionCoverage): they cannot double-count a position, so the union is additive across parallel regions at full span, and the segment runs the same feed builds are measured at their true length rather than the region’s. A record beginning past the region’s end is not owned and covers nothing – the gain#636 verdict, reached here by the partition rather than by a clip.The spans reach
RegionCoverage.add_interval_batch(), which owns the run-collapse algebra; nothing here knows what “equal values” means. The batches the backends return rarely carry a row outside the queried region, so the all-kept batch skips the mask copies entirely.
- gain.genomic_resources.statistics.coverage.build_coverage_display(resource_id: str, statistics: CoverageStatistics, lengths: dict[str, int]) CoverageDisplay[source]
Turn stored counts plus a resolved denominator into the payload.
What this resolves is each share’s DENOMINATOR; the shares themselves are derived on the payload, from that denominator and the count it bounds (see
CoverageRow). Either way nothing is stored: the statistic stays raw counts (seeCoverageStatistics). A denominator that cannot bound what it must is withheld, degrading that row to a raw count rather than rendering a zero-division or a >100%.lengthsis the whole reference the score is measured against, not merely the contigs it touched, so the global share answers what part of the reference genome has values and the untouched remainder is reported as one roll-up (gain#1041).
- gain.genomic_resources.statistics.coverage.merge_region_coverage(resource_id: str, regions: Iterable[RegionCoverage | None]) CoverageStatistics | None[source]
Fold the regions’ coverage, or
Nonefor an uncovered kind.
- gain.genomic_resources.statistics.coverage.normalize_values(values: Iterable[Any]) tuple[source]
A row’s score values as the tuple segment equality compares.
Every spelling of “no value” –
Noneon the per-record path, nan in a bulk float column – becomesNone, so NA equals NA whichever path produced the row (ADR 0020). Everything else is compared as stored: floats exactly, no tolerance.
- gain.genomic_resources.statistics.coverage.region_coverage_for(score: GenomicScore, chrom: str, start: int | None, end: int | None) RegionCoverage | None[source]
A region accumulator for a position score,
Nonefor other kinds.Gated on the built score’s class rather than on the resource type string, for the reason
region_alleles_for()gives.A position score is the one kind whose rows are pairwise disjoint – the rule it is registered under in
record_validationrefuses a row beginning at or before its predecessor’s end – and that is the property this statistic depends on: only then does the union of the spans answer “is there data at this position at all?” exactly, so that the count is a genuine measure of what the resource covers and the fraction a genuine completeness figure. What disjointness buys the accumulator – full-span rows, an additive union, published segments – isRegionCoverage’s own docstring.An allele score’s rows are points, so there is no span to union. A fragment score’s rows overlap by design;
fragmentsand ADR 0020 say why that keeps the kind out of both coverage and segments.
- gain.genomic_resources.statistics.coverage.resolve_chrom_lengths(resource: GenomicResource, ref_genome: ReferenceGenome | None, score_lengths: Callable[[], Mapping[str, ChromLength]], chroms: Iterable[str]) dict[str, int][source]
Resolve chromosome lengths for the render-time denominator.
The ladder: the
reference_genomethe caller resolved from the resource’s label; else the score’s own lengths, kept where their source is exact (a bigWig header’s contig sizes); else raw counts (an empty mapping).What comes back is the whole universe the fraction is measured against, not only the contigs the score touched (gain#1041): every contig of the resolved genome, or every contig the score rung has an exact length for.
chroms– the covered contigs – is still passed in so that a covered contig the resolved source does NOT list is visible to the caller by its absence, which is what degrades the fraction.The two rungs are not interchangeable: only the genome rung answers “what part of the reference genome has values”, while the score rung answers “what part of what this file declares”.
docs/adr/0020carries the worked example and is the record to amend if this changes; the user-facing half is indocs/source/grr.rst.Both rungs are the CALLER’s to supply: the genome needs a repository, which only exists during a page build, and the score’s records come from the implementation’s ladder (
GenomicScoreImplementation.get_chrom_lengths()), which opens the score to ask – so they are asked for, throughscore_lengths, only once the genome rung has nothing. Which of a record’s sources may serve as a denominator isChromLengthSource.is_exact’s call, not this function’s (gain#1414): a tabix probe’s upper bound and an in-memory table’s extent are dropped here, and a contig with no length at all (proven empty, or one the probe could not bracket) with them.
- gain.genomic_resources.statistics.coverage.save_and_plot_coverage(resource: GenomicResource, statistics: CoverageStatistics | None) None[source]
Write the coverage statistics and their histogram images.
Does nothing for a kind that has no coverage, and skips a group’s image when there is nothing to draw – whether the group is unknown or known and empty. The same rule as
save_and_plot_alleles.
gain.genomic_resources.statistics.fragments module
Fragment-count statistics for fragment scores.
Vocabulary per CONTEXT.md: a fragment is a table row AS STORED.
Overlapping, nested and duplicate rows each count once, at their own
unclipped span — there is no run algebra and no stitching here, because
a row is owned whole by exactly one scanned region.
This statistic used to ride inside
gain.genomic_resources.statistics.coverage, as one more optional
group in the coverage file. It does not any more (gain#1127): a
fragment score’s rows deliberately overlap, so the union of their spans
measures nothing a reader wants, and the kind is no longer
coverage-scanned at all. While the two shared a carrier, dropping the
union would have dropped the tally with it — which is why the tally
moved out first.
Laid out like its two twins, coverage and alleles: the
per-region accumulator, the resource-wide statistic, the fold that
merges a scan’s regions into one, the write, and the render payload the
info page reads. The scan wiring that feeds all three is in
implementations/genomic_scores_impl/scan.py.
- class gain.genomic_resources.statistics.fragments.FragmentDisplay(rows: list[FragmentRow], fragment_lengths: list[int] | None)[source]
Bases:
NamedTupleThe Fragments section’s render payload.
Counts only – nothing to resolve: a fragment is a table row, and rows have no natural total to be a fraction of. The global count is the sum of the rows, exactly as the stored statistic’s global entry is the merge of its per-chromosome ones.
- fragment_lengths: list[int] | None
The global fragment-length histogram, or
Noneif unknown.Unknown is a THIRD answer, distinct from a histogram that is known and all zero: the counts and the histogram are read independently, so a stored histogram binned on foreign edges leaves the lengths unknown while the counts stay exact. The section renders all three apart – “not computed”, the image, “no fragments” – because collapsing the first into the last would deny fragments the table beside it is counting.
- property global_fragments: int
- rows: list[FragmentRow]
Alias for field number 0
- class gain.genomic_resources.statistics.fragments.FragmentRow(chrom: str, fragments: int)[source]
Bases:
NamedTupleOne chromosome’s fragment count, as the info page renders it.
- chrom: str
Alias for field number 0
- fragments: int
Alias for field number 1
- class gain.genomic_resources.statistics.fragments.FragmentStatistics[source]
Bases:
RegionFoldedStatistic[RegionFragments]A resource’s fragment counts, per chromosome and global.
Folds
RegionFragmentsthe way the base class does, and serializes toFRAGMENT_STATISTICS_FILEas raw counts.- static deserialize(content: str) FragmentStatistics[source]
Create a statistic from serialized data.
- fragment_lengths_by_chromosome() dict[str, list[int]][source]
Per-chromosome fragment-length histograms, as stored.
A chromosome whose histogram is unknown is left OUT rather than given an all-zero one, which would read as “measured, and empty”.
- fragment_lengths_global() list[int] | None[source]
The bin-wise sum of every chromosome’s histogram.
Nonewhen any chromosome’s is unknown: a partial roll-up would silently understate, the same all-or-nothing rule the coverage twin applies to its own optional groups.
- fragments_by_chromosome() dict[str, int][source]
- fragments_global() int[source]
- serialize() str[source]
Return a serialized version of this statistic.
- class gain.genomic_resources.statistics.fragments.RegionFragments(chrom: str, start: int | None, end: int | None)[source]
Bases:
objectThe fragments of one scanned region, counted row by row.
Consumes row spans and counts each row once, binned by its own length. Unlike
RegionCoveragethis carries no opt-out flag: a region is built only for a kind whose rows ARE fragments, so every instance publishes a tally.- add_fragment(length: int) None[source]
Count one fragment of that many base pairs.
The row’s OWN span, never clipped to the region: a region owns the rows beginning inside it and measures them whole, so a fragment is counted once at its true length however the contig was split.
- add_fragment_batch(lengths: ndarray) None[source]
Count a whole batch of fragment lengths at once.
The vectorized statement of
add_fragment(), and it lives HERE beside that rule so the binning has one home. Vectorized because a genome-scale fragment score has hundreds of thousands of rows, and this is the path ADR 0001 deleted the per-row object churn from.The bin is found by INTEGER comparison against the ladder’s own edges, not by
log2: the edges are part of the stored format, and a float log of a large integer can land on the wrong side of a power of two.searchsortedclamps into the open-ended last bin for free.
- property fragments: int
How many rows this region counted.
- classmethod frozen(chrom: str, fragments: int, bins: list[int] | None) RegionFragments[source]
A region restored from serialized counts, with no scan state.
binsofNonemarks the length histogram unknown – the stored one was binned on edges this code cannot merge with. The COUNT is unaffected and still reads, so a file like that renders its table and no image.
- length_histogram() list[int] | None[source]
The region’s fragment-length bins, or
Noneif unknown.
- merge(other: RegionFragments) None[source]
Fold the adjacent region to the right into this one.
Refuses a pair that is not adjacent-and-in-order on one chromosome – see
refuse_unmergeable, which states that rule for this statistic and its two twins alike.No stitch is needed: a row is owned whole by exactly one region, so the merged count and histogram are plain sums.
- gain.genomic_resources.statistics.fragments.accumulate_fragments(arrays: tuple[ndarray, ndarray, dict[str, ndarray]], fragments: RegionFragments, region: tuple[str, int | None, int | None]) None[source]
Fold one batch of column arrays into the region’s fragment tally.
Fragments partition RECORDS: the rows this region OWNS, each measured at its own unclipped span. So this rides
owned_records_mask(), the record partition every statistic reads – a row is owned whole by exactly one region however the contig was split.
- gain.genomic_resources.statistics.fragments.build_fragment_display(statistics: FragmentStatistics) FragmentDisplay[source]
The Fragments payload for a resource that has the statistic.
Always a payload: the file existing IS the statistic, where the group riding inside the coverage file used to have to answer “present but carrying no fragments” as well (gain#1127). Whether the section renders at all is decided by the file’s presence, in the implementation’s
get_fragment_display.
- gain.genomic_resources.statistics.fragments.merge_region_fragments(resource_id: str, regions: Iterable[RegionFragments | None]) FragmentStatistics | None[source]
Fold the regions’ counts, or
Nonefor a kind with no fragments.
- gain.genomic_resources.statistics.fragments.region_fragments_for(score: GenomicScore, chrom: str, start: int | None, end: int | None) RegionFragments | None[source]
A region accumulator for a fragment score,
Nonefor other kinds.Gated on the built score’s class rather than on the resource type string, for the reason
region_alleles_for()gives: the builder has already resolved both spellings of the kind toFragmentScore, and asking the string again would restate its dispatch. The class is the property this statistic depends on – that the rows ARE fragments, each counted once at its own span.
- gain.genomic_resources.statistics.fragments.save_and_plot_fragments(resource: GenomicResource, statistics: FragmentStatistics | None) None[source]
Write the fragment statistics and their histogram image.
Does nothing for a kind that has no fragments, and skips the image when there is nothing to draw. The same rule as
save_and_plot_coverageandsave_allele_statistics.
gain.genomic_resources.statistics.indel_lengths module
The stored form of an indel group: an exact length map plus scalars.
ADR 0020 gives segments, fragments and indels one log2 binning, and until gain#1118 the indel groups stored theirs on it. They no longer do. The ladder lumps {2, 3} into one bin and {4, 5, 6, 7} into the next, which is exactly where indels live, so no exact minimum, maximum, mean or median survived it – and those four are what the statistics table on the info page exists to show.
The ladder is still what the CHART is drawn on; it is derived from the
map at render time by indel_length_ladder() rather than stored
beside it, so the picture and the numbers beneath it cannot drift. For
segments and fragments the ladder remains the stored format.
Split out of gain.genomic_resources.statistics.alleles when the
exact map arrived: the stored format of one allele class, its merge
rule and the statistics derived from it are one subject, and the module
they came from carries four other groups besides.
- gain.genomic_resources.statistics.indel_lengths.INDEL_LENGTH_CLAMP = 8192
The longest indel length the stored map resolves exactly. A length at or above it folds into one overflow bucket keyed by the clamp, which therefore reads “this many bases or more”. The clamp is TOTAL in the sense
COMPLEX_LENGTH_CLAMPis: every indel lands in exactly one bucket, so the map’s values sum to the group’s count.Exact lengths rather than the shared log2 ladder (gain#1118). The ladder’s second bin is {2, 3} and its third {4, 5, 6, 7}, which is where indels actually live, so NO exact min, max, mean or median survives it – and those four are what the statistics table exists to show. Part of the stored format: it must not change once resources carry maps built from it.
It must never fall BELOW
LENGTH_HISTOGRAM_DISPLAY_CAP, because the chart’s bins are derived from this map and a bin between the two would be drawn from lengths the map had already folded away. They are EQUAL today, which is the tightest the rule allows and is what makes the derived chart identical to the one the stored histograms drew: the plot sums every bin at or above the cap into one overflow bar anyway, so folding at the same length loses nothing it would have drawn separately. The consequence to know is the other direction – the display cap is documented as free to change, and it is not free to be RAISED any more. Raising it means raising this first, which is a stored-format change and needs every resource rebuilt.
- class gain.genomic_resources.statistics.indel_lengths.IndelLengths(lengths: dict[int, int], alleles: int, sum: int, min: int | None, max: int | None)[source]
Bases:
NamedTupleOne indel group’s lengths: an exact map and four scalars.
lengthsmaps a length in base pairs to how many alleles have it, clamped atINDEL_LENGTH_CLAMP– the key AT the clamp means “that long or longer”. The map is what the chart’s bins and the median are derived from.The scalars are what keep the clamp from becoming a lie.
alleles,sum,minandmaxare all accumulated on the UNCLAMPED length, somin,maxand the mean stay exact however far the tail runs; only a median landing in the overflow bucket degrades, and it says so. A clamped map alone would understate the mean and cap the max – the one statistic that exists to describe the tail.They are stored rather than derived even where the map could give them, because that is the whole point:
sumcannot be recovered from a clamped map at all, and amaxrecovered from one would read 8192 for a 40,000 bp deletion.allelesdoes equal the map’s total and is kept beside the other three so the four merge as one thing and the file says outright what the mean is over.minandmaxareNoneexactly whenallelesis zero: a group with no alleles has no shortest and no longest, and 0 is not a length any indel can have.- alleles: int
Spelled as the column it sits under rather than
count, which atuplealready means something else by – the same rename, for the same reason, asMatrixCell’s. The STORED key stayscount, which is what it is in the file.
- property has_counts_to_plot: bool
Whether this group has anything to draw a chart of.
The indel side of
has_counts_to_plot(), which asks the same question of the two coverage groups still stored on the ladder. Unknown and known-and-empty are one answer for the same reason: the counts axis is logarithmic and can render neither.One spelling, because the statistics build and the page must agree exactly – a build that skips the image while the page links it leaves a dangling thumbnail, and the reverse leaves a file nothing references.
- lengths: dict[int, int]
Alias for field number 0
- max: int | None
Alias for field number 4
- property mean: float | None
The mean length, exact past the clamp.
Noneif empty.
- property median: float | None
The middle length, or the mean of the middle two if even.
The standard convention, stated on the ALLELES rather than on the distinct lengths: {2, 3} is 2.5, and a group of one 2 and nine 3s has a median of 3, not 2.5.
Read off the map, so it degrades where the map does. When
median_is_clampedthis is a LOWER BOUND rather than the median, and the page renders it as one.
- property median_is_clamped: bool
Whether either middle allele fell in the overflow bucket.
The one statistic here the clamp can blunt, so it is asked outright rather than left to a reader to notice that a suspicious number is suspicious.
Asked of the two middle LENGTHS, never of their average, which is the distinction a first cut got wrong. Every key in the map is at most the clamp, so an average reaching it means BOTH middles were in the overflow bucket; a group with one middle below the clamp and one above averages to something under it and would have been published as though exact. On a group of one 1 bp and one 40,000 bp deletion that reads 4096.5, where the truth is 20,000.5 – a fabricated number beside three exact ones.
Since the clamped side is a floor, the average is a floor too: the true median is at least what
mediancomputes, whichever of the two middles was clamped.
- middle_lengths() tuple[int, int] | None[source]
The one or two lengths the median is taken over.
Both indices land on the same allele when the count is odd, so the pair collapses and the median is that length. Kept as the PAIR rather than folded straight into an average, because whether the clamp blunted the answer is a property of these two lengths and cannot be read back off their mean – see
median_is_clamped.The rank arithmetic is the same convention
MedianAggregatorapplies to a weighted value list, restated here rather than shared: that one is a private method on a stateful annotation-layer aggregator keyed on its own accumulated values, and reaching it would cost more than the six lines it saves. If a third caller ever wants it, the extraction is a freeweighted_medianover(value, count)pairs ingain.utils.
- min: int | None
Alias for field number 3
- sum: int
Alias for field number 2
- class gain.genomic_resources.statistics.indel_lengths.IndelStatisticsRow(group: str, alleles: str, min: str, max: str, mean: str, median: str)[source]
Bases:
NamedTupleOne indel group as the statistics table renders it.
Every field is already TEXT. The table shows five different kinds of number – an allele count, two exact lengths and two averages, one of which can be a floor – and formatting each where it is built keeps the template free of a second rule about how a length is written.
- alleles: str
Alias for field number 1
- group: str
Alias for field number 0
- max: str
Alias for field number 3
- mean: str
Alias for field number 4
- median: str
Alias for field number 5
- min: str
Alias for field number 2
- classmethod of(group: str, lengths: IndelLengths) IndelStatisticsRow[source]
One group’s row; the LENGTH cells empty when it holds none.
A group that was scanned and found nothing has a genuine count of zero, which renders as
0– that is the answer, not a missing one. What it has no answer for is a shortest, longest or average length, so those four render empty rather than as zeros, which would read as indels of length nothing.
- class gain.genomic_resources.statistics.indel_lengths.IndelTally[source]
Bases:
objectA mutable indel group, accumulated row by row.
The scan’s counterpart to
IndelLengths, which is what a region hands out. Separate because the map is updated in place: a tuple rebuilt per row would copy the whole map each time, which on a resource whose deletions run to thousands of distinct lengths is quadratic in the lengths seen.- add(length: int, multiplicity: int) None[source]
Fold
multiplicityalleles of one exact length in.Written out longhand rather than with
min()/max(), which is not style: this runs once per DISTINCT ref/alt pair per scanned batch, and the two builtin calls measured 158ns of the 258ns this method had added over the fixed-bin increment it replaced. Comparing in place brings it to ~20ns over. An all-substitution score never reaches here at all – the class gate above decides that – so the cost lands only on indel rows.
- frozen() IndelLengths[source]
This group as the inert record a region hands out.
- lengths: dict[int, int]
- max: int | None
- merge(other: IndelTally) None[source]
Fold another group of the same kind into this one.
The ONE statement of how two indel groups come together, so the region merge and the global roll-up cannot drift: maps add per length,
countandsumadd, and the extremes take the extreme. No re-clamping – both maps are already keyed on clamped lengths, whilemin/maxare exact on both sides and stay exact here.
- min: int | None
- classmethod restored(lengths: IndelLengths) IndelTally[source]
A tally holding what a stored group already counted.
- gain.genomic_resources.statistics.indel_lengths.NO_INDELS = ({}, 0, 0, None, None)
An indel group that was scanned and holds nothing – distinct from a group that was never scanned, which is
None.Its map is shared, so nothing may mutate it. Nothing does: it is the identity the roll-up starts from, and
merged_indels()copies both sides throughIndelTally.restored()rather than folding into either. Add a path that mutates a group in place and it must start fromIndelTally(), not from this.
- gain.genomic_resources.statistics.indel_lengths.indel_length_ladder(lengths: IndelLengths) list[int][source]
The exact map binned onto the shared log2 ladder, for the chart.
Derived at render time rather than stored (gain#1118), so the chart and the statistics beside it cannot drift: there is one source of truth and the picture is a view of it.
The result is identical to the histogram this replaced, not merely close.
plot_length_histogramsums every bin at or above its display cap into a single overflow bar, andINDEL_LENGTH_CLAMPequals that cap – so the lengths the map folded together are exactly the ones the chart was going to add up anyway. Below the cap the binning is the same function on the same lengths.
- gain.genomic_resources.statistics.indel_lengths.merged_indels(left: IndelLengths | None, right: IndelLengths | None) IndelLengths | None[source]
The sum of two indel groups, unknown if either is.
The
_merged_matrix()rule over the exact length maps: an unknown side makes the whole merge unknown rather than a smaller number. The arithmetic itself isIndelTally.merge(), so the roll-up here and the region merge in the scan are one rule.
- gain.genomic_resources.statistics.indel_lengths.merged_tallies(left: IndelTally | None, right: IndelTally | None) IndelTally | None[source]
The same rule between two SCANNED groups, folded left in place.
gain.genomic_resources.statistics.length_histogram module
The one fixed length ladder, and the chart drawn on it.
ADR 0020 gives segments and fragments this binning as their
STORED form, so that per-chromosome results merge into exact global ones
and chunked scans merge exactly for the same reason. Both live in
gain.genomic_resources.statistics.coverage, which is why the
ladder lives in neither: it is the shared contract, not a coverage
detail its sibling happens to import.
Indels left the stored ladder in gain#1118 (ADR 0020 as amended).
They keep an exact {length: count} map in
gain.genomic_resources.statistics.indel_lengths and merge on
that; what they still use from here is the RENDERING – the map is
projected onto these bins at draw time so the indel chart keeps the
shape the stored histograms drew. So the two callers now use this
module for different things, and only the coverage pair depends on the
edges being part of any file.
What the ladder does NOT bin at all is the complex allele grid: its cells are exact lengths (ADR 0020 as amended by gain#779), for reasons that belong with that grid rather than here.
- gain.genomic_resources.statistics.length_histogram.accumulate_bins(target: list[int], source: Iterable[int]) None[source]
Add one length histogram into another, bin for bin.
The one statement of “counts are added, not replaced”, for every place two histograms on the fixed ladder come together: a merge of two regions, a batch of fresh counts, a global roll-up.
- gain.genomic_resources.statistics.length_histogram.binwise_sum(histograms: Iterable[Iterable[int]]) list[int][source]
Merge any number of histograms on this ladder into one.
Exact rather than approximate, and that is the point of the fixed edges: every histogram is binned the same way, so a global roll-up is a merge and never a re-scan.
- gain.genomic_resources.statistics.length_histogram.has_counts_to_plot(histogram: list[int] | None) TypeGuard[list[int]][source]
Whether a length histogram has a positive count to draw.
Unknown and known-and-empty are one answer here: the counts axis is logarithmic and can render neither, and a chart of nothing under a “Segment lengths” heading states nothing either.
Coverage’s two groups – segments and fragments – are the callers. The indel groups asked this too until gain#1118 took them off the stored ladder: they carry an exact length map now, so the same question is
lengths is None or not lengths.alleles, read off the thing they actually store rather than off bins derived from it.
- gain.genomic_resources.statistics.length_histogram.histogram_on_this_ladder(counts: Iterable[Any] | None) list[int] | None[source]
A stored histogram, or
Nonewhen it is not one of ours.Nonein means the key was absent;Noneout additionally covers a histogram of the wrong length, which was binned on foreign edges and so cannot merge with these fixed bins. Either way the reader learns unknown, which is a different answer from an all-zero histogram – that one says the thing was measured and found empty. Stated once here because both stored statistics apply it.
- gain.genomic_resources.statistics.length_histogram.length_histogram_bin_index(length: int) int[source]
The fixed log2 bin a length of that many base pairs falls in.
- gain.genomic_resources.statistics.length_histogram.plot_length_histogram(outfile: IO, histogram: list[int], item: str, display_cap: int = 8192) None[source]
Render a length histogram on the fixed log2 bins as PNG.
Styled to sit beside the per-score value histograms on the resource info page: the figure size and label font are the ones the statistics charts share, read from
gain.genomic_resources.statistics.chart_style.itemnames what was measured – segments, fragments, insertions, deletions – and appears in both axis labels; the bins are the same ladder every time, which is what lets one renderer serve them all. Required, with no default: a fragment histogram silently labelled “segment” is the one mistake this parameter exists to prevent.display_capis the length the drawn axis stops at: every bin at or above it becomes one overflow bar. A parameter rather than a constant so a resource kind whose lengths genuinely run longer can raise its own axis without anything touching the stored format, which the fold never reads back. It is snapped down to its own bin on the ladder, so a cap between two edges caps at the lower one – pass a power of two to get the axis the number reads as.
gain.genomic_resources.statistics.min_max module
- class gain.genomic_resources.statistics.min_max.MinMaxValue(score_id: str, min_value: float = nan, max_value: float = nan)[source]
Bases:
StatisticStatistic that calculates Min and Max values in a genomic score.
- add_value(value: float | generic | None) None[source]
Fold one value into the running extremum.
The same numeric contract as
NumberHistogram.add_value, stated the same way and in the same order, so that a nullified score’s reason reads alike whichever twin refused (gain#1313, gain#1358). The agreement is pinned value by value intest_numeric_reducer_twins.
- static deserialize(content: str) MinMaxValue[source]
Create a statistic from serialized data.
- merge(other: Statistic) None[source]
Merge the values from another statistic in place.
- serialize() str[source]
Return a serialized version of this statistic.
- class gain.genomic_resources.statistics.min_max.MinMaxValueStatisticMixin[source]
Bases:
object- static get_min_max_file(score_id: str) str[source]
- class gain.genomic_resources.statistics.min_max.NullMinMaxValue(score_id: str, reason: str)[source]
Bases:
MinMaxValueA score’s min/max, refused rather than measured.
The min/max twin of
NullHistogram, and it exists for the same reason: a reducer that refuses a value has to leave something behind that the rest of the pass can keep feeding harmlessly, so one score costs one score rather than the resource’s whole statistics build (gain#1285, gain#1313).add_valueis a no-op, which is what makes the refusal cost ONE exception rather than one per record – the score is swapped out for this and every later record folds into nothing. Without that latch a caller catching per value would pay a raise per record, which on a genome-scale resource is multiples of the pass it is protecting.It keeps the inherited nan min and max, so a caller that never learned about this type still reads it as a score with no values to bin.
update_hist_confsdoes know about it, and nullifies it in a branch of its own – the same outcome as the nan branch beside it, reached without depending on the seed, and worded fromreason: “the values could not be folded” and “there were no values” are different facts about a resource, and a curator reading a nullified score needs to know which.It deliberately does NOT override the inherited
MinMaxValue.serialize()/MinMaxValue.deserialize(), which would write the nan seed and read back a plainMinMaxValue– losing the refusal. Those two have no callers on any live path (ADR 0001), and a refused min/max never reaches a file; a refusal travels between the region tasks and the merge as an OBJECT. This is the one asymmetry withNullHistogram, which avoids the trap by being a sibling of its histogram rather than a subclass.- add_value(value: float | generic | None) None[source]
Fold one value into the running extremum.
The same numeric contract as
NumberHistogram.add_value, stated the same way and in the same order, so that a nullified score’s reason reads alike whichever twin refused (gain#1313, gain#1358). The agreement is pinned value by value intest_numeric_reducer_twins.
- merge(other: Statistic) None[source]
Merge the values from another statistic in place.
gain.genomic_resources.statistics.percentages module
How a share of a whole is written, for every table that writes one.
One rule in one place (gain#1057). The info page carries two tables
that each render a count as a percentage of a total – Coverage, whose
denominator is a chromosome’s length, and Alleles, whose denominator is
the allele total – and before this module they formatted it two ways:
the Alleles section through percentages_over()’s floor and
ceiling, the Coverage section through a bare "%.2f%%" inline in the
template. So one page could read 100.00% in Coverage and
>99.99% in Alleles of the identical shape.
The rule is deliberately SCALAR – one (count, total) pair to one
string. What varies between the callers is the contract around a
MISSING denominator, and that is theirs to keep. Coverage resolves a
denominator per row and degrades only that row; so, since gain#1118,
do the Alleles table’s per-class share columns, whose denominator is
each chromosome’s own allele count. The whole-map answer – no total,
no column at all – is still percentages_over()’s, and still what
the substitution matrix and the complex table ask.
The REFERENCE GENOME page’s nucleotide distributions are a third table of this shape and do not come through here yet: they are stored already multiplied out, so the counts this rule needs to decide exactness on are gone before a template sees them. gain#1086 holds that.
- gain.genomic_resources.statistics.percentages.percentage_of(count: int, total: int) str[source]
countas a percentage oftotal, floored and capped.The result is text for an HTML page and can carry markup-significant characters – the floor and the ceiling begin with
<and>– so a template rendering it must escape, which the.jinjaHTML templates do and the Markdown ones deliberately do not.totalmust be positive; a share of nothing is not a percentage, and what to show instead is the caller’s question rather than this one’s (see the module docstring).Two answers a bare
"%.2f%%"gets wrong:A nonzero count too small to survive two decimals renders
<0.01%, never0.00%. On a real scorecomplexis 881 alleles out of 727,413,443 whileotheris genuinely empty – and telling those two apart is the whole reason a percentage is shown at all.A count that falls SHORT of the total but rounds up to it renders
>99.99%, never100.00%– the floor reflected (gain#990). On that same score the substitutions are all but 881 of the alleles, and a column readingsubstitution 100.00%besidecomplex <0.01%says the resource is entirely one class in the act of showing that it is not.
Both boundaries are decided on the INTEGERS, never on the rendered float: a count that IS the total renders
100.00%and a count of zero renders0.00%, because only a share that is not the whole is written as short of it, and only a share that exists at all is written as too small to see.
gain.genomic_resources.statistics.record_validation module
What the statistics scan refuses, one rule per score kind (ADR 0027).
ADR 0008 gave the statistics scan sole ownership of validation – reads never validate – and gave each score kind its own rule rather than a shared one it did not choose. Both halves stand; this module is where the second one is written down.
The rules used to be @abstractmethod on GenomicScore with one body per
kind beside the read code, while their only callers were the scan’s two doors
(genomic_scores_impl/scan.py) and statistics/alleles.py. A read class
carrying a rule only its consumer applies is what gain#1269 moved here.
Two functions, both functools.singledispatch(), both dispatching on the
score’s class:
validate_records()– the per-record door’s rule;validate_record_arrays()– the same rule over a batch’s columns.
Both are transducers: they hand back exactly what they were given, in
order, and raise at the first record their kind cannot mean. A record the
kind’s ORDERING rule refuses raises
MalformedResourceError; a
record whose own end precedes its own begin raises the plain OSError that
inverted_span_error() builds,
which is a claim about one record rather than about the resource’s order.
Neither re-reads and neither materialises the region – the scan pays for one
read, and these ride it.
Both read RAW records rather than the spans a kind yields, because a kind’s normalization destroys the evidence: an allele score collapses a record to the point it sits at, discarding its end entirely. Raw is also the only layer at which the per-record and the vectorized rule can say the same thing – clipping a record to the scanned region would tie the verdict to how the contig happened to be partitioned.
Three kinds, two rules. A position score promises one value per position, so it refuses records that overlap or merely touch. An allele score and a fragment score both allow several records at one position and refuse only a record that moves backwards; they differ solely in the noun their message names. So the backwards rule is written once per shape and registered twice, with the noun supplied at the registration. ADR 0008 accepted six copies to avoid a hidden shared statement – one class attribute two validators read and interpreted differently. A body called explicitly from two registrations hides nothing, so that reason does not reach this arrangement (ADR 0027).
A kind nobody wrote a rule for cannot be scanned. The undecorated body
of each function – singledispatch’s default, which it keys on object
rather than on GenomicScore – raises NotImplementedError naming the
class it was handed. The three kinds are flat siblings, so a kind added later
reaches that default rather than inheriting a rule chosen for something else.
That refusal is at run time – a missing
registration has no static analogue the way a missing @abstractmethod
override had – so test_every_buildable_kind_is_registered stands in for
the check the type checker used to make. ADR 0001’s gain#1261 Amendment
depends on this; see ADR 0027.
- gain.genomic_resources.statistics.record_validation.validate_record_arrays(score: GenomicScore, batches: Iterator[tuple[ndarray, ndarray, dict[str, ndarray]]], chrom: str) Generator[tuple[ndarray, ndarray, dict[str, ndarray]], None, None][source]
- gain.genomic_resources.statistics.record_validation.validate_record_arrays(score: PositionScore, batches: Iterator[tuple[ndarray, ndarray, dict[str, ndarray]]], chrom: str) Generator[tuple[ndarray, ndarray, dict[str, ndarray]], None, None]
- gain.genomic_resources.statistics.record_validation.validate_record_arrays(score: GenomicScore, batches: Iterator[RecordArrays], chrom: str, *, kind: str = "an allele score's") Generator[RecordArrays, None, None]
- gain.genomic_resources.statistics.record_validation.validate_record_arrays(score: GenomicScore, batches: Iterator[RecordArrays], chrom: str, *, kind: str = "a fragment score's") Generator[RecordArrays, None, None]
Yield a stream of raw column batches through, refusing a bad one.
The vectorized counterpart of
validate_records(), over the batches the bulk scan is already pulling. It states the SAME ordering rule as its per-record twin, so a resource whose records are out of order is refused identically whichever path it was eligible for. Divergence between the two is what ADR 0008 records as the reason the shared class attribute was removed.The ordering rule is all it states. The per-record path additionally refuses a record whose end precedes its begin (see
inverted_span_error()); there is no array counterpart, because no backend the bulk path reads can produce one – tabix refuses to index such a row, and a bigWig cannot express it. If that ever stops being true, this is where the check belongs.chromis what the batches were read for. A bulk scan reads one region, which lies within one contig, so the rules carry their ordering state across batches but never across contigs.Refuses an unregistered kind, for the reason
validate_records()gives.
- gain.genomic_resources.statistics.record_validation.validate_records(score: GenomicScore, records: Iterator[tuple[Any, ...]]) Generator[tuple[Any, ...], None, None][source]
- gain.genomic_resources.statistics.record_validation.validate_records(score: PositionScore, records: Iterator[tuple[Any, ...]]) Generator[tuple[Any, ...], None, None]
- gain.genomic_resources.statistics.record_validation.validate_records(score: GenomicScore, records: Iterator[Record], *, kind: str = "an allele score's") Generator[Record, None, None]
- gain.genomic_resources.statistics.record_validation.validate_records(score: GenomicScore, records: Iterator[Record], *, kind: str = "a fragment score's") Generator[Record, None, None]
Yield a raw record stream through, refusing a malformed one.
Dispatches on
score’s class to the rule registered for that kind. A kind with no registration reaches this body and is refused: it would otherwise be validated by a rule nobody chose for it, which is the failure ADR 0008 exists to undo.
gain.genomic_resources.statistics.region_fold module
The scan-side fold that turns a kind’s regions into one statistic.
Not part of base_statistic because this fold reports through
cli_errors, and the line that draws is a real one: base_statistic
says what a statistic IS, and stays free of anything CLI-facing.
- gain.genomic_resources.statistics.region_fold.merge_regions(resource_id: str, regions: Iterable[MergeableRegion | None], build: Callable[[], S], what: str) S | None[source]
Fold one kind’s scanned regions into a single statistic.
Nonefor a kind that contributed no regions at all – the caller writes no file for it, which is how “this resource has no such statistic” is said. A kind that produced nothing on SOME task is aNoneamong the regions and is simply dropped.whatnames the statistic in the failure line, and is the same stringrefuse_unmergeable()names it by – the two halves of what an operator reads on one failure. The failure is reported AND re-raised: reporting alone would leave the build writing a half-merged statistic.