@gmod/hic 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +91 -0
  3. package/dist/binWindow.d.ts +24 -0
  4. package/dist/binWindow.js +32 -0
  5. package/dist/binWindow.js.map +1 -0
  6. package/dist/binary.d.ts +15 -0
  7. package/dist/binary.js +67 -0
  8. package/dist/binary.js.map +1 -0
  9. package/dist/bufferedFile.d.ts +13 -0
  10. package/dist/bufferedFile.js +78 -0
  11. package/dist/bufferedFile.js.map +1 -0
  12. package/dist/contactRecords.d.ts +25 -0
  13. package/dist/contactRecords.js +3 -0
  14. package/dist/contactRecords.js.map +1 -0
  15. package/dist/hicFile.d.ts +94 -0
  16. package/dist/hicFile.js +769 -0
  17. package/dist/hicFile.js.map +1 -0
  18. package/dist/index.d.ts +5 -0
  19. package/dist/index.js +9 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/lru.d.ts +29 -0
  22. package/dist/lru.js +55 -0
  23. package/dist/lru.js.map +1 -0
  24. package/dist/matrix.d.ts +9 -0
  25. package/dist/matrix.js +41 -0
  26. package/dist/matrix.js.map +1 -0
  27. package/dist/matrixZoomData.d.ts +14 -0
  28. package/dist/matrixZoomData.js +96 -0
  29. package/dist/matrixZoomData.js.map +1 -0
  30. package/dist/normalizationVector.d.ts +10 -0
  31. package/dist/normalizationVector.js +51 -0
  32. package/dist/normalizationVector.js.map +1 -0
  33. package/dist/package.json +1 -0
  34. package/dist/reader.d.ts +3 -0
  35. package/dist/reader.js +12 -0
  36. package/dist/reader.js.map +1 -0
  37. package/dist/types.d.ts +35 -0
  38. package/dist/types.js +3 -0
  39. package/dist/types.js.map +1 -0
  40. package/esm/binWindow.d.ts +24 -0
  41. package/esm/binWindow.js +29 -0
  42. package/esm/binWindow.js.map +1 -0
  43. package/esm/binary.d.ts +15 -0
  44. package/esm/binary.js +64 -0
  45. package/esm/binary.js.map +1 -0
  46. package/esm/bufferedFile.d.ts +13 -0
  47. package/esm/bufferedFile.js +75 -0
  48. package/esm/bufferedFile.js.map +1 -0
  49. package/esm/contactRecords.d.ts +25 -0
  50. package/esm/contactRecords.js +2 -0
  51. package/esm/contactRecords.js.map +1 -0
  52. package/esm/hicFile.d.ts +94 -0
  53. package/esm/hicFile.js +762 -0
  54. package/esm/hicFile.js.map +1 -0
  55. package/esm/index.d.ts +5 -0
  56. package/esm/index.js +3 -0
  57. package/esm/index.js.map +1 -0
  58. package/esm/lru.d.ts +29 -0
  59. package/esm/lru.js +52 -0
  60. package/esm/lru.js.map +1 -0
  61. package/esm/matrix.d.ts +9 -0
  62. package/esm/matrix.js +35 -0
  63. package/esm/matrix.js.map +1 -0
  64. package/esm/matrixZoomData.d.ts +14 -0
  65. package/esm/matrixZoomData.js +93 -0
  66. package/esm/matrixZoomData.js.map +1 -0
  67. package/esm/normalizationVector.d.ts +10 -0
  68. package/esm/normalizationVector.js +45 -0
  69. package/esm/normalizationVector.js.map +1 -0
  70. package/esm/reader.d.ts +3 -0
  71. package/esm/reader.js +9 -0
  72. package/esm/reader.js.map +1 -0
  73. package/esm/types.d.ts +35 -0
  74. package/esm/types.js +2 -0
  75. package/esm/types.js.map +1 -0
  76. package/package.json +73 -0
  77. package/src/binWindow.ts +30 -0
  78. package/src/binary.ts +72 -0
  79. package/src/bufferedFile.ts +74 -0
  80. package/src/contactRecords.ts +25 -0
  81. package/src/hicFile.ts +943 -0
  82. package/src/index.ts +12 -0
  83. package/src/lru.ts +77 -0
  84. package/src/matrix.ts +43 -0
  85. package/src/matrixZoomData.ts +133 -0
  86. package/src/normalizationVector.ts +58 -0
  87. package/src/reader.ts +11 -0
  88. package/src/types.ts +40 -0
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export { HicFile, NO_DATA_FOR_RESOLUTION } from './hicFile.ts'
2
+ export { readerFromFilehandle } from './reader.ts'
3
+
4
+ export type { HicConfig } from './hicFile.ts'
5
+ export type { ContactRecords } from './contactRecords.ts'
6
+ export type {
7
+ Chromosome,
8
+ HicMetadata,
9
+ HicRegion,
10
+ Reader,
11
+ Zoom,
12
+ } from './types.ts'
package/src/lru.ts ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Optional second bound, for a cache whose entries differ wildly in size.
3
+ *
4
+ * An entry cap is a memory bound only while entries are interchangeable, and the
5
+ * block cache's are not: a block holds every contact in its bin square, so one
6
+ * can be a few thousand contacts or a few hundred thousand depending on binsize
7
+ * and distance from the diagonal. Capping entries therefore forced one number to
8
+ * answer two questions — how many blocks a fetch needs at once, and how much
9
+ * memory the biggest of them may hold — and the memory question won, which is
10
+ * what left the cache too small to serve a multi-region fetch (see hicFile.ts).
11
+ *
12
+ * Splitting them lets the entry cap track the working set while `maxBytes`
13
+ * stays the backstop it was standing in for.
14
+ */
15
+ interface WeightOpts<V> {
16
+ maxBytes: number
17
+ weigh: (value: V) => number
18
+ }
19
+
20
+ export default class LRU<K, V> {
21
+ private max: number
22
+ private map = new Map<K, V>()
23
+ private weight = 0
24
+ private weightOpts: WeightOpts<V> | undefined
25
+
26
+ constructor(max = 10, weightOpts?: WeightOpts<V>) {
27
+ this.max = max
28
+ this.weightOpts = weightOpts
29
+ }
30
+
31
+ get(key: K) {
32
+ const item = this.map.get(key)
33
+ if (item !== undefined) {
34
+ // refresh key
35
+ this.map.delete(key)
36
+ this.map.set(key, item)
37
+ }
38
+ return item
39
+ }
40
+
41
+ set(key: K, val: V) {
42
+ if (this.map.has(key)) {
43
+ // refresh key, and drop its old weight — `set` on an existing key
44
+ // replaces the value, so the weight it contributed goes with it
45
+ this.delete(key)
46
+ }
47
+ this.map.set(key, val)
48
+ this.weight += this.weightOpts?.weigh(val) ?? 0
49
+
50
+ // Entry cap first, then the byte budget. `size > 1` keeps a single entry
51
+ // larger than the whole budget rather than evicting it and looping forever
52
+ // on an empty map — a cache that can't hold the thing just asked for should
53
+ // still answer the caller that asked.
54
+ while (
55
+ this.map.size > this.max ||
56
+ (this.weightOpts !== undefined &&
57
+ this.weight > this.weightOpts.maxBytes &&
58
+ this.map.size > 1)
59
+ ) {
60
+ const oldest = this.map.keys().next().value
61
+ if (oldest === undefined) {
62
+ break
63
+ }
64
+ this.delete(oldest)
65
+ }
66
+ }
67
+
68
+ // Used to evict a rejected in-flight promise so the next caller retries
69
+ // rather than resolving against a cached failure forever.
70
+ delete(key: K) {
71
+ const existing = this.map.get(key)
72
+ if (existing !== undefined) {
73
+ this.weight -= this.weightOpts?.weigh(existing) ?? 0
74
+ }
75
+ this.map.delete(key)
76
+ }
77
+ }
package/src/matrix.ts ADDED
@@ -0,0 +1,43 @@
1
+ import BinaryParser from './binary.ts'
2
+ import MatrixZoomData from './matrixZoomData.ts'
3
+
4
+ import type { Chromosome } from './types.ts'
5
+
6
+ export default class Matrix {
7
+ // BP levels only. The FRAG code paths were dropped in this fork, so a
8
+ // matrix's FRAG zoom levels are parsed off the wire and discarded rather than
9
+ // kept in a second list nothing can reach.
10
+ private bpZoomData: MatrixZoomData[]
11
+
12
+ constructor(zoomDataList: MatrixZoomData[]) {
13
+ this.bpZoomData = zoomDataList.filter(zd => zd.zoom.unit === 'BP')
14
+ }
15
+
16
+ // Fetch zoom data by bin size. If no matching level exists return undefined.
17
+ getZoomData(binSize: number) {
18
+ return this.bpZoomData.find(zd => binSize === zd.zoom.binSize)
19
+ }
20
+
21
+ static getKey(chrIdx1: number, chrIdx2: number) {
22
+ const lo = Math.min(chrIdx1, chrIdx2)
23
+ const hi = Math.max(chrIdx1, chrIdx2)
24
+ return `${lo}_${hi}`
25
+ }
26
+
27
+ static parseMatrix(data: ArrayBuffer, chromosomes: Chromosome[]) {
28
+ const dis = new BinaryParser(new DataView(data))
29
+ const c1 = dis.getInt() // Should equal chrIdx1
30
+ const c2 = dis.getInt() // Should equal chrIdx2
31
+
32
+ const chr1 = chromosomes[c1]!
33
+ const chr2 = chromosomes[c2]!
34
+
35
+ // # of resolution levels (bp and frags)
36
+ let nResolutions = dis.getInt()
37
+ const zdList: MatrixZoomData[] = []
38
+ while (nResolutions-- > 0) {
39
+ zdList.push(MatrixZoomData.parseMatrixZoomData(chr1, chr2, dis))
40
+ }
41
+ return new Matrix(zdList)
42
+ }
43
+ }
@@ -0,0 +1,133 @@
1
+ import { binWindow } from './binWindow.ts'
2
+
3
+ import type BinaryParser from './binary.ts'
4
+ import type { BlockIndexEntry, Chromosome, HicRegion, Zoom } from './types.ts'
5
+
6
+ export default class MatrixZoomData {
7
+ chr1: Chromosome
8
+ chr2: Chromosome
9
+ zoom: Zoom
10
+ blockBinCount: number
11
+ blockColumnCount: number
12
+ blockIndex: Record<number, BlockIndexEntry>
13
+
14
+ constructor(
15
+ chr1: Chromosome,
16
+ chr2: Chromosome,
17
+ zoom: Zoom,
18
+ blockBinCount: number,
19
+ blockColumnCount: number,
20
+ blockIndex: Record<number, BlockIndexEntry>,
21
+ ) {
22
+ this.chr1 = chr1
23
+ this.chr2 = chr2
24
+ this.zoom = zoom
25
+ this.blockBinCount = blockBinCount
26
+ this.blockColumnCount = blockColumnCount
27
+ this.blockIndex = blockIndex
28
+ }
29
+
30
+ getKey() {
31
+ return `${this.chr1.name}_${this.chr2.name}_${this.zoom.unit}_${this.zoom.binSize}`
32
+ }
33
+
34
+ getBlockNumbers(region1: HicRegion, region2: HicRegion, version: number) {
35
+ const sameChr = this.chr1 === this.chr2
36
+ const binsize = this.zoom.binSize
37
+ const blockBinCount = this.blockBinCount
38
+ const blockColumnCount = this.blockColumnCount
39
+
40
+ // Integer bin bounds, not the raw fractional quotients upstream used: the
41
+ // blocks selected here must cover every bin the record filter will accept,
42
+ // and `binWindow` widens that to any bin overlapping the region. Selecting
43
+ // too many blocks only costs a read the filter then discards; selecting too
44
+ // few silently drops records (see binWindow.ts).
45
+ const [x1, x2] = binWindow(region1, binsize)
46
+ const [y1, y2] = binWindow(region2, binsize)
47
+
48
+ // A Set dedups the block numbers that transposition can collide on the
49
+ // same-chr diagonal, and keeps both branches uniform.
50
+ const blockNumbers = new Set<number>()
51
+ if (version < 9 || !sameChr) {
52
+ const col1 = Math.floor(x1 / blockBinCount)
53
+ const col2 = Math.floor((x2 - 1) / blockBinCount)
54
+ const row1 = Math.floor(y1 / blockBinCount)
55
+ const row2 = Math.floor((y2 - 1) / blockBinCount)
56
+
57
+ for (let row = row1; row <= row2; row++) {
58
+ for (let column = col1; column <= col2; column++) {
59
+ blockNumbers.add(
60
+ sameChr && row < column
61
+ ? column * blockColumnCount + row
62
+ : row * blockColumnCount + column,
63
+ )
64
+ }
65
+ }
66
+ } else {
67
+ // PAD = positionAlongDiagonal (~projected). Depth is the axis
68
+ // perpendicular to the diagonal; nearer means closer to the diagonal.
69
+ // Widening stays safe here too: overlapping regions take the
70
+ // `containsDiagonal` branch (nearerDepth 0) and disjoint ones only see
71
+ // the near-corner gap shrink, so no depth level is lost.
72
+ const translatedLowerPAD = Math.floor((x1 + y1) / 2 / blockBinCount)
73
+ const translatedHigherPAD = Math.floor((x2 + y2) / 2 / blockBinCount)
74
+ const translatedNearerDepth = Math.floor(
75
+ Math.log2(1 + Math.abs(x1 - y2) / Math.sqrt(2) / blockBinCount),
76
+ )
77
+ const translatedFurtherDepth = Math.floor(
78
+ Math.log2(1 + Math.abs(x2 - y1) / Math.sqrt(2) / blockBinCount),
79
+ )
80
+
81
+ // code above assumes above diagonal, but we could be below it
82
+ const containsDiagonal = (x2 - y1) * (x1 - y2) < 0
83
+ const nearerDepth = containsDiagonal
84
+ ? 0
85
+ : Math.min(translatedNearerDepth, translatedFurtherDepth)
86
+ const furtherDepth = Math.max(
87
+ translatedNearerDepth,
88
+ translatedFurtherDepth,
89
+ )
90
+
91
+ for (let depth = nearerDepth; depth <= furtherDepth; depth++) {
92
+ for (let pad = translatedLowerPAD; pad <= translatedHigherPAD; pad++) {
93
+ blockNumbers.add(depth * blockColumnCount + pad)
94
+ }
95
+ }
96
+ }
97
+ return [...blockNumbers]
98
+ }
99
+
100
+ static parseMatrixZoomData(
101
+ chr1: Chromosome,
102
+ chr2: Chromosome,
103
+ dis: BinaryParser,
104
+ ) {
105
+ const unit = dis.getString()
106
+ const zoomIndex = dis.getInt()
107
+ dis.getFloat() // sumCounts
108
+ dis.getFloat() // occupiedCellCount
109
+ dis.getFloat() // stdDev
110
+ dis.getFloat() // percent95
111
+ const binSize = dis.getInt()
112
+ const blockBinCount = dis.getInt()
113
+ const blockColumnCount = dis.getInt()
114
+ let nBlocks = dis.getInt()
115
+
116
+ const blockIndex: Record<number, BlockIndexEntry> = {}
117
+ while (nBlocks-- > 0) {
118
+ const blockNumber = dis.getInt()
119
+ const filePosition = dis.getLong()
120
+ const size = dis.getInt()
121
+ blockIndex[blockNumber] = { filePosition, size }
122
+ }
123
+
124
+ return new MatrixZoomData(
125
+ chr1,
126
+ chr2,
127
+ { index: zoomIndex, unit, binSize },
128
+ blockBinCount,
129
+ blockColumnCount,
130
+ blockIndex,
131
+ )
132
+ }
133
+ }
@@ -0,0 +1,58 @@
1
+ import BinaryParser from './binary.ts'
2
+
3
+ import type { Reader } from './types.ts'
4
+
5
+ const DOUBLE = 8
6
+
7
+ export default class NormalizationVector {
8
+ private cache:
9
+ { start: number; end: number; values: Float64Array } | undefined
10
+
11
+ private file: Reader
12
+ private filePosition: number
13
+ private nValues: number
14
+ private dataType: number
15
+
16
+ constructor(
17
+ file: Reader,
18
+ filePosition: number,
19
+ nValues: number,
20
+ dataType: number,
21
+ ) {
22
+ this.file = file
23
+ this.filePosition = filePosition
24
+ this.nValues = nValues
25
+ this.dataType = dataType
26
+ }
27
+
28
+ async getValues(start: number, end: number) {
29
+ // The read below clamps to `nValues`, so the cache can never cover past it.
30
+ // Compare against the clamped bound or a request that runs off the end of
31
+ // the vector — reachable when the assembly's refseq is longer than the size
32
+ // the `.hic` recorded for that chromosome — leaves `end > cache.end` true
33
+ // forever, and every call re-reads the same range from the file.
34
+ const wanted = Math.min(end, this.nValues)
35
+ if (!this.cache || start < this.cache.start || wanted > this.cache.end) {
36
+ const adjustedStart = Math.max(0, start - 1000)
37
+ const adjustedEnd = Math.min(this.nValues, end + 1000)
38
+ const startPosition = this.filePosition + adjustedStart * this.dataType
39
+ const n = adjustedEnd - adjustedStart
40
+ const data = await this.file.read(startPosition, n * this.dataType)
41
+ const parser = new BinaryParser(new DataView(data))
42
+ const values = new Float64Array(n)
43
+ for (let i = 0; i < n; i++) {
44
+ values[i] =
45
+ this.dataType === DOUBLE ? parser.getDouble() : parser.getFloat()
46
+ }
47
+ this.cache = { start: adjustedStart, end: adjustedEnd, values }
48
+ }
49
+
50
+ // A view, not a copy: the caller only reads it, and a fetch asks for one of
51
+ // these per chromosome per region pair. Both this and `slice` clamp a
52
+ // request running past the end to what exists, which the caller already
53
+ // handles — an out-of-range index reads undefined, making the product NaN,
54
+ // which the record filter drops.
55
+ const sliceStart = start - this.cache.start
56
+ return this.cache.values.subarray(sliceStart, sliceStart + (end - start))
57
+ }
58
+ }
package/src/reader.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { Reader } from './types.ts'
2
+ import type { GenericFilehandle } from 'generic-filehandle2'
3
+
4
+ export function readerFromFilehandle(filehandle: GenericFilehandle): Reader {
5
+ return {
6
+ async read(position: number, length: number) {
7
+ const buf = await filehandle.read(length, position)
8
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength)
9
+ },
10
+ }
11
+ }
package/src/types.ts ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The only read this parser needs: `length` bytes at `position`, coming back
3
+ * SHORT at end-of-file rather than throwing — the header walk deliberately
4
+ * reads past the end to discover whether a file carries normalization data at
5
+ * all. `generic-filehandle2` handles behave this way (a remote 416 included),
6
+ * and `readerFromFilehandle` adapts one to this shape.
7
+ */
8
+ export interface Reader {
9
+ read: (position: number, length: number) => Promise<ArrayBuffer>
10
+ }
11
+
12
+ export interface Chromosome {
13
+ index: number
14
+ name: string
15
+ size: number
16
+ }
17
+
18
+ export interface HicRegion {
19
+ chr: string
20
+ start: number
21
+ end: number
22
+ }
23
+
24
+ export interface Zoom {
25
+ index: number
26
+ unit: string
27
+ binSize: number
28
+ }
29
+
30
+ export interface BlockIndexEntry {
31
+ filePosition: number
32
+ size: number
33
+ }
34
+
35
+ export interface HicMetadata {
36
+ version: number
37
+ genome: string
38
+ chromosomes: Chromosome[]
39
+ resolutions: number[]
40
+ }