@gmod/hic 1.3.2 → 2.1.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.
- package/README.md +17 -32
- package/dist/bufferedFile.js +8 -25
- package/dist/bufferedFile.js.map +1 -1
- package/dist/hicFile.d.ts +22 -9
- package/dist/hicFile.js +218 -175
- package/dist/hicFile.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/matrixZoomData.d.ts +3 -1
- package/dist/matrixZoomData.js +6 -3
- package/dist/matrixZoomData.js.map +1 -1
- package/dist/normalizationVector.d.ts +3 -1
- package/dist/normalizationVector.js +60 -39
- package/dist/normalizationVector.js.map +1 -1
- package/dist/types.d.ts +16 -0
- package/esm/bufferedFile.js +8 -25
- package/esm/bufferedFile.js.map +1 -1
- package/esm/hicFile.d.ts +22 -9
- package/esm/hicFile.js +218 -175
- package/esm/hicFile.js.map +1 -1
- package/esm/index.d.ts +1 -1
- package/esm/matrixZoomData.d.ts +3 -1
- package/esm/matrixZoomData.js +6 -3
- package/esm/matrixZoomData.js.map +1 -1
- package/esm/normalizationVector.d.ts +3 -1
- package/esm/normalizationVector.js +60 -39
- package/esm/normalizationVector.js.map +1 -1
- package/esm/types.d.ts +16 -0
- package/package.json +1 -1
- package/src/bufferedFile.ts +8 -22
- package/src/hicFile.ts +303 -218
- package/src/index.ts +1 -0
- package/src/matrixZoomData.ts +6 -1
- package/src/normalizationVector.ts +77 -43
- package/src/types.ts +12 -0
- package/dist/lru.d.ts +0 -29
- package/dist/lru.js +0 -53
- package/dist/lru.js.map +0 -1
- package/esm/lru.d.ts +0 -29
- package/esm/lru.js +0 -50
- package/esm/lru.js.map +0 -1
- package/src/lru.ts +0 -75
package/src/hicFile.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { LocalFile } from 'generic-filehandle2'
|
|
|
5
5
|
import { binWindow } from './binWindow.ts'
|
|
6
6
|
import BinaryParser from './binary.ts'
|
|
7
7
|
import BufferedFile from './bufferedFile.ts'
|
|
8
|
-
import LRU from './lru.ts'
|
|
9
8
|
import Matrix from './matrix.ts'
|
|
10
9
|
import NormalizationVector from './normalizationVector.ts'
|
|
11
10
|
import { readerFromFilehandle } from './reader.ts'
|
|
@@ -16,6 +15,7 @@ import type {
|
|
|
16
15
|
BaseOpts,
|
|
17
16
|
BlockIndexEntry,
|
|
18
17
|
Chromosome,
|
|
18
|
+
ExpectedValues,
|
|
19
19
|
HicMetadata,
|
|
20
20
|
HicRegion,
|
|
21
21
|
ProgressOpts,
|
|
@@ -50,6 +50,36 @@ interface SignalOpts extends ProgressOpts {
|
|
|
50
50
|
signal: AbortSignal
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/** One expected-value vector: its values on disk and its chromosome scale factors. */
|
|
54
|
+
interface ExpectedChunk {
|
|
55
|
+
vector: NormalizationVector
|
|
56
|
+
nValues: number
|
|
57
|
+
chrScaleFactors: Map<number, number>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A walked expected-value section, and where the next section starts. */
|
|
61
|
+
interface ExpectedSection {
|
|
62
|
+
chunks: Map<string, ExpectedChunk>
|
|
63
|
+
end: number
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getExpectedKey(type: string, unit: string, binSize: number) {
|
|
67
|
+
return `${type}_${unit}_${binSize}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The range of `|bin2 - bin1|` over two half-open bin windows.
|
|
71
|
+
function distanceRange(
|
|
72
|
+
[x1, x2]: readonly [number, number],
|
|
73
|
+
[y1, y2]: readonly [number, number],
|
|
74
|
+
) {
|
|
75
|
+
const overlap = x1 < y2 && y1 < x2
|
|
76
|
+
const nearest = overlap
|
|
77
|
+
? 0
|
|
78
|
+
: Math.min(Math.abs(y1 - (x2 - 1)), Math.abs(x1 - (y2 - 1)))
|
|
79
|
+
const farthest = Math.max(Math.abs(y2 - 1 - x1), Math.abs(x2 - 1 - y1))
|
|
80
|
+
return [nearest, farthest] as const
|
|
81
|
+
}
|
|
82
|
+
|
|
53
83
|
export interface HicConfig {
|
|
54
84
|
/** a `generic-filehandle2` handle, e.g. `RemoteFile` for a URL */
|
|
55
85
|
filehandle?: GenericFilehandle
|
|
@@ -66,8 +96,7 @@ export interface HicConfig {
|
|
|
66
96
|
/**
|
|
67
97
|
* Ceiling on the decompressed contacts the block cache holds, in bytes.
|
|
68
98
|
* Defaults to 128 MB. Lower it where the budget is tight and re-reading is
|
|
69
|
-
* cheaper than the memory
|
|
70
|
-
* fits, since the entry cap bounds the cache too.
|
|
99
|
+
* cheaper than the memory.
|
|
71
100
|
*/
|
|
72
101
|
blockCacheMaxBytes?: number
|
|
73
102
|
}
|
|
@@ -75,7 +104,7 @@ export interface HicConfig {
|
|
|
75
104
|
// Cached blocks are the decompressed records and nothing else. hic-straw also
|
|
76
105
|
// hung the MatrixZoomData and the block-index entry off each Block; neither was
|
|
77
106
|
// ever read back, and the zoom data pinned a whole `blockIndex` record per
|
|
78
|
-
// cached block for the lifetime of the
|
|
107
|
+
// cached block for the lifetime of the cache.
|
|
79
108
|
interface Block {
|
|
80
109
|
blockNumber: number
|
|
81
110
|
records: ContactRecords
|
|
@@ -122,15 +151,13 @@ function truncateRecords(records: ContactRecords, n: number): ContactRecords {
|
|
|
122
151
|
// The cliffs are exactly these three capacities. Note the first column: 41% of a
|
|
123
152
|
// cold whole-genome fetch's reads were re-reads of normalization vectors the
|
|
124
153
|
// same fetch had already fetched.
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
// the entry cap now tracks the working set and this tracks the memory, instead
|
|
133
|
-
// of one number answering both badly. See LRU's WeightOpts.
|
|
154
|
+
//
|
|
155
|
+
// Blocks became struct-of-arrays (see contactRecords.ts), so a cached contact is
|
|
156
|
+
// 12 bytes of untraced ArrayBuffer rather than ~55 on the GC-traced heap — but a
|
|
157
|
+
// block still holds every contact in its bin square, which is the one thing here
|
|
158
|
+
// that can hold real memory, and block sizes vary by more than an order of
|
|
159
|
+
// magnitude with binsize. Measured on the file above: 0.05 MB at 2.5 Mb, 0.23 MB
|
|
160
|
+
// at 100 kb. So the block cache's only bound is its byte budget.
|
|
134
161
|
//
|
|
135
162
|
// This is the one capacity here a caller can override (`blockCacheMaxBytes`),
|
|
136
163
|
// because it is the only one that bounds MEMORY. The two entry caps below are
|
|
@@ -152,7 +179,10 @@ const NORM_VECTOR_CACHE_SIZE = 64
|
|
|
152
179
|
// so no separate resolution generation is needed to keep entries apart —
|
|
153
180
|
// hic-straw's extra `resolution` field only made the cache single-resolution
|
|
154
181
|
// (every zoom step threw the previous level away).
|
|
155
|
-
function blockCacheWeight(block: Block) {
|
|
182
|
+
function blockCacheWeight(block: Block | undefined) {
|
|
183
|
+
if (!block) {
|
|
184
|
+
return 0
|
|
185
|
+
}
|
|
156
186
|
const { bin1, bin2, counts } = block.records
|
|
157
187
|
return bin1.byteLength + bin2.byteLength + counts.byteLength
|
|
158
188
|
}
|
|
@@ -167,7 +197,7 @@ function getNormalizationVectorKey(
|
|
|
167
197
|
}
|
|
168
198
|
|
|
169
199
|
export class HicFile {
|
|
170
|
-
private
|
|
200
|
+
private nvi: string | undefined
|
|
171
201
|
private file: Reader
|
|
172
202
|
|
|
173
203
|
// These caches share the in-flight read, not just its result. A
|
|
@@ -181,6 +211,9 @@ export class HicFile {
|
|
|
181
211
|
string,
|
|
182
212
|
NormVectorIndex | undefined
|
|
183
213
|
>()
|
|
214
|
+
// 'raw' and 'normalized', each walked once: the sections carry no index, so
|
|
215
|
+
// a chunk's position is known only by walking every chunk before it.
|
|
216
|
+
private expectedSectionCache = new SharedReadCache<string, ExpectedSection>()
|
|
184
217
|
private normVectorCache = new SharedReadCache<
|
|
185
218
|
string,
|
|
186
219
|
NormalizationVector | undefined
|
|
@@ -190,31 +223,32 @@ export class HicFile {
|
|
|
190
223
|
})
|
|
191
224
|
// Assigned in the constructor rather than here, since its byte budget is
|
|
192
225
|
// configurable and a field initializer cannot see `config`.
|
|
193
|
-
private blockCache:
|
|
226
|
+
private blockCache: SharedReadCache<string, Block | undefined>
|
|
194
227
|
private normVectorIndexPosition = -1
|
|
195
228
|
private normVectorIndexSize = -1
|
|
196
229
|
|
|
197
230
|
private version = 0
|
|
198
231
|
private genomeId = ''
|
|
199
232
|
private footerPosition = 0
|
|
233
|
+
// Where the raw (NONE) expected values begin, right after the master index.
|
|
234
|
+
private expectedValuesPosition = 0
|
|
200
235
|
// Where the file's normalization data begins, and so also where a file
|
|
201
236
|
// carrying none of it ends. Public so a test can cut a real file there to
|
|
202
237
|
// make that second shape.
|
|
203
238
|
normExpectedValueVectorsPosition: number | undefined
|
|
204
239
|
|
|
205
240
|
private chromosomes: Chromosome[] = []
|
|
206
|
-
private chromosomeIndexMap
|
|
207
|
-
private chrAliasTable: Record<string, string> = {}
|
|
241
|
+
private chromosomeIndexMap = new Map<string, number>()
|
|
208
242
|
private bpResolutions: number[] = []
|
|
209
243
|
private masterIndex: Record<string, MasterIndexEntry> = {}
|
|
210
244
|
private meta: HicMetadata | undefined
|
|
211
245
|
|
|
212
246
|
constructor(config: HicConfig) {
|
|
213
|
-
const { filehandle, path, reader, blockCacheMaxBytes } = config
|
|
214
|
-
this.
|
|
215
|
-
this.blockCache = new
|
|
216
|
-
|
|
217
|
-
|
|
247
|
+
const { filehandle, path, reader, nvi, blockCacheMaxBytes } = config
|
|
248
|
+
this.nvi = nvi
|
|
249
|
+
this.blockCache = new SharedReadCache({
|
|
250
|
+
maxSize: blockCacheMaxBytes ?? BLOCK_CACHE_MAX_BYTES,
|
|
251
|
+
sizeOf: blockCacheWeight,
|
|
218
252
|
})
|
|
219
253
|
if (reader) {
|
|
220
254
|
this.file = reader
|
|
@@ -255,8 +289,11 @@ export class HicFile {
|
|
|
255
289
|
}
|
|
256
290
|
const footerPosition = binaryParser.getLong()
|
|
257
291
|
|
|
258
|
-
const {
|
|
259
|
-
|
|
292
|
+
const {
|
|
293
|
+
masterIndex,
|
|
294
|
+
expectedValuesPosition,
|
|
295
|
+
normExpectedValueVectorsPosition,
|
|
296
|
+
} = await this.readFooter(version, footerPosition, signal)
|
|
260
297
|
|
|
261
298
|
const bodyPosition = Object.values(masterIndex).reduce(
|
|
262
299
|
(min, entry) => Math.min(min, entry.start),
|
|
@@ -282,7 +319,7 @@ export class HicFile {
|
|
|
282
319
|
}
|
|
283
320
|
|
|
284
321
|
const chromosomes: Chromosome[] = []
|
|
285
|
-
const chromosomeIndexMap
|
|
322
|
+
const chromosomeIndexMap = new Map<string, number>()
|
|
286
323
|
let nChrs = binaryParser.getInt()
|
|
287
324
|
let i = 0
|
|
288
325
|
while (nChrs-- > 0) {
|
|
@@ -292,7 +329,7 @@ export class HicFile {
|
|
|
292
329
|
size: version < 9 ? binaryParser.getInt() : binaryParser.getLong(),
|
|
293
330
|
}
|
|
294
331
|
chromosomes.push(chr)
|
|
295
|
-
chromosomeIndexMap
|
|
332
|
+
chromosomeIndexMap.set(chr.name, chr.index)
|
|
296
333
|
i++
|
|
297
334
|
}
|
|
298
335
|
|
|
@@ -302,20 +339,10 @@ export class HicFile {
|
|
|
302
339
|
bpResolutions.push(binaryParser.getInt())
|
|
303
340
|
}
|
|
304
341
|
|
|
305
|
-
const chrAliasTable: Record<string, string> = {}
|
|
306
|
-
for (const chrName of Object.keys(chromosomeIndexMap)) {
|
|
307
|
-
if (chrName.startsWith('chr')) {
|
|
308
|
-
chrAliasTable[chrName.slice(3)] = chrName
|
|
309
|
-
} else if (chrName === 'MT') {
|
|
310
|
-
chrAliasTable.chrM = chrName
|
|
311
|
-
} else {
|
|
312
|
-
chrAliasTable[`chr${chrName}`] = chrName
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
342
|
throwIfAborted(signal)
|
|
317
343
|
this.version = version
|
|
318
344
|
this.footerPosition = footerPosition
|
|
345
|
+
this.expectedValuesPosition = expectedValuesPosition
|
|
319
346
|
this.masterIndex = masterIndex
|
|
320
347
|
this.normExpectedValueVectorsPosition = normExpectedValueVectorsPosition
|
|
321
348
|
this.genomeId = genomeId
|
|
@@ -324,7 +351,6 @@ export class HicFile {
|
|
|
324
351
|
this.chromosomes = chromosomes
|
|
325
352
|
this.chromosomeIndexMap = chromosomeIndexMap
|
|
326
353
|
this.bpResolutions = bpResolutions
|
|
327
|
-
this.chrAliasTable = chrAliasTable
|
|
328
354
|
this.meta = {
|
|
329
355
|
version,
|
|
330
356
|
genome: genomeId,
|
|
@@ -364,10 +390,15 @@ export class HicFile {
|
|
|
364
390
|
masterIndex[key] = { start: pos, size }
|
|
365
391
|
}
|
|
366
392
|
|
|
393
|
+
const expectedValuesPosition = footerPosition + skip + binaryParser.position
|
|
367
394
|
// Normalized expected values start after the expected values
|
|
368
395
|
const normExpectedValueVectorsPosition =
|
|
369
396
|
version > 5 ? footerPosition + (version < 9 ? 4 : 8) + nBytes : undefined
|
|
370
|
-
return {
|
|
397
|
+
return {
|
|
398
|
+
masterIndex,
|
|
399
|
+
expectedValuesPosition,
|
|
400
|
+
normExpectedValueVectorsPosition,
|
|
401
|
+
}
|
|
371
402
|
}
|
|
372
403
|
|
|
373
404
|
async getMatrix(chrIdx1: number, chrIdx2: number, opts?: BaseOpts) {
|
|
@@ -397,14 +428,13 @@ export class HicFile {
|
|
|
397
428
|
normalization: string,
|
|
398
429
|
region1: HicRegion,
|
|
399
430
|
region2: HicRegion,
|
|
400
|
-
units: string,
|
|
401
431
|
binsize: number,
|
|
402
432
|
opts?: ProgressOpts,
|
|
403
433
|
) {
|
|
404
434
|
await this.init(opts)
|
|
405
435
|
|
|
406
|
-
const idx1 = this.chromosomeIndexMap
|
|
407
|
-
const idx2 = this.chromosomeIndexMap
|
|
436
|
+
const idx1 = this.chromosomeIndexMap.get(region1.chr)
|
|
437
|
+
const idx2 = this.chromosomeIndexMap.get(region2.chr)
|
|
408
438
|
|
|
409
439
|
// A `.hic` stores only `bin1 <= bin2`, so a query whose x window sits to the
|
|
410
440
|
// right of its y window has to be swapped or it asks for the half of the
|
|
@@ -444,7 +474,7 @@ export class HicFile {
|
|
|
444
474
|
// resolved once per pair rather than per block, each paired with the bin
|
|
445
475
|
// offset its values start at.
|
|
446
476
|
const [norm, blocks] = await Promise.all([
|
|
447
|
-
this.getNormVectors(normalization, r1, r2,
|
|
477
|
+
this.getNormVectors(normalization, r1, r2, binsize, opts),
|
|
448
478
|
// The measurable half of the pair: blocks are counted and the two norm
|
|
449
479
|
// vectors are not, because the vector chain is two hops whatever the
|
|
450
480
|
// query and the block chain is the one that grows with the region.
|
|
@@ -522,9 +552,8 @@ export class HicFile {
|
|
|
522
552
|
//
|
|
523
553
|
// `transposed` says the query was swapped, so `bin1` runs along `region2`.
|
|
524
554
|
// Reported rather than left for the caller to re-derive: it is decided here
|
|
525
|
-
// from this file's
|
|
526
|
-
//
|
|
527
|
-
// wrong axis.
|
|
555
|
+
// from this file's chromosome indices, which a caller would otherwise have
|
|
556
|
+
// to look up again to know which axis to un-swap.
|
|
528
557
|
return {
|
|
529
558
|
records: contactRecords,
|
|
530
559
|
appliedNormalization: norm ? normalization : 'NONE',
|
|
@@ -536,59 +565,29 @@ export class HicFile {
|
|
|
536
565
|
normalization: string,
|
|
537
566
|
r1: HicRegion,
|
|
538
567
|
r2: HicRegion,
|
|
539
|
-
units: string,
|
|
540
568
|
binsize: number,
|
|
541
569
|
opts?: BaseOpts,
|
|
542
570
|
) {
|
|
571
|
+
if (!normalization || normalization === 'NONE') {
|
|
572
|
+
return undefined
|
|
573
|
+
}
|
|
543
574
|
const signalOpts = { signal: opts?.signal }
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
if (
|
|
553
|
-
|
|
554
|
-
const chr2 = this.getFileChrName(r2.chr)
|
|
555
|
-
const offset1 = Math.floor(r1.start / binsize)
|
|
556
|
-
const offset2 = Math.floor(r2.start / binsize)
|
|
557
|
-
const nv1 = await this.getNormalizationVector(
|
|
558
|
-
normalization,
|
|
559
|
-
chr1,
|
|
560
|
-
units,
|
|
561
|
-
binsize,
|
|
562
|
-
signalOpts,
|
|
563
|
-
)
|
|
564
|
-
const nv2 =
|
|
565
|
-
chr1 === chr2
|
|
566
|
-
? nv1
|
|
567
|
-
: await this.getNormalizationVector(
|
|
568
|
-
normalization,
|
|
569
|
-
chr2,
|
|
570
|
-
units,
|
|
571
|
-
binsize,
|
|
572
|
-
signalOpts,
|
|
573
|
-
)
|
|
574
|
-
if (nv1 && nv2) {
|
|
575
|
-
result = {
|
|
576
|
-
v1: await nv1.getValues(
|
|
577
|
-
offset1,
|
|
578
|
-
Math.ceil(r1.end / binsize),
|
|
579
|
-
signalOpts,
|
|
580
|
-
),
|
|
581
|
-
v2: await nv2.getValues(
|
|
582
|
-
offset2,
|
|
583
|
-
Math.ceil(r2.end / binsize),
|
|
584
|
-
signalOpts,
|
|
585
|
-
),
|
|
586
|
-
offset1,
|
|
587
|
-
offset2,
|
|
588
|
-
}
|
|
589
|
-
}
|
|
575
|
+
const [offset1, end1] = binWindow(r1, binsize)
|
|
576
|
+
const [offset2, end2] = binWindow(r2, binsize)
|
|
577
|
+
// Awaited in sequence, an inter-chromosomal pair would pay four
|
|
578
|
+
// round-trip waves for what is two hops per vector.
|
|
579
|
+
const [nv1, nv2] = await Promise.all([
|
|
580
|
+
this.getNormalizationVector(normalization, r1.chr, binsize, signalOpts),
|
|
581
|
+
this.getNormalizationVector(normalization, r2.chr, binsize, signalOpts),
|
|
582
|
+
])
|
|
583
|
+
if (!nv1 || !nv2) {
|
|
584
|
+
return undefined
|
|
590
585
|
}
|
|
591
|
-
|
|
586
|
+
const [v1, v2] = await Promise.all([
|
|
587
|
+
nv1.getValues(offset1, end1, signalOpts),
|
|
588
|
+
nv2.getValues(offset2, end2, signalOpts),
|
|
589
|
+
])
|
|
590
|
+
return { v1, v2, offset1, offset2 }
|
|
592
591
|
}
|
|
593
592
|
|
|
594
593
|
async getBlocks(
|
|
@@ -601,10 +600,8 @@ export class HicFile {
|
|
|
601
600
|
`${zd.getKey()}_${blockNumber}`
|
|
602
601
|
|
|
603
602
|
await this.init(opts)
|
|
604
|
-
const
|
|
605
|
-
const
|
|
606
|
-
const idx1 = this.chromosomeIndexMap[chr1]
|
|
607
|
-
const idx2 = this.chromosomeIndexMap[chr2]
|
|
603
|
+
const idx1 = this.chromosomeIndexMap.get(region1.chr)
|
|
604
|
+
const idx2 = this.chromosomeIndexMap.get(region2.chr)
|
|
608
605
|
|
|
609
606
|
let blocks: (Block | undefined)[] = []
|
|
610
607
|
if (idx1 === undefined) {
|
|
@@ -627,45 +624,42 @@ export class HicFile {
|
|
|
627
624
|
}
|
|
628
625
|
|
|
629
626
|
const blockNumbers = zd.getBlockNumbers(region1, region2, this.version)
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
const cached = this.blockCache.get(blockKey(num, zd))
|
|
633
|
-
if (cached) {
|
|
634
|
-
blocks.push(cached)
|
|
635
|
-
} else {
|
|
636
|
-
blockNumbersToQuery.push(num)
|
|
637
|
-
}
|
|
638
|
-
}
|
|
627
|
+
const keys = blockNumbers.map(num => blockKey(num, zd))
|
|
628
|
+
const cached = keys.map(key => this.blockCache.has(key))
|
|
639
629
|
|
|
640
630
|
// Cached blocks are already done, not absent: a pan that reuses most
|
|
641
631
|
// of its blocks reads most of the way along rather than restarting at
|
|
642
632
|
// zero, which is what the bar should say — the work left is the reads.
|
|
633
|
+
// A block another fetch has in flight counts as cached too.
|
|
643
634
|
//
|
|
644
635
|
// A query covering no blocks at all reports nothing rather than `0, 0`:
|
|
645
636
|
// there is no fraction in it, and a caller dividing would get NaN.
|
|
646
637
|
const { onProgress } = opts ?? {}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
638
|
+
const total = blockNumbers.length
|
|
639
|
+
let done = cached.filter(Boolean).length
|
|
640
|
+
if (total > 0) {
|
|
641
|
+
onProgress?.(done, total)
|
|
650
642
|
}
|
|
651
|
-
//
|
|
652
|
-
//
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
643
|
+
// One `Promise.all`, so every block is issued in one wave: what a
|
|
644
|
+
// remote file pays is round-trip DEPTH, and counting the completions
|
|
645
|
+
// must not turn one wave into a queue. `readChainDepth` pins that.
|
|
646
|
+
//
|
|
647
|
+
// A block number missing from the index has no data, so it skips the
|
|
648
|
+
// cache, which would otherwise keep a weightless entry for it forever.
|
|
649
|
+
blocks = await Promise.all(
|
|
650
|
+
blockNumbers.map(async (num, i) => {
|
|
651
|
+
const block = zd.blockIndex[num]
|
|
652
|
+
? await this.blockCache.get(keys[i]!, opts?.signal, signal =>
|
|
653
|
+
this.readBlock(num, zd, { signal }),
|
|
654
|
+
)
|
|
655
|
+
: undefined
|
|
656
|
+
if (!cached[i]) {
|
|
657
|
+
done++
|
|
658
|
+
onProgress?.(done, total)
|
|
659
|
+
}
|
|
660
660
|
return block
|
|
661
661
|
}),
|
|
662
662
|
)
|
|
663
|
-
for (const block of newBlocks) {
|
|
664
|
-
if (block) {
|
|
665
|
-
this.blockCache.set(blockKey(block.blockNumber, zd), block)
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
blocks = blocks.concat(newBlocks)
|
|
669
663
|
}
|
|
670
664
|
}
|
|
671
665
|
return blocks
|
|
@@ -793,17 +787,16 @@ export class HicFile {
|
|
|
793
787
|
async getNormalizationVector(
|
|
794
788
|
type: string,
|
|
795
789
|
chr: string,
|
|
796
|
-
unit: string,
|
|
797
790
|
binSize: number,
|
|
798
791
|
opts?: BaseOpts,
|
|
799
792
|
) {
|
|
800
793
|
await this.init(opts)
|
|
801
794
|
|
|
802
|
-
const chrIdx = this.chromosomeIndexMap
|
|
795
|
+
const chrIdx = this.chromosomeIndexMap.get(chr)
|
|
803
796
|
if (chrIdx === undefined) {
|
|
804
797
|
return undefined
|
|
805
798
|
}
|
|
806
|
-
const key = getNormalizationVectorKey(type, chrIdx,
|
|
799
|
+
const key = getNormalizationVectorKey(type, chrIdx, 'BP', binSize)
|
|
807
800
|
|
|
808
801
|
// Sharing the in-flight read is what keeps concurrent region pairs sharing
|
|
809
802
|
// one vector: a chromosome appears in every pair it takes part in, so with a
|
|
@@ -872,8 +865,8 @@ export class HicFile {
|
|
|
872
865
|
},
|
|
873
866
|
opts.signal,
|
|
874
867
|
)
|
|
875
|
-
} else if (this.
|
|
876
|
-
const nviArray = decodeURIComponent(this.
|
|
868
|
+
} else if (this.nvi) {
|
|
869
|
+
const nviArray = decodeURIComponent(this.nvi).split(',')
|
|
877
870
|
return this.readNormVectorIndex(
|
|
878
871
|
{
|
|
879
872
|
start: parseInt(nviArray[0]!),
|
|
@@ -923,114 +916,210 @@ export class HicFile {
|
|
|
923
916
|
const { signal } = opts
|
|
924
917
|
let index: NormVectorIndex | undefined
|
|
925
918
|
if (this.normExpectedValueVectorsPosition !== undefined) {
|
|
926
|
-
const nviStart = await this.
|
|
927
|
-
|
|
928
|
-
opts,
|
|
929
|
-
)
|
|
930
|
-
let byteCount = INT
|
|
931
|
-
|
|
932
|
-
let data = await this.file.read(nviStart, INT, { signal })
|
|
919
|
+
const nviStart = (await this.getExpectedSection('normalized', opts)).end
|
|
920
|
+
const data = await this.file.read(nviStart, INT, { signal })
|
|
933
921
|
// Possible if there are no norm vectors. A legal v8 file, though uncommon.
|
|
934
922
|
if (data.byteLength !== 0) {
|
|
935
|
-
const
|
|
936
|
-
const nEntries = binaryParser.getInt()
|
|
937
|
-
const sizeEstimate = nEntries * 30
|
|
938
|
-
data = await this.file.read(nviStart + byteCount, sizeEstimate, {
|
|
939
|
-
signal,
|
|
940
|
-
})
|
|
923
|
+
const nEntries = new BinaryParser(new DataView(data)).getInt()
|
|
941
924
|
const found: NormVectorIndex = { entries: {}, types: ['NONE'] }
|
|
942
925
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
return
|
|
956
|
-
}
|
|
957
|
-
this.parseNormVectorEntry(parser, found)
|
|
926
|
+
let position = nviStart + INT
|
|
927
|
+
let requested = nEntries * 30
|
|
928
|
+
let buf = await this.file.read(position, requested, { signal })
|
|
929
|
+
let parser = new BinaryParser(new DataView(buf))
|
|
930
|
+
for (let n = nEntries; n > 0; n--) {
|
|
931
|
+
// A short read means the buffer already runs to EOF, and refilling
|
|
932
|
+
// it would only return the same bytes again.
|
|
933
|
+
if (parser.available() < 100 && buf.byteLength === requested) {
|
|
934
|
+
position += parser.position
|
|
935
|
+
requested = Math.max(1000, n * 30)
|
|
936
|
+
buf = await this.file.read(position, requested, { signal })
|
|
937
|
+
parser = new BinaryParser(new DataView(buf))
|
|
958
938
|
}
|
|
959
|
-
|
|
939
|
+
this.parseNormVectorEntry(parser, found)
|
|
960
940
|
}
|
|
961
|
-
|
|
962
|
-
await processEntries(nEntries, data)
|
|
963
941
|
throwIfAborted(signal)
|
|
964
|
-
this.config.nvi = `${nviStart},${byteCount}`
|
|
965
942
|
index = found
|
|
966
943
|
}
|
|
967
944
|
}
|
|
968
945
|
return index
|
|
969
946
|
}
|
|
970
947
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
948
|
+
/**
|
|
949
|
+
* The expected count for a region pair at `binsize`, the denominator of
|
|
950
|
+
* observed/expected, as straw computes it. `normalization` must be the one
|
|
951
|
+
* the pair's records carry (`getContactRecords`' `appliedNormalization`),
|
|
952
|
+
* since each normalization has its own vector. Undefined where the file has
|
|
953
|
+
* no expected value for it: no vector at this binsize, or a version 5 file
|
|
954
|
+
* asked for a normalized one.
|
|
955
|
+
*
|
|
956
|
+
* Within a chromosome the vector is read for only the distances the pair
|
|
957
|
+
* spans. Between chromosomes straw divides by the map's average contact, the
|
|
958
|
+
* raw `sumCounts` over its bins, whatever the normalization.
|
|
959
|
+
*/
|
|
960
|
+
async getExpectedValues(
|
|
961
|
+
normalization: string,
|
|
962
|
+
region1: HicRegion,
|
|
963
|
+
region2: HicRegion,
|
|
964
|
+
binsize: number,
|
|
965
|
+
opts?: ProgressOpts,
|
|
966
|
+
): Promise<ExpectedValues | undefined> {
|
|
967
|
+
await this.init(opts)
|
|
968
|
+
const idx1 = this.chromosomeIndexMap.get(region1.chr)
|
|
969
|
+
const idx2 = this.chromosomeIndexMap.get(region2.chr)
|
|
970
|
+
if (idx1 === undefined || idx2 === undefined) {
|
|
971
|
+
return undefined
|
|
972
|
+
}
|
|
973
|
+
if (idx1 !== idx2) {
|
|
974
|
+
const matrix = await this.getMatrix(idx1, idx2, opts)
|
|
975
|
+
const zd = matrix?.getZoomData(binsize)
|
|
976
|
+
if (!zd) {
|
|
977
|
+
return undefined
|
|
978
|
+
}
|
|
979
|
+
// straw's bin counts leave out a chromosome's partial last bin
|
|
980
|
+
const nBins1 = Math.max(
|
|
981
|
+
1,
|
|
982
|
+
Math.floor(this.chromosomes[idx1]!.size / binsize),
|
|
983
|
+
)
|
|
984
|
+
const nBins2 = Math.max(
|
|
985
|
+
1,
|
|
986
|
+
Math.floor(this.chromosomes[idx2]!.size / binsize),
|
|
987
|
+
)
|
|
988
|
+
return { kind: 'inter', average: zd.sumCounts / nBins1 / nBins2 }
|
|
989
|
+
}
|
|
990
|
+
const normalized = !!normalization && normalization !== 'NONE'
|
|
991
|
+
if (normalized && this.normExpectedValueVectorsPosition === undefined) {
|
|
992
|
+
return undefined
|
|
993
|
+
}
|
|
994
|
+
const section = await this.getExpectedSection(
|
|
995
|
+
normalized ? 'normalized' : 'raw',
|
|
996
|
+
opts ?? {},
|
|
997
|
+
)
|
|
998
|
+
const chunk = section.chunks.get(
|
|
999
|
+
getExpectedKey(normalized ? normalization : 'NONE', 'BP', binsize),
|
|
1000
|
+
)
|
|
1001
|
+
if (!chunk || chunk.nValues === 0) {
|
|
1002
|
+
return undefined
|
|
1003
|
+
}
|
|
1004
|
+
const [first, last] = distanceRange(
|
|
1005
|
+
binWindow(region1, binsize),
|
|
1006
|
+
binWindow(region2, binsize),
|
|
1007
|
+
)
|
|
1008
|
+
const signalOpts = { signal: opts?.signal }
|
|
1009
|
+
const stored = await chunk.vector.getValues(first, last + 1, signalOpts)
|
|
1010
|
+
const tail =
|
|
1011
|
+
stored.length < last + 1 - first
|
|
1012
|
+
? (
|
|
1013
|
+
await chunk.vector.getValues(
|
|
1014
|
+
chunk.nValues - 1,
|
|
1015
|
+
chunk.nValues,
|
|
1016
|
+
signalOpts,
|
|
1017
|
+
)
|
|
1018
|
+
)[0]!
|
|
1019
|
+
: 0
|
|
1020
|
+
// straw divides by 1 where the section names no factor for the chromosome
|
|
1021
|
+
const factor = chunk.chrScaleFactors.get(idx1) ?? 1
|
|
1022
|
+
const values = new Float64Array(last + 1 - first)
|
|
1023
|
+
for (let i = 0; i < values.length; i++) {
|
|
1024
|
+
values[i] = (i < stored.length ? stored[i]! : tail) / factor
|
|
1025
|
+
}
|
|
1026
|
+
return { kind: 'intra', firstDistance: first, values }
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
private async getExpectedSection(
|
|
1030
|
+
section: 'raw' | 'normalized',
|
|
1031
|
+
opts: ProgressOpts,
|
|
1032
|
+
) {
|
|
1033
|
+
return this.expectedSectionCache.get(section, opts.signal, signal =>
|
|
1034
|
+
this.walkExpectedSection(
|
|
1035
|
+
section === 'raw'
|
|
1036
|
+
? this.expectedValuesPosition
|
|
1037
|
+
: this.normExpectedValueVectorsPosition!,
|
|
1038
|
+
section === 'normalized',
|
|
1039
|
+
{ signal, onProgress: opts.onProgress },
|
|
1040
|
+
),
|
|
1041
|
+
)
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Walk one expected-value section, recording where each chunk's values sit
|
|
1045
|
+
// and its chromosome scale factors. The normalized section's end is also
|
|
1046
|
+
// where a pre-v9 file's normalization vector index begins.
|
|
1047
|
+
//
|
|
1048
|
+
// A chunk's two reads land a whole expected-value vector apart — one per bin
|
|
1049
|
+
// of the genome's longest chromosome, so megabytes at a fine binsize — which
|
|
1050
|
+
// is further than the buffer reaches. On a remote file that is two round
|
|
1051
|
+
// trips per chunk that nothing can merge, and it is why this walk, not the
|
|
1052
|
+
// index it leads to, is the slow part of opening a pre-v9 file. It is also
|
|
1053
|
+
// why the chunk is the unit worth counting: known before the first read, each
|
|
1054
|
+
// costing about the same. Chunks are walked in order, since each one's size
|
|
1055
|
+
// is known only once the one before it has been parsed.
|
|
1056
|
+
private async walkExpectedSection(
|
|
1057
|
+
start: number,
|
|
1058
|
+
normalized: boolean,
|
|
1059
|
+
opts: SignalOpts,
|
|
1060
|
+
): Promise<ExpectedSection> {
|
|
974
1061
|
const { signal, onProgress } = opts
|
|
975
1062
|
const version = this.version
|
|
1063
|
+
const valueSize = version < 9 ? DOUBLE : FLOAT
|
|
976
1064
|
const file = new BufferedFile({ file: this.file, size: 256000 })
|
|
1065
|
+
const chunks = new Map<string, ExpectedChunk>()
|
|
977
1066
|
const data = await file.read(start, INT, { signal })
|
|
978
|
-
// A file with no normalization at all ends where
|
|
979
|
-
// so the count this is about to read is past EOF.
|
|
980
|
-
//
|
|
981
|
-
//
|
|
982
|
-
//
|
|
983
|
-
// discovery path, and that path starts by skipping a section that isn't
|
|
984
|
-
// there. Answering `start` leaves the caller's guard to do the rest.
|
|
1067
|
+
// A file with no normalization at all ends where the normalized section
|
|
1068
|
+
// would start, so the count this is about to read is past EOF. A v9 file
|
|
1069
|
+
// that records no norm-vector-index position takes the v8 discovery path,
|
|
1070
|
+
// which starts by walking a section that isn't there, so the empty answer
|
|
1071
|
+
// has to come from here.
|
|
985
1072
|
if (data.byteLength < INT) {
|
|
986
|
-
return start
|
|
1073
|
+
return { chunks, end: start }
|
|
987
1074
|
}
|
|
988
|
-
const
|
|
989
|
-
const nEntries = binaryParser.getInt() // Total # of expected value chunks
|
|
990
|
-
|
|
991
|
-
// Skip one chunk, answering where the next one starts.
|
|
992
|
-
//
|
|
993
|
-
// Its two reads land a whole expected-value vector apart — one per bin of
|
|
994
|
-
// the genome, so tens of MB at a fine binsize — which is further than the
|
|
995
|
-
// buffer reaches. On a remote file that is two round trips per chunk that
|
|
996
|
-
// nothing can merge, and it is why this walk, not the index it is looking
|
|
997
|
-
// for, is the slow part of opening a pre-v9 file. It is also why the chunk
|
|
998
|
-
// is the unit worth counting: `nEntries` of them, known before the first
|
|
999
|
-
// read, each costing about the same.
|
|
1000
|
-
const skipChunk = async (chunkStart: number) => {
|
|
1001
|
-
let chunkSize = 0
|
|
1002
|
-
|
|
1003
|
-
let buf = await file.read(chunkStart, 500, { signal })
|
|
1004
|
-
let parser = new BinaryParser(new DataView(buf))
|
|
1005
|
-
parser.getString() // type
|
|
1006
|
-
parser.getString() // unit
|
|
1007
|
-
parser.getInt() // binSize
|
|
1008
|
-
const nValues = version < 9 ? parser.getInt() : parser.getLong()
|
|
1009
|
-
chunkSize += parser.position + nValues * (version < 9 ? DOUBLE : FLOAT)
|
|
1010
|
-
|
|
1011
|
-
buf = await file.read(chunkStart + chunkSize, INT, { signal })
|
|
1012
|
-
parser = new BinaryParser(new DataView(buf))
|
|
1013
|
-
const nChrScaleFactors = parser.getInt()
|
|
1014
|
-
chunkSize +=
|
|
1015
|
-
INT + nChrScaleFactors * (INT + (version < 9 ? DOUBLE : FLOAT))
|
|
1016
|
-
|
|
1017
|
-
return chunkStart + chunkSize
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
// A loop rather than the tail recursion this was, because the position is
|
|
1021
|
-
// the only thing carried between chunks and a counter now rides along with
|
|
1022
|
-
// it. Chunks must be walked in order — each one's size is only known once
|
|
1023
|
-
// the one before it has been parsed — so the awaits are sequential by
|
|
1024
|
-
// nature, not by oversight.
|
|
1075
|
+
const nEntries = new BinaryParser(new DataView(data)).getInt()
|
|
1025
1076
|
if (nEntries > 0) {
|
|
1026
1077
|
onProgress?.(0, nEntries)
|
|
1027
1078
|
}
|
|
1028
1079
|
let position = start + INT
|
|
1029
1080
|
for (let done = 0; done < nEntries; done++) {
|
|
1030
|
-
|
|
1081
|
+
let parser = new BinaryParser(
|
|
1082
|
+
new DataView(await file.read(position, 500, { signal })),
|
|
1083
|
+
)
|
|
1084
|
+
const type = normalized ? parser.getString() : 'NONE'
|
|
1085
|
+
const unit = parser.getString()
|
|
1086
|
+
const binSize = parser.getInt()
|
|
1087
|
+
const nValues = version < 9 ? parser.getInt() : parser.getLong()
|
|
1088
|
+
const valuesPosition = position + parser.position
|
|
1089
|
+
const factorsPosition = valuesPosition + nValues * valueSize
|
|
1090
|
+
|
|
1091
|
+
parser = new BinaryParser(
|
|
1092
|
+
new DataView(await file.read(factorsPosition, INT, { signal })),
|
|
1093
|
+
)
|
|
1094
|
+
const nFactors = parser.getInt()
|
|
1095
|
+
const factorBytes = nFactors * (INT + valueSize)
|
|
1096
|
+
parser = new BinaryParser(
|
|
1097
|
+
new DataView(
|
|
1098
|
+
await file.read(factorsPosition + INT, factorBytes, { signal }),
|
|
1099
|
+
),
|
|
1100
|
+
)
|
|
1101
|
+
const chrScaleFactors = new Map<number, number>()
|
|
1102
|
+
for (let i = 0; i < nFactors; i++) {
|
|
1103
|
+
const chrIdx = parser.getInt()
|
|
1104
|
+
chrScaleFactors.set(
|
|
1105
|
+
chrIdx,
|
|
1106
|
+
version < 9 ? parser.getDouble() : parser.getFloat(),
|
|
1107
|
+
)
|
|
1108
|
+
}
|
|
1109
|
+
chunks.set(getExpectedKey(type, unit, binSize), {
|
|
1110
|
+
vector: new NormalizationVector(
|
|
1111
|
+
this.file,
|
|
1112
|
+
valuesPosition,
|
|
1113
|
+
nValues,
|
|
1114
|
+
valueSize,
|
|
1115
|
+
),
|
|
1116
|
+
nValues,
|
|
1117
|
+
chrScaleFactors,
|
|
1118
|
+
})
|
|
1119
|
+
position = factorsPosition + INT + factorBytes
|
|
1031
1120
|
onProgress?.(done + 1, nEntries)
|
|
1032
1121
|
}
|
|
1033
|
-
return position
|
|
1122
|
+
return { chunks, end: position }
|
|
1034
1123
|
}
|
|
1035
1124
|
|
|
1036
1125
|
private parseNormVectorEntry(
|
|
@@ -1044,15 +1133,11 @@ export class HicFile {
|
|
|
1044
1133
|
const filePosition = binaryParser.getLong() // 8
|
|
1045
1134
|
const sizeInBytes =
|
|
1046
1135
|
this.version < 9 ? binaryParser.getInt() : binaryParser.getLong() // 4:8
|
|
1047
|
-
const key =
|
|
1136
|
+
const key = getNormalizationVectorKey(type, chrIdx, unit, binSize)
|
|
1048
1137
|
|
|
1049
1138
|
if (!index.types.includes(type)) {
|
|
1050
1139
|
index.types.push(type)
|
|
1051
1140
|
}
|
|
1052
1141
|
index.entries[key] = { filePosition, size: sizeInBytes }
|
|
1053
1142
|
}
|
|
1054
|
-
|
|
1055
|
-
getFileChrName(chrAlias: string) {
|
|
1056
|
-
return this.chrAliasTable[chrAlias] ?? chrAlias
|
|
1057
|
-
}
|
|
1058
1143
|
}
|