@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/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
- const BLOCK_CACHE_MAX_ENTRIES = 1024;
69
- // The bound the entry cap was standing in for. Blocks became struct-of-arrays
70
- // (see contactRecords.ts), so a cached contact is 12 bytes of untraced
71
- // ArrayBuffer rather than ~55 on the GC-traced heap — but a block still holds
72
- // every contact in its bin square, which is the one thing here that can hold
73
- // real memory, and block sizes vary by more than an order of magnitude with
74
- // binsize. Measured on the file above: 0.05 MB at 2.5 Mb, 0.23 MB at 100 kb. So
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
- config;
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.config = config;
140
- this.blockCache = new lru_ts_1.default(BLOCK_CACHE_MAX_ENTRIES, {
141
- maxBytes: blockCacheMaxBytes ?? BLOCK_CACHE_MAX_BYTES,
142
- weigh: blockCacheWeight,
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[chr.name] = chr.index;
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 { masterIndex, normExpectedValueVectorsPosition };
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, units, binsize, opts) {
291
+ async getContactRecords(normalization, region1, region2, binsize, opts) {
283
292
  await this.init(opts);
284
- const idx1 = this.chromosomeIndexMap[this.getFileChrName(region1.chr)];
285
- const idx2 = this.chromosomeIndexMap[this.getFileChrName(region2.chr)];
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, units, binsize, opts),
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 own alias table and chromosome indices, and a caller
397
- // re-deriving it off a divergent chr-name scheme would silently un-swap the
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, units, binsize, opts) {
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
- let result;
408
- if (normalization && normalization !== 'NONE') {
409
- const chr1 = this.getFileChrName(r1.chr);
410
- const chr2 = this.getFileChrName(r2.chr);
411
- const offset1 = Math.floor(r1.start / binsize);
412
- const offset2 = Math.floor(r2.start / binsize);
413
- const nv1 = await this.getNormalizationVector(normalization, chr1, units, binsize, signalOpts);
414
- const nv2 = chr1 === chr2
415
- ? nv1
416
- : await this.getNormalizationVector(normalization, chr2, units, binsize, signalOpts);
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
- return result;
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 chr1 = this.getFileChrName(region1.chr);
432
- const chr2 = this.getFileChrName(region2.chr);
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 blockNumbersToQuery = [];
456
- for (const num of blockNumbers) {
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
- let done = blocks.length;
473
- if (blockNumbers.length > 0) {
474
- onProgress?.(done, blockNumbers.length);
470
+ const total = blockNumbers.length;
471
+ let done = cached.filter(Boolean).length;
472
+ if (total > 0) {
473
+ onProgress?.(done, total);
475
474
  }
476
- // Still one `Promise.all`, so every block is still issued in one wave:
477
- // what a remote file pays is round-trip DEPTH, and counting the
478
- // completions must not turn one wave into a queue. `readChainDepth`
479
- // pins that.
480
- const newBlocks = await Promise.all(blockNumbersToQuery.map(async (blockNumber) => {
481
- const block = await this.readBlock(blockNumber, zd, signalOpts);
482
- done++;
483
- onProgress?.(done, blockNumbers.length);
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, unit, binSize, opts) {
597
+ async getNormalizationVector(type, chr, binSize, opts) {
599
598
  await this.init(opts);
600
- const chrIdx = this.chromosomeIndexMap[this.getFileChrName(chr)];
599
+ const chrIdx = this.chromosomeIndexMap.get(chr);
601
600
  if (chrIdx === undefined) {
602
601
  return undefined;
603
602
  }
604
- const key = getNormalizationVectorKey(type, chrIdx, unit, binSize);
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.config.nvi) {
662
- const nviArray = decodeURIComponent(this.config.nvi).split(',');
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.skipExpectedValues(this.normExpectedValueVectorsPosition, opts);
706
- let byteCount = INT;
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 binaryParser = new binary_ts_1.default(new DataView(data));
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
- const processEntries = async (remaining, buf) => {
718
- const parser = new binary_ts_1.default(new DataView(buf));
719
- let n = remaining;
720
- while (n-- > 0) {
721
- if (parser.available() < 100) {
722
- n++; // Reset counter as entry is not processed
723
- byteCount += parser.position;
724
- const est = Math.max(1000, n * 30);
725
- const more = await this.file.read(nviStart + byteCount, est, {
726
- signal,
727
- });
728
- await processEntries(n, more);
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
- byteCount += parser.position;
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
- // Used when the position of the norm vector index is unknown: skip the
744
- // normalized expected values to find the index.
745
- async skipExpectedValues(start, opts) {
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 this section would start,
751
- // so the count this is about to read is past EOF. That is the same "no norm
752
- // vectors" case `readNormExpectedValuesAndNormVectorIndex` guards its own
753
- // read for, and it has to be caught HERE too, because this runs first:
754
- // a v9 file that records no norm-vector-index position takes the v8
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 binaryParser = new binary_ts_1.default(new DataView(data));
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
- position = await skipChunk(position);
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 = `${type}_${chrIdx}_${unit}_${binSize}`;
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