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