Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 | 2x 6x 6x 6x 5x 4x 4x 4x 4x 6x 2x 15x 15x 15x 15x 15x 103x 103x 103x 103x 103x 15x 15x 2x 23x 23x 23x 23x 23x 23x 17x 17x 23x 17x 17x 17x 6x 6x 6x 15x 25x 25x 25x 25x 25x 25x 25x 6x 6x 2x 1x 1x 1x 1x 2x 2x 4x 4x 2x 16x 16x 16x 16x 16x 15x 15x 15x 15x 16x 876x 876x 876x 876x 16x 16x | /**
* Two files in one square: side by side across the diagonal, or reduced to the
* one number that is the comparison itself.
*
* A contact matrix is symmetric, so half of every map on screen is a mirror of
* the other half and carries no information at all. That is the room this uses:
* the lower-left triangle is given to a *second* file, and the picture stops
* being symmetric — which is the point, because the eye reads a broken symmetry
* far faster than it reads two maps side by side.
*
* The split is the **matrix diagonal**, not the viewport's. A cell belongs to
* the second file when its row bin is greater than its column bin, wherever the
* view happens to be — so panning off the diagonal is not a special case and
* needs no rule of its own: the far corner of a trans block is entirely above
* the diagonal and shows the first file alone, and its transpose shows the
* second. Splitting the *screen* instead would put two unrelated blocks side by
* side and invite them to be read as one picture.
*
* The split answers "what do these two look like"; it does not answer "where do
* they differ", and past a certain similarity the eye cannot get that out of two
* triangles at all. {@link combine} is the other reading: collapse the pair to
* one derived field — a ratio or a difference — where the *only* thing drawn is
* the disagreement, and every cell that matches is the neutral colour. Both are
* the same two blocks and the same alignment gate; they differ in what is thrown
* away, which is why they are one control with three settings rather than a
* comparison mode and a separate arithmetic feature.
*
* Nothing here draws. It produces one values array in the shape the renderer
* already takes, which is what keeps `render/heatmap.ts` unaware that comparing
* exists — pooling, colouring, pan and zoom are the same code on a compared map
* as on any other.
*/
import type { BlockValues } from './derive'
import type { CompareOp } from '../types'
export type { CompareOp }
/**
* The part of a read this module needs: shape, values, and where they sit.
*
* The same shape `render/derive.ts` works over, and named there because a single
* file's derivations are the more basic of the two: comparing is arithmetic over
* two of these, deriving is arithmetic over one.
*/
export type CompareBlock = BlockValues
export interface Blend {
/** Merged values, row-major, `nrows * ncols` long. */
values: Float32Array
/** Cells taken from the second file — 0 when the view is all above the diagonal. */
lower: number
/** Cells left to the first. Both are reported so a view can say what is on screen. */
upper: number
}
/**
* `true` when two blocks describe the same cells and can therefore be merged.
*
* Shape *and* position: two reads of the same request against files with the
* same contigs and the same bin size agree on all four, and anything that does
* not agree is a block of something else. The caller draws the first file alone
* rather than merging misaligned data, which would put file 2's chromosome III
* under file 1's chromosome VII and look like a discovery.
*/
export function alignable(a: CompareBlock, b: CompareBlock): boolean {
return (
a.nrows === b.nrows &&
a.ncols === b.ncols &&
a.rowExtent[0] === b.rowExtent[0] &&
a.rowExtent[1] === b.rowExtent[1] &&
a.colExtent[0] === b.colExtent[0] &&
a.colExtent[1] === b.colExtent[1] &&
b.values.length >= b.nrows * b.ncols
)
}
/**
* Merge two aligned blocks across the matrix diagonal.
*
* On one scale, always: the same colour means the same number of contacts on
* both sides of the line. Two files sequenced to different depths are made
* comparable by drawing both to one total (§6.5) — the breadcrumb's *contacts*
* box — which changes the contacts rather than the colours, so the two
* triangles are then two libraries of one depth and not one library rescaled.
*
* The diagonal itself stays with the *first* file. A cell on it is one bin
* against itself and is the brightest cell in any Hi-C map; handing alternate
* diagonal cells to different files would draw a dashed line down the middle of
* the picture that is an artefact of nothing.
*/
export function blendAcrossDiagonal(primary: CompareBlock, second: CompareBlock): Blend {
const { nrows, ncols } = primary
const values = new Float32Array(primary.values.subarray(0, nrows * ncols))
const split = diagonalSplit(primary)
let lower = 0
for (let r = 0; r < nrows; r++) {
const row = r * ncols
const end = split[r]
for (let c = 0; c < end; c++) values[row + c] = second.values[row + c]
lower += end
}
return { values, lower, upper: nrows * ncols - lower }
}
/**
* Where each row of a block crosses the matrix diagonal: the cells
* `[0, split[r])` of row `r` are **below** it, and the rest of the row is on it
* or above it.
*
* One answer for every picture that puts something else in the lower triangle —
* a second file merged by {@link blendAcrossDiagonal}, and the second reading of
* one file, whose two triangles are coloured on two scales (`render/heatmap.ts`,
* §5.10). Written once so the cells the merge took from one side are exactly the
* cells the renderer colours on that side's scale.
*
* The two axes may cover different bin ranges — a `contig_A × contig_B` block
* does — so "below the diagonal" is a comparison of *absolute bins*, not of row
* and column indices. Where the extents coincide the two are the same question,
* and the cheap form is taken. A row's lower cells are always a prefix of it,
* because the column's bin only grows along the row.
*/
export function diagonalSplit(
block: Pick<CompareBlock, 'nrows' | 'ncols' | 'rowExtent' | 'colExtent'>,
): Int32Array {
const { nrows, ncols } = block
const split = new Int32Array(nrows)
const square =
block.rowExtent[0] === block.colExtent[0] &&
block.rowExtent[1] === block.colExtent[1] &&
nrows === ncols
if (square) {
for (let r = 0; r < nrows; r++) split[r] = r
return split
}
const rowStep = (block.rowExtent[1] - block.rowExtent[0]) / Math.max(1, nrows)
const colStep = (block.colExtent[1] - block.colExtent[0]) / Math.max(1, ncols)
const below = (c: number, rowBin: number): boolean => block.colExtent[0] + c * colStep < rowBin
for (let r = 0; r < nrows; r++) {
const rowBin = block.rowExtent[0] + r * rowStep
// A first guess from the arithmetic, then walked onto the same comparison
// the cells are tested by, so a rounding in the division cannot move a
// cell across the line.
let k = colStep > 0 ? Math.ceil((rowBin - block.colExtent[0]) / colStep) : below(0, rowBin) ? ncols : 0
k = Math.max(0, Math.min(ncols, k))
while (k > 0 && !below(k - 1, rowBin)) k--
while (k < ncols && below(k, rowBin)) k++
split[r] = k
}
return split
}
/**
* {@link diagonalSplit} for a **mirrored** turned pair (`render/rotate.ts`):
* the rows from the fold down are the band read from the lower triangle, and
* every cell of them is the second side's.
*/
export function foldSplit(nrows: number, ncols: number, fold: number): Int32Array {
const split = new Int32Array(nrows)
for (let r = Math.max(0, fold); r < nrows; r++) split[r] = ncols
return split
}
/**
* Every way of putting two files together, in the order they are offered.
*
* `split` is the picture; the other two are the measurement. They are one
* control because they are one question — *how* do I want these two put together
* — and because every one of them is served by the same pair of reads and the
* same alignment gate, so offering them anywhere but beside each other would
* suggest they cost different amounts.
*/
export const COMPARE_OPS: readonly CompareOp[] = ['split', 'ratio', 'difference']
export function isCompareOp(value: string): value is CompareOp {
return (COMPARE_OPS as readonly string[]).includes(value)
}
export interface Combined extends Blend {
/**
* The value that means "these two agree", or `null` for the split, which is
* not a derived quantity at all but two files' own counts side by side.
*
* `1` for the ratio and `0` for the difference — the number the colour scale
* is made symmetric about and the colourbar's middle tick.
*/
neutral: number | null
}
/**
* Put two aligned blocks together, whichever way was asked for.
*
* Two libraries at different depth differ by roughly a scalar, so the *ratio* of
* their raw counts is that scalar everywhere — a flat wash whose colour is the
* sequencing depth. The answer is not to divide a guess at that scalar out of
* the picture but to compare the libraries at one depth: the breadcrumb's
* *contacts* box draws both files to one total before either is read (§6.5).
* Balanced values need neither, having already been put on one normalisation.
*
* **A ratio is made only where both files have contacts.** A zero on either
* side is no ratio at all, and is `NaN`, missing, as two zeros and a `NaN` from
* either file — an unbalanced bin — always were (§6). `4 / 0` used to be
* `Infinity`, the top of the scale, and `0 / 4` zero, the bottom: on a sparse
* map, where a cell empty in one library is a matter of depth far more than of
* biology, the two saturated colours were most of the picture, and they said
* the loudest thing the scale can say about the cells that held the least. A
* difference keeps its zeros, which it can subtract.
*/
export function combine(
primary: CompareBlock,
second: CompareBlock,
options: { op: CompareOp },
): Combined {
if (options.op === 'split') return { ...blendAcrossDiagonal(primary, second), neutral: null }
const { nrows, ncols } = primary
const cells = nrows * ncols
const values = new Float32Array(cells)
const ratio = options.op === 'ratio'
for (let i = 0; i < cells; i++) {
const mine = primary.values[i]
const other = second.values[i]
values[i] = ratio ? (mine === 0 || other === 0 ? Number.NaN : mine / other) : mine - other
}
// Both files are drawn over the whole square rather than a triangle each, so
// neither count is a count of cells on screen any more. Reported as the whole
// block for both, which is what a caller counting "cells from file 2" has to
// be told when the answer is "all of them, and from file 1 as well".
return { values, lower: cells, upper: cells, neutral: ratio ? 1 : 0 }
}
|