@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/dist/hicFile.js
CHANGED
|
@@ -10,7 +10,6 @@ const generic_filehandle2_1 = require("generic-filehandle2");
|
|
|
10
10
|
const binWindow_ts_1 = require("./binWindow.js");
|
|
11
11
|
const binary_ts_1 = __importDefault(require("./binary.js"));
|
|
12
12
|
const bufferedFile_ts_1 = __importDefault(require("./bufferedFile.js"));
|
|
13
|
-
const lru_ts_1 = __importDefault(require("./lru.js"));
|
|
14
13
|
const matrix_ts_1 = __importDefault(require("./matrix.js"));
|
|
15
14
|
const normalizationVector_ts_1 = __importDefault(require("./normalizationVector.js"));
|
|
16
15
|
const reader_ts_1 = require("./reader.js");
|
|
@@ -25,6 +24,18 @@ const Short_MIN_VALUE = -32768;
|
|
|
25
24
|
const DOUBLE = 8;
|
|
26
25
|
const FLOAT = 4;
|
|
27
26
|
const INT = 4;
|
|
27
|
+
function getExpectedKey(type, unit, binSize) {
|
|
28
|
+
return `${type}_${unit}_${binSize}`;
|
|
29
|
+
}
|
|
30
|
+
// The range of `|bin2 - bin1|` over two half-open bin windows.
|
|
31
|
+
function distanceRange([x1, x2], [y1, y2]) {
|
|
32
|
+
const overlap = x1 < y2 && y1 < x2;
|
|
33
|
+
const nearest = overlap
|
|
34
|
+
? 0
|
|
35
|
+
: Math.min(Math.abs(y1 - (x2 - 1)), Math.abs(x1 - (y2 - 1)));
|
|
36
|
+
const farthest = Math.max(Math.abs(y2 - 1 - x1), Math.abs(x2 - 1 - y1));
|
|
37
|
+
return [nearest, farthest];
|
|
38
|
+
}
|
|
28
39
|
/**
|
|
29
40
|
* Copy a partially-filled decode buffer down to its true length, or hand it
|
|
30
41
|
* back untouched when the filter dropped nothing. A `subarray` would be free
|
|
@@ -65,15 +76,13 @@ function truncateRecords(records, n) {
|
|
|
65
76
|
// The cliffs are exactly these three capacities. Note the first column: 41% of a
|
|
66
77
|
// cold whole-genome fetch's reads were re-reads of normalization vectors the
|
|
67
78
|
// same fetch had already fetched.
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
// the entry cap now tracks the working set and this tracks the memory, instead
|
|
76
|
-
// of one number answering both badly. See LRU's WeightOpts.
|
|
79
|
+
//
|
|
80
|
+
// Blocks became struct-of-arrays (see contactRecords.ts), so a cached contact is
|
|
81
|
+
// 12 bytes of untraced ArrayBuffer rather than ~55 on the GC-traced heap — but a
|
|
82
|
+
// block still holds every contact in its bin square, which is the one thing here
|
|
83
|
+
// that can hold real memory, and block sizes vary by more than an order of
|
|
84
|
+
// magnitude with binsize. Measured on the file above: 0.05 MB at 2.5 Mb, 0.23 MB
|
|
85
|
+
// at 100 kb. So the block cache's only bound is its byte budget.
|
|
77
86
|
//
|
|
78
87
|
// This is the one capacity here a caller can override (`blockCacheMaxBytes`),
|
|
79
88
|
// because it is the only one that bounds MEMORY. The two entry caps below are
|
|
@@ -95,6 +104,9 @@ const NORM_VECTOR_CACHE_SIZE = 64;
|
|
|
95
104
|
// hic-straw's extra `resolution` field only made the cache single-resolution
|
|
96
105
|
// (every zoom step threw the previous level away).
|
|
97
106
|
function blockCacheWeight(block) {
|
|
107
|
+
if (!block) {
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
98
110
|
const { bin1, bin2, counts } = block.records;
|
|
99
111
|
return bin1.byteLength + bin2.byteLength + counts.byteLength;
|
|
100
112
|
}
|
|
@@ -102,7 +114,7 @@ function getNormalizationVectorKey(type, chrIdx, unit, resolution) {
|
|
|
102
114
|
return `${type}_${chrIdx}_${unit}_${resolution}`;
|
|
103
115
|
}
|
|
104
116
|
class HicFile {
|
|
105
|
-
|
|
117
|
+
nvi;
|
|
106
118
|
file;
|
|
107
119
|
// These caches share the in-flight read, not just its result. A
|
|
108
120
|
// multi-region fetch runs its region pairs concurrently and they share
|
|
@@ -112,6 +124,9 @@ class HicFile {
|
|
|
112
124
|
// it has aborted, and drops a rejection so the next caller retries.
|
|
113
125
|
headerCache = new shared_read_cache_1.SharedReadCache();
|
|
114
126
|
normVectorIndexCache = new shared_read_cache_1.SharedReadCache();
|
|
127
|
+
// 'raw' and 'normalized', each walked once: the sections carry no index, so
|
|
128
|
+
// a chunk's position is known only by walking every chunk before it.
|
|
129
|
+
expectedSectionCache = new shared_read_cache_1.SharedReadCache();
|
|
115
130
|
normVectorCache = new shared_read_cache_1.SharedReadCache({ maxSize: NORM_VECTOR_CACHE_SIZE });
|
|
116
131
|
matrixCache = new shared_read_cache_1.SharedReadCache({
|
|
117
132
|
maxSize: MATRIX_CACHE_SIZE,
|
|
@@ -124,22 +139,23 @@ class HicFile {
|
|
|
124
139
|
version = 0;
|
|
125
140
|
genomeId = '';
|
|
126
141
|
footerPosition = 0;
|
|
142
|
+
// Where the raw (NONE) expected values begin, right after the master index.
|
|
143
|
+
expectedValuesPosition = 0;
|
|
127
144
|
// Where the file's normalization data begins, and so also where a file
|
|
128
145
|
// carrying none of it ends. Public so a test can cut a real file there to
|
|
129
146
|
// make that second shape.
|
|
130
147
|
normExpectedValueVectorsPosition;
|
|
131
148
|
chromosomes = [];
|
|
132
|
-
chromosomeIndexMap =
|
|
133
|
-
chrAliasTable = {};
|
|
149
|
+
chromosomeIndexMap = new Map();
|
|
134
150
|
bpResolutions = [];
|
|
135
151
|
masterIndex = {};
|
|
136
152
|
meta;
|
|
137
153
|
constructor(config) {
|
|
138
|
-
const { filehandle, path, reader, blockCacheMaxBytes } = config;
|
|
139
|
-
this.
|
|
140
|
-
this.blockCache = new
|
|
141
|
-
|
|
142
|
-
|
|
154
|
+
const { filehandle, path, reader, nvi, blockCacheMaxBytes } = config;
|
|
155
|
+
this.nvi = nvi;
|
|
156
|
+
this.blockCache = new shared_read_cache_1.SharedReadCache({
|
|
157
|
+
maxSize: blockCacheMaxBytes ?? BLOCK_CACHE_MAX_BYTES,
|
|
158
|
+
sizeOf: blockCacheWeight,
|
|
143
159
|
});
|
|
144
160
|
if (reader) {
|
|
145
161
|
this.file = reader;
|
|
@@ -177,7 +193,7 @@ class HicFile {
|
|
|
177
193
|
throw new Error(`Unsupported hic version: ${version}`);
|
|
178
194
|
}
|
|
179
195
|
const footerPosition = binaryParser.getLong();
|
|
180
|
-
const { masterIndex, normExpectedValueVectorsPosition } = await this.readFooter(version, footerPosition, signal);
|
|
196
|
+
const { masterIndex, expectedValuesPosition, normExpectedValueVectorsPosition, } = await this.readFooter(version, footerPosition, signal);
|
|
181
197
|
const bodyPosition = Object.values(masterIndex).reduce((min, entry) => Math.min(min, entry.start), Number.MAX_VALUE);
|
|
182
198
|
data = await this.file.read(16, bodyPosition - 16, { signal });
|
|
183
199
|
binaryParser = new binary_ts_1.default(new DataView(data));
|
|
@@ -194,7 +210,7 @@ class HicFile {
|
|
|
194
210
|
binaryParser.getString(); // attribute value
|
|
195
211
|
}
|
|
196
212
|
const chromosomes = [];
|
|
197
|
-
const chromosomeIndexMap =
|
|
213
|
+
const chromosomeIndexMap = new Map();
|
|
198
214
|
let nChrs = binaryParser.getInt();
|
|
199
215
|
let i = 0;
|
|
200
216
|
while (nChrs-- > 0) {
|
|
@@ -204,7 +220,7 @@ class HicFile {
|
|
|
204
220
|
size: version < 9 ? binaryParser.getInt() : binaryParser.getLong(),
|
|
205
221
|
};
|
|
206
222
|
chromosomes.push(chr);
|
|
207
|
-
chromosomeIndexMap
|
|
223
|
+
chromosomeIndexMap.set(chr.name, chr.index);
|
|
208
224
|
i++;
|
|
209
225
|
}
|
|
210
226
|
const bpResolutions = [];
|
|
@@ -212,21 +228,10 @@ class HicFile {
|
|
|
212
228
|
while (nBpResolutions-- > 0) {
|
|
213
229
|
bpResolutions.push(binaryParser.getInt());
|
|
214
230
|
}
|
|
215
|
-
const chrAliasTable = {};
|
|
216
|
-
for (const chrName of Object.keys(chromosomeIndexMap)) {
|
|
217
|
-
if (chrName.startsWith('chr')) {
|
|
218
|
-
chrAliasTable[chrName.slice(3)] = chrName;
|
|
219
|
-
}
|
|
220
|
-
else if (chrName === 'MT') {
|
|
221
|
-
chrAliasTable.chrM = chrName;
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
chrAliasTable[`chr${chrName}`] = chrName;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
231
|
(0, shared_read_cache_1.throwIfAborted)(signal);
|
|
228
232
|
this.version = version;
|
|
229
233
|
this.footerPosition = footerPosition;
|
|
234
|
+
this.expectedValuesPosition = expectedValuesPosition;
|
|
230
235
|
this.masterIndex = masterIndex;
|
|
231
236
|
this.normExpectedValueVectorsPosition = normExpectedValueVectorsPosition;
|
|
232
237
|
this.genomeId = genomeId;
|
|
@@ -235,7 +240,6 @@ class HicFile {
|
|
|
235
240
|
this.chromosomes = chromosomes;
|
|
236
241
|
this.chromosomeIndexMap = chromosomeIndexMap;
|
|
237
242
|
this.bpResolutions = bpResolutions;
|
|
238
|
-
this.chrAliasTable = chrAliasTable;
|
|
239
243
|
this.meta = {
|
|
240
244
|
version,
|
|
241
245
|
genome: genomeId,
|
|
@@ -262,9 +266,14 @@ class HicFile {
|
|
|
262
266
|
const size = binaryParser.getInt();
|
|
263
267
|
masterIndex[key] = { start: pos, size };
|
|
264
268
|
}
|
|
269
|
+
const expectedValuesPosition = footerPosition + skip + binaryParser.position;
|
|
265
270
|
// Normalized expected values start after the expected values
|
|
266
271
|
const normExpectedValueVectorsPosition = version > 5 ? footerPosition + (version < 9 ? 4 : 8) + nBytes : undefined;
|
|
267
|
-
return {
|
|
272
|
+
return {
|
|
273
|
+
masterIndex,
|
|
274
|
+
expectedValuesPosition,
|
|
275
|
+
normExpectedValueVectorsPosition,
|
|
276
|
+
};
|
|
268
277
|
}
|
|
269
278
|
async getMatrix(chrIdx1, chrIdx2, opts) {
|
|
270
279
|
await this.init(opts);
|
|
@@ -279,10 +288,10 @@ class HicFile {
|
|
|
279
288
|
}
|
|
280
289
|
return matrix;
|
|
281
290
|
}
|
|
282
|
-
async getContactRecords(normalization, region1, region2,
|
|
291
|
+
async getContactRecords(normalization, region1, region2, binsize, opts) {
|
|
283
292
|
await this.init(opts);
|
|
284
|
-
const idx1 = this.chromosomeIndexMap
|
|
285
|
-
const idx2 = this.chromosomeIndexMap
|
|
293
|
+
const idx1 = this.chromosomeIndexMap.get(region1.chr);
|
|
294
|
+
const idx2 = this.chromosomeIndexMap.get(region2.chr);
|
|
286
295
|
// A `.hic` stores only `bin1 <= bin2`, so a query whose x window sits to the
|
|
287
296
|
// right of its y window has to be swapped or it asks for the half of the
|
|
288
297
|
// matrix that does not exist. The same-chromosome test compares **starts**,
|
|
@@ -318,7 +327,7 @@ class HicFile {
|
|
|
318
327
|
// resolved once per pair rather than per block, each paired with the bin
|
|
319
328
|
// offset its values start at.
|
|
320
329
|
const [norm, blocks] = await Promise.all([
|
|
321
|
-
this.getNormVectors(normalization, r1, r2,
|
|
330
|
+
this.getNormVectors(normalization, r1, r2, binsize, opts),
|
|
322
331
|
// The measurable half of the pair: blocks are counted and the two norm
|
|
323
332
|
// vectors are not, because the vector chain is two hops whatever the
|
|
324
333
|
// query and the block chain is the one that grows with the region.
|
|
@@ -393,45 +402,41 @@ class HicFile {
|
|
|
393
402
|
//
|
|
394
403
|
// `transposed` says the query was swapped, so `bin1` runs along `region2`.
|
|
395
404
|
// Reported rather than left for the caller to re-derive: it is decided here
|
|
396
|
-
// from this file's
|
|
397
|
-
//
|
|
398
|
-
// wrong axis.
|
|
405
|
+
// from this file's chromosome indices, which a caller would otherwise have
|
|
406
|
+
// to look up again to know which axis to un-swap.
|
|
399
407
|
return {
|
|
400
408
|
records: contactRecords,
|
|
401
409
|
appliedNormalization: norm ? normalization : 'NONE',
|
|
402
410
|
transposed: transpose,
|
|
403
411
|
};
|
|
404
412
|
}
|
|
405
|
-
async getNormVectors(normalization, r1, r2,
|
|
413
|
+
async getNormVectors(normalization, r1, r2, binsize, opts) {
|
|
414
|
+
if (!normalization || normalization === 'NONE') {
|
|
415
|
+
return undefined;
|
|
416
|
+
}
|
|
406
417
|
const signalOpts = { signal: opts?.signal };
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
if (nv1 && nv2) {
|
|
418
|
-
result = {
|
|
419
|
-
v1: await nv1.getValues(offset1, Math.ceil(r1.end / binsize), signalOpts),
|
|
420
|
-
v2: await nv2.getValues(offset2, Math.ceil(r2.end / binsize), signalOpts),
|
|
421
|
-
offset1,
|
|
422
|
-
offset2,
|
|
423
|
-
};
|
|
424
|
-
}
|
|
418
|
+
const [offset1, end1] = (0, binWindow_ts_1.binWindow)(r1, binsize);
|
|
419
|
+
const [offset2, end2] = (0, binWindow_ts_1.binWindow)(r2, binsize);
|
|
420
|
+
// Awaited in sequence, an inter-chromosomal pair would pay four
|
|
421
|
+
// round-trip waves for what is two hops per vector.
|
|
422
|
+
const [nv1, nv2] = await Promise.all([
|
|
423
|
+
this.getNormalizationVector(normalization, r1.chr, binsize, signalOpts),
|
|
424
|
+
this.getNormalizationVector(normalization, r2.chr, binsize, signalOpts),
|
|
425
|
+
]);
|
|
426
|
+
if (!nv1 || !nv2) {
|
|
427
|
+
return undefined;
|
|
425
428
|
}
|
|
426
|
-
|
|
429
|
+
const [v1, v2] = await Promise.all([
|
|
430
|
+
nv1.getValues(offset1, end1, signalOpts),
|
|
431
|
+
nv2.getValues(offset2, end2, signalOpts),
|
|
432
|
+
]);
|
|
433
|
+
return { v1, v2, offset1, offset2 };
|
|
427
434
|
}
|
|
428
435
|
async getBlocks(region1, region2, binSize, opts) {
|
|
429
436
|
const blockKey = (blockNumber, zd) => `${zd.getKey()}_${blockNumber}`;
|
|
430
437
|
await this.init(opts);
|
|
431
|
-
const
|
|
432
|
-
const
|
|
433
|
-
const idx1 = this.chromosomeIndexMap[chr1];
|
|
434
|
-
const idx2 = this.chromosomeIndexMap[chr2];
|
|
438
|
+
const idx1 = this.chromosomeIndexMap.get(region1.chr);
|
|
439
|
+
const idx2 = this.chromosomeIndexMap.get(region2.chr);
|
|
435
440
|
let blocks = [];
|
|
436
441
|
if (idx1 === undefined) {
|
|
437
442
|
console.warn(`No chromosome named: ${region1.chr}`);
|
|
@@ -452,43 +457,37 @@ class HicFile {
|
|
|
452
457
|
throw new Error(`${exports.NO_DATA_FOR_RESOLUTION}: ${binSize} for map ${region1.chr}-${region2.chr}`);
|
|
453
458
|
}
|
|
454
459
|
const blockNumbers = zd.getBlockNumbers(region1, region2, this.version);
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
const cached = this.blockCache.get(blockKey(num, zd));
|
|
458
|
-
if (cached) {
|
|
459
|
-
blocks.push(cached);
|
|
460
|
-
}
|
|
461
|
-
else {
|
|
462
|
-
blockNumbersToQuery.push(num);
|
|
463
|
-
}
|
|
464
|
-
}
|
|
460
|
+
const keys = blockNumbers.map(num => blockKey(num, zd));
|
|
461
|
+
const cached = keys.map(key => this.blockCache.has(key));
|
|
465
462
|
// Cached blocks are already done, not absent: a pan that reuses most
|
|
466
463
|
// of its blocks reads most of the way along rather than restarting at
|
|
467
464
|
// zero, which is what the bar should say — the work left is the reads.
|
|
465
|
+
// A block another fetch has in flight counts as cached too.
|
|
468
466
|
//
|
|
469
467
|
// A query covering no blocks at all reports nothing rather than `0, 0`:
|
|
470
468
|
// there is no fraction in it, and a caller dividing would get NaN.
|
|
471
469
|
const { onProgress } = opts ?? {};
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
470
|
+
const total = blockNumbers.length;
|
|
471
|
+
let done = cached.filter(Boolean).length;
|
|
472
|
+
if (total > 0) {
|
|
473
|
+
onProgress?.(done, total);
|
|
475
474
|
}
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
475
|
+
// One `Promise.all`, so every block is issued in one wave: what a
|
|
476
|
+
// remote file pays is round-trip DEPTH, and counting the completions
|
|
477
|
+
// must not turn one wave into a queue. `readChainDepth` pins that.
|
|
478
|
+
//
|
|
479
|
+
// A block number missing from the index has no data, so it skips the
|
|
480
|
+
// cache, which would otherwise keep a weightless entry for it forever.
|
|
481
|
+
blocks = await Promise.all(blockNumbers.map(async (num, i) => {
|
|
482
|
+
const block = zd.blockIndex[num]
|
|
483
|
+
? await this.blockCache.get(keys[i], opts?.signal, signal => this.readBlock(num, zd, { signal }))
|
|
484
|
+
: undefined;
|
|
485
|
+
if (!cached[i]) {
|
|
486
|
+
done++;
|
|
487
|
+
onProgress?.(done, total);
|
|
488
|
+
}
|
|
484
489
|
return block;
|
|
485
490
|
}));
|
|
486
|
-
for (const block of newBlocks) {
|
|
487
|
-
if (block) {
|
|
488
|
-
this.blockCache.set(blockKey(block.blockNumber, zd), block);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
blocks = blocks.concat(newBlocks);
|
|
492
491
|
}
|
|
493
492
|
}
|
|
494
493
|
return blocks;
|
|
@@ -595,13 +594,13 @@ class HicFile {
|
|
|
595
594
|
}
|
|
596
595
|
throw new Error(`Unknown block type: ${type}`);
|
|
597
596
|
}
|
|
598
|
-
async getNormalizationVector(type, chr,
|
|
597
|
+
async getNormalizationVector(type, chr, binSize, opts) {
|
|
599
598
|
await this.init(opts);
|
|
600
|
-
const chrIdx = this.chromosomeIndexMap
|
|
599
|
+
const chrIdx = this.chromosomeIndexMap.get(chr);
|
|
601
600
|
if (chrIdx === undefined) {
|
|
602
601
|
return undefined;
|
|
603
602
|
}
|
|
604
|
-
const key = getNormalizationVectorKey(type, chrIdx,
|
|
603
|
+
const key = getNormalizationVectorKey(type, chrIdx, 'BP', binSize);
|
|
605
604
|
// Sharing the in-flight read is what keeps concurrent region pairs sharing
|
|
606
605
|
// one vector: a chromosome appears in every pair it takes part in, so with a
|
|
607
606
|
// result-only cache each of those pairs missed while the first was still in
|
|
@@ -658,8 +657,8 @@ class HicFile {
|
|
|
658
657
|
size: this.normVectorIndexSize,
|
|
659
658
|
}, opts.signal);
|
|
660
659
|
}
|
|
661
|
-
else if (this.
|
|
662
|
-
const nviArray = decodeURIComponent(this.
|
|
660
|
+
else if (this.nvi) {
|
|
661
|
+
const nviArray = decodeURIComponent(this.nvi).split(',');
|
|
663
662
|
return this.readNormVectorIndex({
|
|
664
663
|
start: parseInt(nviArray[0]),
|
|
665
664
|
size: parseInt(nviArray[1]),
|
|
@@ -702,102 +701,149 @@ class HicFile {
|
|
|
702
701
|
const { signal } = opts;
|
|
703
702
|
let index;
|
|
704
703
|
if (this.normExpectedValueVectorsPosition !== undefined) {
|
|
705
|
-
const nviStart = await this.
|
|
706
|
-
|
|
707
|
-
let data = await this.file.read(nviStart, INT, { signal });
|
|
704
|
+
const nviStart = (await this.getExpectedSection('normalized', opts)).end;
|
|
705
|
+
const data = await this.file.read(nviStart, INT, { signal });
|
|
708
706
|
// Possible if there are no norm vectors. A legal v8 file, though uncommon.
|
|
709
707
|
if (data.byteLength !== 0) {
|
|
710
|
-
const
|
|
711
|
-
const nEntries = binaryParser.getInt();
|
|
712
|
-
const sizeEstimate = nEntries * 30;
|
|
713
|
-
data = await this.file.read(nviStart + byteCount, sizeEstimate, {
|
|
714
|
-
signal,
|
|
715
|
-
});
|
|
708
|
+
const nEntries = new binary_ts_1.default(new DataView(data)).getInt();
|
|
716
709
|
const found = { entries: {}, types: ['NONE'] };
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
731
|
-
this.parseNormVectorEntry(parser, found);
|
|
710
|
+
let position = nviStart + INT;
|
|
711
|
+
let requested = nEntries * 30;
|
|
712
|
+
let buf = await this.file.read(position, requested, { signal });
|
|
713
|
+
let parser = new binary_ts_1.default(new DataView(buf));
|
|
714
|
+
for (let n = nEntries; n > 0; n--) {
|
|
715
|
+
// A short read means the buffer already runs to EOF, and refilling
|
|
716
|
+
// it would only return the same bytes again.
|
|
717
|
+
if (parser.available() < 100 && buf.byteLength === requested) {
|
|
718
|
+
position += parser.position;
|
|
719
|
+
requested = Math.max(1000, n * 30);
|
|
720
|
+
buf = await this.file.read(position, requested, { signal });
|
|
721
|
+
parser = new binary_ts_1.default(new DataView(buf));
|
|
732
722
|
}
|
|
733
|
-
|
|
734
|
-
}
|
|
735
|
-
await processEntries(nEntries, data);
|
|
723
|
+
this.parseNormVectorEntry(parser, found);
|
|
724
|
+
}
|
|
736
725
|
(0, shared_read_cache_1.throwIfAborted)(signal);
|
|
737
|
-
this.config.nvi = `${nviStart},${byteCount}`;
|
|
738
726
|
index = found;
|
|
739
727
|
}
|
|
740
728
|
}
|
|
741
729
|
return index;
|
|
742
730
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
731
|
+
/**
|
|
732
|
+
* The expected count for a region pair at `binsize`, the denominator of
|
|
733
|
+
* observed/expected, as straw computes it. `normalization` must be the one
|
|
734
|
+
* the pair's records carry (`getContactRecords`' `appliedNormalization`),
|
|
735
|
+
* since each normalization has its own vector. Undefined where the file has
|
|
736
|
+
* no expected value for it: no vector at this binsize, or a version 5 file
|
|
737
|
+
* asked for a normalized one.
|
|
738
|
+
*
|
|
739
|
+
* Within a chromosome the vector is read for only the distances the pair
|
|
740
|
+
* spans. Between chromosomes straw divides by the map's average contact, the
|
|
741
|
+
* raw `sumCounts` over its bins, whatever the normalization.
|
|
742
|
+
*/
|
|
743
|
+
async getExpectedValues(normalization, region1, region2, binsize, opts) {
|
|
744
|
+
await this.init(opts);
|
|
745
|
+
const idx1 = this.chromosomeIndexMap.get(region1.chr);
|
|
746
|
+
const idx2 = this.chromosomeIndexMap.get(region2.chr);
|
|
747
|
+
if (idx1 === undefined || idx2 === undefined) {
|
|
748
|
+
return undefined;
|
|
749
|
+
}
|
|
750
|
+
if (idx1 !== idx2) {
|
|
751
|
+
const matrix = await this.getMatrix(idx1, idx2, opts);
|
|
752
|
+
const zd = matrix?.getZoomData(binsize);
|
|
753
|
+
if (!zd) {
|
|
754
|
+
return undefined;
|
|
755
|
+
}
|
|
756
|
+
// straw's bin counts leave out a chromosome's partial last bin
|
|
757
|
+
const nBins1 = Math.max(1, Math.floor(this.chromosomes[idx1].size / binsize));
|
|
758
|
+
const nBins2 = Math.max(1, Math.floor(this.chromosomes[idx2].size / binsize));
|
|
759
|
+
return { kind: 'inter', average: zd.sumCounts / nBins1 / nBins2 };
|
|
760
|
+
}
|
|
761
|
+
const normalized = !!normalization && normalization !== 'NONE';
|
|
762
|
+
if (normalized && this.normExpectedValueVectorsPosition === undefined) {
|
|
763
|
+
return undefined;
|
|
764
|
+
}
|
|
765
|
+
const section = await this.getExpectedSection(normalized ? 'normalized' : 'raw', opts ?? {});
|
|
766
|
+
const chunk = section.chunks.get(getExpectedKey(normalized ? normalization : 'NONE', 'BP', binsize));
|
|
767
|
+
if (!chunk || chunk.nValues === 0) {
|
|
768
|
+
return undefined;
|
|
769
|
+
}
|
|
770
|
+
const [first, last] = distanceRange((0, binWindow_ts_1.binWindow)(region1, binsize), (0, binWindow_ts_1.binWindow)(region2, binsize));
|
|
771
|
+
const signalOpts = { signal: opts?.signal };
|
|
772
|
+
const stored = await chunk.vector.getValues(first, last + 1, signalOpts);
|
|
773
|
+
const tail = stored.length < last + 1 - first
|
|
774
|
+
? (await chunk.vector.getValues(chunk.nValues - 1, chunk.nValues, signalOpts))[0]
|
|
775
|
+
: 0;
|
|
776
|
+
// straw divides by 1 where the section names no factor for the chromosome
|
|
777
|
+
const factor = chunk.chrScaleFactors.get(idx1) ?? 1;
|
|
778
|
+
const values = new Float64Array(last + 1 - first);
|
|
779
|
+
for (let i = 0; i < values.length; i++) {
|
|
780
|
+
values[i] = (i < stored.length ? stored[i] : tail) / factor;
|
|
781
|
+
}
|
|
782
|
+
return { kind: 'intra', firstDistance: first, values };
|
|
783
|
+
}
|
|
784
|
+
async getExpectedSection(section, opts) {
|
|
785
|
+
return this.expectedSectionCache.get(section, opts.signal, signal => this.walkExpectedSection(section === 'raw'
|
|
786
|
+
? this.expectedValuesPosition
|
|
787
|
+
: this.normExpectedValueVectorsPosition, section === 'normalized', { signal, onProgress: opts.onProgress }));
|
|
788
|
+
}
|
|
789
|
+
// Walk one expected-value section, recording where each chunk's values sit
|
|
790
|
+
// and its chromosome scale factors. The normalized section's end is also
|
|
791
|
+
// where a pre-v9 file's normalization vector index begins.
|
|
792
|
+
//
|
|
793
|
+
// A chunk's two reads land a whole expected-value vector apart — one per bin
|
|
794
|
+
// of the genome's longest chromosome, so megabytes at a fine binsize — which
|
|
795
|
+
// is further than the buffer reaches. On a remote file that is two round
|
|
796
|
+
// trips per chunk that nothing can merge, and it is why this walk, not the
|
|
797
|
+
// index it leads to, is the slow part of opening a pre-v9 file. It is also
|
|
798
|
+
// why the chunk is the unit worth counting: known before the first read, each
|
|
799
|
+
// costing about the same. Chunks are walked in order, since each one's size
|
|
800
|
+
// is known only once the one before it has been parsed.
|
|
801
|
+
async walkExpectedSection(start, normalized, opts) {
|
|
746
802
|
const { signal, onProgress } = opts;
|
|
747
803
|
const version = this.version;
|
|
804
|
+
const valueSize = version < 9 ? DOUBLE : FLOAT;
|
|
748
805
|
const file = new bufferedFile_ts_1.default({ file: this.file, size: 256000 });
|
|
806
|
+
const chunks = new Map();
|
|
749
807
|
const data = await file.read(start, INT, { signal });
|
|
750
|
-
// A file with no normalization at all ends where
|
|
751
|
-
// so the count this is about to read is past EOF.
|
|
752
|
-
//
|
|
753
|
-
//
|
|
754
|
-
//
|
|
755
|
-
// discovery path, and that path starts by skipping a section that isn't
|
|
756
|
-
// there. Answering `start` leaves the caller's guard to do the rest.
|
|
808
|
+
// A file with no normalization at all ends where the normalized section
|
|
809
|
+
// would start, so the count this is about to read is past EOF. A v9 file
|
|
810
|
+
// that records no norm-vector-index position takes the v8 discovery path,
|
|
811
|
+
// which starts by walking a section that isn't there, so the empty answer
|
|
812
|
+
// has to come from here.
|
|
757
813
|
if (data.byteLength < INT) {
|
|
758
|
-
return start;
|
|
814
|
+
return { chunks, end: start };
|
|
759
815
|
}
|
|
760
|
-
const
|
|
761
|
-
const nEntries = binaryParser.getInt(); // Total # of expected value chunks
|
|
762
|
-
// Skip one chunk, answering where the next one starts.
|
|
763
|
-
//
|
|
764
|
-
// Its two reads land a whole expected-value vector apart — one per bin of
|
|
765
|
-
// the genome, so tens of MB at a fine binsize — which is further than the
|
|
766
|
-
// buffer reaches. On a remote file that is two round trips per chunk that
|
|
767
|
-
// nothing can merge, and it is why this walk, not the index it is looking
|
|
768
|
-
// for, is the slow part of opening a pre-v9 file. It is also why the chunk
|
|
769
|
-
// is the unit worth counting: `nEntries` of them, known before the first
|
|
770
|
-
// read, each costing about the same.
|
|
771
|
-
const skipChunk = async (chunkStart) => {
|
|
772
|
-
let chunkSize = 0;
|
|
773
|
-
let buf = await file.read(chunkStart, 500, { signal });
|
|
774
|
-
let parser = new binary_ts_1.default(new DataView(buf));
|
|
775
|
-
parser.getString(); // type
|
|
776
|
-
parser.getString(); // unit
|
|
777
|
-
parser.getInt(); // binSize
|
|
778
|
-
const nValues = version < 9 ? parser.getInt() : parser.getLong();
|
|
779
|
-
chunkSize += parser.position + nValues * (version < 9 ? DOUBLE : FLOAT);
|
|
780
|
-
buf = await file.read(chunkStart + chunkSize, INT, { signal });
|
|
781
|
-
parser = new binary_ts_1.default(new DataView(buf));
|
|
782
|
-
const nChrScaleFactors = parser.getInt();
|
|
783
|
-
chunkSize +=
|
|
784
|
-
INT + nChrScaleFactors * (INT + (version < 9 ? DOUBLE : FLOAT));
|
|
785
|
-
return chunkStart + chunkSize;
|
|
786
|
-
};
|
|
787
|
-
// A loop rather than the tail recursion this was, because the position is
|
|
788
|
-
// the only thing carried between chunks and a counter now rides along with
|
|
789
|
-
// it. Chunks must be walked in order — each one's size is only known once
|
|
790
|
-
// the one before it has been parsed — so the awaits are sequential by
|
|
791
|
-
// nature, not by oversight.
|
|
816
|
+
const nEntries = new binary_ts_1.default(new DataView(data)).getInt();
|
|
792
817
|
if (nEntries > 0) {
|
|
793
818
|
onProgress?.(0, nEntries);
|
|
794
819
|
}
|
|
795
820
|
let position = start + INT;
|
|
796
821
|
for (let done = 0; done < nEntries; done++) {
|
|
797
|
-
|
|
822
|
+
let parser = new binary_ts_1.default(new DataView(await file.read(position, 500, { signal })));
|
|
823
|
+
const type = normalized ? parser.getString() : 'NONE';
|
|
824
|
+
const unit = parser.getString();
|
|
825
|
+
const binSize = parser.getInt();
|
|
826
|
+
const nValues = version < 9 ? parser.getInt() : parser.getLong();
|
|
827
|
+
const valuesPosition = position + parser.position;
|
|
828
|
+
const factorsPosition = valuesPosition + nValues * valueSize;
|
|
829
|
+
parser = new binary_ts_1.default(new DataView(await file.read(factorsPosition, INT, { signal })));
|
|
830
|
+
const nFactors = parser.getInt();
|
|
831
|
+
const factorBytes = nFactors * (INT + valueSize);
|
|
832
|
+
parser = new binary_ts_1.default(new DataView(await file.read(factorsPosition + INT, factorBytes, { signal })));
|
|
833
|
+
const chrScaleFactors = new Map();
|
|
834
|
+
for (let i = 0; i < nFactors; i++) {
|
|
835
|
+
const chrIdx = parser.getInt();
|
|
836
|
+
chrScaleFactors.set(chrIdx, version < 9 ? parser.getDouble() : parser.getFloat());
|
|
837
|
+
}
|
|
838
|
+
chunks.set(getExpectedKey(type, unit, binSize), {
|
|
839
|
+
vector: new normalizationVector_ts_1.default(this.file, valuesPosition, nValues, valueSize),
|
|
840
|
+
nValues,
|
|
841
|
+
chrScaleFactors,
|
|
842
|
+
});
|
|
843
|
+
position = factorsPosition + INT + factorBytes;
|
|
798
844
|
onProgress?.(done + 1, nEntries);
|
|
799
845
|
}
|
|
800
|
-
return position;
|
|
846
|
+
return { chunks, end: position };
|
|
801
847
|
}
|
|
802
848
|
parseNormVectorEntry(binaryParser, index) {
|
|
803
849
|
const type = binaryParser.getString(); // 15
|
|
@@ -806,15 +852,12 @@ class HicFile {
|
|
|
806
852
|
const binSize = binaryParser.getInt(); // 4
|
|
807
853
|
const filePosition = binaryParser.getLong(); // 8
|
|
808
854
|
const sizeInBytes = this.version < 9 ? binaryParser.getInt() : binaryParser.getLong(); // 4:8
|
|
809
|
-
const key =
|
|
855
|
+
const key = getNormalizationVectorKey(type, chrIdx, unit, binSize);
|
|
810
856
|
if (!index.types.includes(type)) {
|
|
811
857
|
index.types.push(type);
|
|
812
858
|
}
|
|
813
859
|
index.entries[key] = { filePosition, size: sizeInBytes };
|
|
814
860
|
}
|
|
815
|
-
getFileChrName(chrAlias) {
|
|
816
|
-
return this.chrAliasTable[chrAlias] ?? chrAlias;
|
|
817
|
-
}
|
|
818
861
|
}
|
|
819
862
|
exports.HicFile = HicFile;
|
|
820
863
|
//# sourceMappingURL=hicFile.js.map
|