@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/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
- const BLOCK_CACHE_MAX_ENTRIES = 1024;
63
- // The bound the entry cap was standing in for. Blocks became struct-of-arrays
64
- // (see contactRecords.ts), so a cached contact is 12 bytes of untraced
65
- // ArrayBuffer rather than ~55 on the GC-traced heap — but a block still holds
66
- // every contact in its bin square, which is the one thing here that can hold
67
- // real memory, and block sizes vary by more than an order of magnitude with
68
- // binsize. Measured on the file above: 0.05 MB at 2.5 Mb, 0.23 MB at 100 kb. So
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
- config;
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.config = config;
134
- this.blockCache = new LRU(BLOCK_CACHE_MAX_ENTRIES, {
135
- maxBytes: blockCacheMaxBytes ?? BLOCK_CACHE_MAX_BYTES,
136
- weigh: blockCacheWeight,
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[chr.name] = chr.index;
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 { masterIndex, normExpectedValueVectorsPosition };
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, units, binsize, opts) {
285
+ async getContactRecords(normalization, region1, region2, binsize, opts) {
277
286
  await this.init(opts);
278
- const idx1 = this.chromosomeIndexMap[this.getFileChrName(region1.chr)];
279
- const idx2 = this.chromosomeIndexMap[this.getFileChrName(region2.chr)];
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, units, binsize, opts),
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 own alias table and chromosome indices, and a caller
391
- // re-deriving it off a divergent chr-name scheme would silently un-swap the
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, units, binsize, opts) {
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
- let result;
402
- if (normalization && normalization !== 'NONE') {
403
- const chr1 = this.getFileChrName(r1.chr);
404
- const chr2 = this.getFileChrName(r2.chr);
405
- const offset1 = Math.floor(r1.start / binsize);
406
- const offset2 = Math.floor(r2.start / binsize);
407
- const nv1 = await this.getNormalizationVector(normalization, chr1, units, binsize, signalOpts);
408
- const nv2 = chr1 === chr2
409
- ? nv1
410
- : await this.getNormalizationVector(normalization, chr2, units, binsize, signalOpts);
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
- return result;
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 chr1 = this.getFileChrName(region1.chr);
426
- const chr2 = this.getFileChrName(region2.chr);
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 blockNumbersToQuery = [];
450
- for (const num of blockNumbers) {
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
- let done = blocks.length;
467
- if (blockNumbers.length > 0) {
468
- onProgress?.(done, blockNumbers.length);
464
+ const total = blockNumbers.length;
465
+ let done = cached.filter(Boolean).length;
466
+ if (total > 0) {
467
+ onProgress?.(done, total);
469
468
  }
470
- // Still one `Promise.all`, so every block is still issued in one wave:
471
- // what a remote file pays is round-trip DEPTH, and counting the
472
- // completions must not turn one wave into a queue. `readChainDepth`
473
- // pins that.
474
- const newBlocks = await Promise.all(blockNumbersToQuery.map(async (blockNumber) => {
475
- const block = await this.readBlock(blockNumber, zd, signalOpts);
476
- done++;
477
- onProgress?.(done, blockNumbers.length);
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, unit, binSize, opts) {
591
+ async getNormalizationVector(type, chr, binSize, opts) {
593
592
  await this.init(opts);
594
- const chrIdx = this.chromosomeIndexMap[this.getFileChrName(chr)];
593
+ const chrIdx = this.chromosomeIndexMap.get(chr);
595
594
  if (chrIdx === undefined) {
596
595
  return undefined;
597
596
  }
598
- const key = getNormalizationVectorKey(type, chrIdx, unit, binSize);
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.config.nvi) {
656
- const nviArray = decodeURIComponent(this.config.nvi).split(',');
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.skipExpectedValues(this.normExpectedValueVectorsPosition, opts);
700
- let byteCount = INT;
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 binaryParser = new BinaryParser(new DataView(data));
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
- const processEntries = async (remaining, buf) => {
712
- const parser = new BinaryParser(new DataView(buf));
713
- let n = remaining;
714
- while (n-- > 0) {
715
- if (parser.available() < 100) {
716
- n++; // Reset counter as entry is not processed
717
- byteCount += parser.position;
718
- const est = Math.max(1000, n * 30);
719
- const more = await this.file.read(nviStart + byteCount, est, {
720
- signal,
721
- });
722
- await processEntries(n, more);
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
- byteCount += parser.position;
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
- // Used when the position of the norm vector index is unknown: skip the
738
- // normalized expected values to find the index.
739
- async skipExpectedValues(start, opts) {
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 this section would start,
745
- // so the count this is about to read is past EOF. That is the same "no norm
746
- // vectors" case `readNormExpectedValuesAndNormVectorIndex` guards its own
747
- // read for, and it has to be caught HERE too, because this runs first:
748
- // a v9 file that records no norm-vector-index position takes the v8
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 binaryParser = new BinaryParser(new DataView(data));
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
- position = await skipChunk(position);
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 = `${type}_${chrIdx}_${unit}_${binSize}`;
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