@gmod/hic 1.0.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.
Files changed (88) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +91 -0
  3. package/dist/binWindow.d.ts +24 -0
  4. package/dist/binWindow.js +32 -0
  5. package/dist/binWindow.js.map +1 -0
  6. package/dist/binary.d.ts +15 -0
  7. package/dist/binary.js +67 -0
  8. package/dist/binary.js.map +1 -0
  9. package/dist/bufferedFile.d.ts +13 -0
  10. package/dist/bufferedFile.js +78 -0
  11. package/dist/bufferedFile.js.map +1 -0
  12. package/dist/contactRecords.d.ts +25 -0
  13. package/dist/contactRecords.js +3 -0
  14. package/dist/contactRecords.js.map +1 -0
  15. package/dist/hicFile.d.ts +94 -0
  16. package/dist/hicFile.js +769 -0
  17. package/dist/hicFile.js.map +1 -0
  18. package/dist/index.d.ts +5 -0
  19. package/dist/index.js +9 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/lru.d.ts +29 -0
  22. package/dist/lru.js +55 -0
  23. package/dist/lru.js.map +1 -0
  24. package/dist/matrix.d.ts +9 -0
  25. package/dist/matrix.js +41 -0
  26. package/dist/matrix.js.map +1 -0
  27. package/dist/matrixZoomData.d.ts +14 -0
  28. package/dist/matrixZoomData.js +96 -0
  29. package/dist/matrixZoomData.js.map +1 -0
  30. package/dist/normalizationVector.d.ts +10 -0
  31. package/dist/normalizationVector.js +51 -0
  32. package/dist/normalizationVector.js.map +1 -0
  33. package/dist/package.json +1 -0
  34. package/dist/reader.d.ts +3 -0
  35. package/dist/reader.js +12 -0
  36. package/dist/reader.js.map +1 -0
  37. package/dist/types.d.ts +35 -0
  38. package/dist/types.js +3 -0
  39. package/dist/types.js.map +1 -0
  40. package/esm/binWindow.d.ts +24 -0
  41. package/esm/binWindow.js +29 -0
  42. package/esm/binWindow.js.map +1 -0
  43. package/esm/binary.d.ts +15 -0
  44. package/esm/binary.js +64 -0
  45. package/esm/binary.js.map +1 -0
  46. package/esm/bufferedFile.d.ts +13 -0
  47. package/esm/bufferedFile.js +75 -0
  48. package/esm/bufferedFile.js.map +1 -0
  49. package/esm/contactRecords.d.ts +25 -0
  50. package/esm/contactRecords.js +2 -0
  51. package/esm/contactRecords.js.map +1 -0
  52. package/esm/hicFile.d.ts +94 -0
  53. package/esm/hicFile.js +762 -0
  54. package/esm/hicFile.js.map +1 -0
  55. package/esm/index.d.ts +5 -0
  56. package/esm/index.js +3 -0
  57. package/esm/index.js.map +1 -0
  58. package/esm/lru.d.ts +29 -0
  59. package/esm/lru.js +52 -0
  60. package/esm/lru.js.map +1 -0
  61. package/esm/matrix.d.ts +9 -0
  62. package/esm/matrix.js +35 -0
  63. package/esm/matrix.js.map +1 -0
  64. package/esm/matrixZoomData.d.ts +14 -0
  65. package/esm/matrixZoomData.js +93 -0
  66. package/esm/matrixZoomData.js.map +1 -0
  67. package/esm/normalizationVector.d.ts +10 -0
  68. package/esm/normalizationVector.js +45 -0
  69. package/esm/normalizationVector.js.map +1 -0
  70. package/esm/reader.d.ts +3 -0
  71. package/esm/reader.js +9 -0
  72. package/esm/reader.js.map +1 -0
  73. package/esm/types.d.ts +35 -0
  74. package/esm/types.js +2 -0
  75. package/esm/types.js.map +1 -0
  76. package/package.json +73 -0
  77. package/src/binWindow.ts +30 -0
  78. package/src/binary.ts +72 -0
  79. package/src/bufferedFile.ts +74 -0
  80. package/src/contactRecords.ts +25 -0
  81. package/src/hicFile.ts +943 -0
  82. package/src/index.ts +12 -0
  83. package/src/lru.ts +77 -0
  84. package/src/matrix.ts +43 -0
  85. package/src/matrixZoomData.ts +133 -0
  86. package/src/normalizationVector.ts +58 -0
  87. package/src/reader.ts +11 -0
  88. package/src/types.ts +40 -0
package/esm/hicFile.js ADDED
@@ -0,0 +1,762 @@
1
+ import { LocalFile } from 'generic-filehandle2';
2
+ import { inflate } from 'pako-esm2';
3
+ import { binWindow } from "./binWindow.js";
4
+ import BinaryParser from "./binary.js";
5
+ import BufferedFile from "./bufferedFile.js";
6
+ import LRU from "./lru.js";
7
+ import Matrix from "./matrix.js";
8
+ import NormalizationVector from "./normalizationVector.js";
9
+ import { readerFromFilehandle } from "./reader.js";
10
+ /**
11
+ * Prefix of the error thrown when a region pair has no matrix at the requested
12
+ * binsize. A caller fetching many region pairs at once will want to drop that
13
+ * pair rather than fail the whole fetch, and it can only tell this apart from a
14
+ * real failure by the message — so it is exported instead of hand-copied.
15
+ */
16
+ export const NO_DATA_FOR_RESOLUTION = 'No data available for resolution';
17
+ const Short_MIN_VALUE = -32768;
18
+ const DOUBLE = 8;
19
+ const FLOAT = 4;
20
+ const INT = 4;
21
+ /**
22
+ * Copy a partially-filled decode buffer down to its true length, or hand it
23
+ * back untouched when the filter dropped nothing. A `subarray` would be free
24
+ * but keeps the whole oversized buffer reachable, and these land in a cache
25
+ * that outlives the fetch.
26
+ */
27
+ function truncateRecords(records, n) {
28
+ return n === records.bin1.length
29
+ ? records
30
+ : {
31
+ bin1: records.bin1.slice(0, n),
32
+ bin2: records.bin2.slice(0, n),
33
+ counts: records.counts.slice(0, n),
34
+ };
35
+ }
36
+ // Every cache here is sized against the same thing, and it is worth stating
37
+ // once: a fetch's working set is a function of the DISPLAYED REGION COUNT, and
38
+ // two of the three grow with its square.
39
+ //
40
+ // A multi-region view queries every `(i, j)` pair with `i <= j`, so N displayed
41
+ // regions is N(N+1)/2 pairs — a whole-genome human view is 24 regions and 300
42
+ // pairs. Each pair needs one matrix and its blocks; each region needs one
43
+ // normalization vector per chromosome. Sized for one region, these caches don't
44
+ // merely underperform, they invert: the entries a fetch will need again are
45
+ // evicted by the same fetch, so the cache costs eviction bookkeeping and
46
+ // returns nothing.
47
+ //
48
+ // Measured on `test/data/test.hic` (hg19, 2.5 Mb, 24 regions / 300 pairs),
49
+ // range reads for a fetch and then an identical repeat fetch — which is what
50
+ // every pan issues:
51
+ //
52
+ // N pairs | before | after
53
+ // 4 10 | 29 / 0 | 29 / 0
54
+ // 5 15 | 40 / 15 | 40 / 0
55
+ // 10 55 | 130 / 110 | 130 / 0
56
+ // 24 300 | 1106 /1106 | 648 / 0
57
+ //
58
+ // The cliffs are exactly these three capacities. Note the first column: 41% of a
59
+ // cold whole-genome fetch's reads were re-reads of normalization vectors the
60
+ // same fetch had already fetched.
61
+ const BLOCK_CACHE_MAX_ENTRIES = 1024;
62
+ // The bound the entry cap was standing in for. Blocks became struct-of-arrays
63
+ // (see contactRecords.ts), so a cached contact is 12 bytes of untraced
64
+ // ArrayBuffer rather than ~55 on the GC-traced heap — but a block still holds
65
+ // every contact in its bin square, which is the one thing here that can hold
66
+ // real memory, and block sizes vary by more than an order of magnitude with
67
+ // binsize. Measured on the file above: 0.05 MB at 2.5 Mb, 0.23 MB at 100 kb. So
68
+ // the entry cap now tracks the working set and this tracks the memory, instead
69
+ // of one number answering both badly. See LRU's WeightOpts.
70
+ const BLOCK_CACHE_MAX_BYTES = 128 * 1024 * 1024;
71
+ // One entry per (chrIdx1, chrIdx2), so the working set is the pair count.
72
+ // 512 covers a 31-region view; human whole-genome is 300. A matrix holds the
73
+ // block index for each of its BP zoom levels, which is small beside a block.
74
+ const MATRIX_CACHE_SIZE = 512;
75
+ // One entry per (type, chrIdx, unit, binsize), so the working set is the
76
+ // displayed CHROMOSOME count, not the pair count — 64 covers a whole-genome
77
+ // human view twice over, which is the headroom a zoom step needs (binsize is in
78
+ // the key, so every entry misses at a new resolution). Entries are cheap:
79
+ // `NormalizationVector` retains only the queried slice ±1000 values.
80
+ const NORM_VECTOR_CACHE_SIZE = 64;
81
+ // Keyed by `${zd.getKey()}_${blockNumber}`, which already carries the binsize,
82
+ // so no separate resolution generation is needed to keep entries apart —
83
+ // hic-straw's extra `resolution` field only made the cache single-resolution
84
+ // (every zoom step threw the previous level away).
85
+ function blockCacheWeight(block) {
86
+ const { bin1, bin2, counts } = block.records;
87
+ return bin1.byteLength + bin2.byteLength + counts.byteLength;
88
+ }
89
+ function getNormalizationVectorKey(type, chrIdx, unit, resolution) {
90
+ return `${type}_${chrIdx}_${unit}_${resolution}`;
91
+ }
92
+ export class HicFile {
93
+ config;
94
+ file;
95
+ // Both caches hold the in-flight PROMISE, not the resolved value. A
96
+ // multi-region fetch runs its region pairs concurrently and they share
97
+ // chromosomes, so caching only the result had every concurrent pair miss and
98
+ // re-issue the same reads — measured +12 range requests on a 6-pair fetch.
99
+ // Same shape as `initPromise`/`normVectorIndexP`, rejection eviction included.
100
+ normVectorCache = new LRU(NORM_VECTOR_CACHE_SIZE);
101
+ normalizationTypes = ['NONE'];
102
+ matrixCache = new LRU(MATRIX_CACHE_SIZE);
103
+ blockCache = new LRU(BLOCK_CACHE_MAX_ENTRIES, {
104
+ maxBytes: BLOCK_CACHE_MAX_BYTES,
105
+ weigh: blockCacheWeight,
106
+ });
107
+ normVectorIndexPosition = -1;
108
+ normVectorIndexSize = -1;
109
+ initPromise;
110
+ normVectorIndexP;
111
+ version = 0;
112
+ genomeId = '';
113
+ footerPosition = 0;
114
+ // Where the file's normalization data begins, and so also where a file
115
+ // carrying none of it ends. Public so a test can cut a real file there to
116
+ // make that second shape.
117
+ normExpectedValueVectorsPosition;
118
+ normVectorIndex;
119
+ chromosomes = [];
120
+ chromosomeIndexMap = {};
121
+ chrAliasTable = {};
122
+ bpResolutions = [];
123
+ masterIndex = {};
124
+ meta;
125
+ constructor(config) {
126
+ const { filehandle, path, reader } = config;
127
+ this.config = config;
128
+ if (reader) {
129
+ this.file = reader;
130
+ }
131
+ else if (filehandle) {
132
+ this.file = readerFromFilehandle(filehandle);
133
+ }
134
+ else if (path) {
135
+ this.file = readerFromFilehandle(new LocalFile(path));
136
+ }
137
+ else {
138
+ throw new Error('must supply filehandle, path, or reader');
139
+ }
140
+ }
141
+ async init() {
142
+ // Memoize the promise, not a boolean flag: two concurrent callers must
143
+ // share one header parse rather than both racing readHeaderAndFooter. Clear
144
+ // it on failure so a later call retries instead of caching a rejected
145
+ // promise forever.
146
+ this.initPromise ??= this.readHeaderAndFooter().catch((e) => {
147
+ this.initPromise = undefined;
148
+ throw e;
149
+ });
150
+ return this.initPromise;
151
+ }
152
+ async getMetaData() {
153
+ await this.init();
154
+ return this.meta;
155
+ }
156
+ async readHeaderAndFooter() {
157
+ // `init` clears its memoized promise on failure so a transient read error
158
+ // retries, and this parse appends as it goes — so a retry has to start from
159
+ // empty or a failure part-way through the chromosome loop leaves the second
160
+ // run appending to the first's partial output (duplicate refNames out of
161
+ // `getRefNames`, a duplicated binsize list out of `getMetaData`).
162
+ this.chromosomes = [];
163
+ this.chromosomeIndexMap = {};
164
+ this.chrAliasTable = {};
165
+ this.bpResolutions = [];
166
+ this.masterIndex = {};
167
+ // Read initial fields magic, version, and footer position
168
+ let data = await this.file.read(0, 16);
169
+ if (data.byteLength === 0) {
170
+ throw new Error('File content is empty');
171
+ }
172
+ let binaryParser = new BinaryParser(new DataView(data));
173
+ binaryParser.getString(); // magic
174
+ this.version = binaryParser.getInt();
175
+ if (this.version < 5) {
176
+ throw new Error(`Unsupported hic version: ${this.version}`);
177
+ }
178
+ this.footerPosition = binaryParser.getLong();
179
+ // Read footer to determine file position of body section (end of header)
180
+ await this.readFooter();
181
+ const bodyPosition = Object.values(this.masterIndex).reduce((min, entry) => Math.min(min, entry.start), Number.MAX_VALUE);
182
+ data = await this.file.read(16, bodyPosition - 16);
183
+ binaryParser = new BinaryParser(new DataView(data));
184
+ this.genomeId = binaryParser.getString();
185
+ if (this.version >= 9) {
186
+ this.normVectorIndexPosition = binaryParser.getLong();
187
+ this.normVectorIndexSize = binaryParser.getLong();
188
+ }
189
+ let nAttributes = binaryParser.getInt();
190
+ while (nAttributes-- > 0) {
191
+ binaryParser.getString(); // attribute key
192
+ binaryParser.getString(); // attribute value
193
+ }
194
+ let nChrs = binaryParser.getInt();
195
+ let i = 0;
196
+ while (nChrs-- > 0) {
197
+ const chr = {
198
+ index: i,
199
+ name: binaryParser.getString(),
200
+ size: this.version < 9 ? binaryParser.getInt() : binaryParser.getLong(),
201
+ };
202
+ this.chromosomes.push(chr);
203
+ this.chromosomeIndexMap[chr.name] = chr.index;
204
+ i++;
205
+ }
206
+ let nBpResolutions = binaryParser.getInt();
207
+ while (nBpResolutions-- > 0) {
208
+ this.bpResolutions.push(binaryParser.getInt());
209
+ }
210
+ // Build lookup table for well-known chr aliases
211
+ for (const chrName of Object.keys(this.chromosomeIndexMap)) {
212
+ if (chrName.startsWith('chr')) {
213
+ this.chrAliasTable[chrName.slice(3)] = chrName;
214
+ }
215
+ else if (chrName === 'MT') {
216
+ this.chrAliasTable.chrM = chrName;
217
+ }
218
+ else {
219
+ this.chrAliasTable[`chr${chrName}`] = chrName;
220
+ }
221
+ }
222
+ this.meta = {
223
+ version: this.version,
224
+ genome: this.genomeId,
225
+ chromosomes: this.chromosomes,
226
+ resolutions: this.bpResolutions,
227
+ };
228
+ }
229
+ async readFooter() {
230
+ const skip = this.version < 9 ? 8 : 12;
231
+ let data = await this.file.read(this.footerPosition, skip);
232
+ let binaryParser = new BinaryParser(new DataView(data));
233
+ // Total size, master index + expected values
234
+ const nBytes = this.version < 9 ? binaryParser.getInt() : binaryParser.getLong();
235
+ let nEntries = binaryParser.getInt();
236
+ // Estimate the size of the master index. String key length is unknown, be
237
+ // conservative (100 bytes).
238
+ const miSize = nEntries * (100 + 64 + 32);
239
+ data = await this.file.read(this.footerPosition + skip, Math.min(miSize, nBytes));
240
+ binaryParser = new BinaryParser(new DataView(data));
241
+ while (nEntries-- > 0) {
242
+ const key = binaryParser.getString();
243
+ const pos = binaryParser.getLong();
244
+ const size = binaryParser.getInt();
245
+ this.masterIndex[key] = { start: pos, size };
246
+ }
247
+ // Normalized expected values start after the expected values
248
+ if (this.version > 5) {
249
+ const s = this.version < 9 ? 4 : 8;
250
+ this.normExpectedValueVectorsPosition = this.footerPosition + s + nBytes;
251
+ }
252
+ }
253
+ async getMatrix(chrIdx1, chrIdx2) {
254
+ const key = Matrix.getKey(chrIdx1, chrIdx2);
255
+ let p = this.matrixCache.get(key);
256
+ if (!p) {
257
+ p = this.readMatrix(chrIdx1, chrIdx2).catch((e) => {
258
+ this.matrixCache.delete(key);
259
+ throw e;
260
+ });
261
+ this.matrixCache.set(key, p);
262
+ }
263
+ return p;
264
+ }
265
+ async readMatrix(chrIdx1, chrIdx2) {
266
+ await this.init();
267
+ const idx = this.masterIndex[Matrix.getKey(chrIdx1, chrIdx2)];
268
+ let matrix;
269
+ if (idx) {
270
+ const data = await this.file.read(idx.start, idx.size);
271
+ matrix = Matrix.parseMatrix(data, this.chromosomes);
272
+ }
273
+ return matrix;
274
+ }
275
+ async getContactRecords(normalization, region1, region2, units, binsize) {
276
+ await this.init();
277
+ const idx1 = this.chromosomeIndexMap[this.getFileChrName(region1.chr)];
278
+ const idx2 = this.chromosomeIndexMap[this.getFileChrName(region2.chr)];
279
+ // A `.hic` stores only `bin1 <= bin2`, so a query whose x window sits to the
280
+ // right of its y window has to be swapped or it asks for the half of the
281
+ // matrix that does not exist. The same-chromosome test compares **starts**,
282
+ // not this region's start against the other's end: upstream hic-straw used
283
+ // `region1.start >= region2.end`, which catches a reversed pair only while
284
+ // the two are disjoint, and a multi-region view is free to display two
285
+ // *overlapping* regions of one chromosome right-to-left. Measured on
286
+ // chr1 at 2.5 Mb, `(100-200Mb, 50-150Mb)` returned 78 contacts against 901
287
+ // for the same pair in genomic order — everything but the overlap sliver
288
+ // silently missing, which renders as a sparse off-diagonal block rather
289
+ // than as an error. Comparing starts fires on both the disjoint and the
290
+ // overlapping case and leaves forward order (and an identical pair) alone.
291
+ const transpose = idx1 !== undefined &&
292
+ idx2 !== undefined &&
293
+ (idx1 > idx2 || (idx1 === idx2 && region1.start > region2.start));
294
+ const r1 = transpose ? region2 : region1;
295
+ const r2 = transpose ? region1 : region2;
296
+ const [x1, x2] = binWindow(r1, binsize);
297
+ const [y1, y2] = binWindow(r2, binsize);
298
+ // Two independent read chains, so they run concurrently rather than in
299
+ // sequence. The normalization vectors are keyed on (type, chr, unit,
300
+ // binsize) and the blocks on the region pair; neither reads anything the
301
+ // other produces, and awaiting them in order made every pair pay the SUM of
302
+ // their round-trip depths instead of the deeper of the two.
303
+ //
304
+ // Both are two hops — norm-vector header then its values, matrix header then
305
+ // its blocks — so a pair went 4 sequential waves deep where 2 will do
306
+ // (`readChainDepth.test.ts` measures this against the real file). That is
307
+ // the everyday single-region fetch; a multi-region view then runs several
308
+ // pairs at a time on top of it. This loop is latency-bound, not CPU-bound.
309
+ //
310
+ // Vectors are loop-invariant across blocks either way, so they are still
311
+ // resolved once per pair rather than per block, each paired with the bin
312
+ // offset its values start at.
313
+ const [norm, blocks] = await Promise.all([
314
+ this.getNormVectors(normalization, r1, r2, units, binsize),
315
+ this.getBlocks(r1, r2, binsize),
316
+ ]);
317
+ // Sum of the blocks' record counts bounds the survivors, so the output is
318
+ // allocated once and filled by a write cursor. Blocks overlap the window
319
+ // rather than nest in it, so the true count isn't known without either this
320
+ // upper bound or a counting pre-pass over the same data.
321
+ let capacity = 0;
322
+ for (const block of blocks) {
323
+ if (block) {
324
+ capacity += block.records.bin1.length;
325
+ }
326
+ }
327
+ const outBin1 = new Int32Array(capacity);
328
+ const outBin2 = new Int32Array(capacity);
329
+ const outCounts = new Float32Array(capacity);
330
+ let n = 0;
331
+ for (const block of blocks) {
332
+ // An undefined block is most likely a base-pair range outside the
333
+ // chromosome
334
+ if (!block) {
335
+ continue;
336
+ }
337
+ const { bin1, bin2, counts } = block.records;
338
+ const len = bin1.length;
339
+ // `norm` is loop-invariant, so it selects the loop rather than being
340
+ // retested per record — which also hoists the vector/offset reads
341
+ if (norm) {
342
+ const { v1, v2, offset1, offset2 } = norm;
343
+ for (let i = 0; i < len; i++) {
344
+ const b1 = bin1[i];
345
+ const b2 = bin2[i];
346
+ if (b1 >= x1 && b1 < x2 && b2 >= y1 && b2 < y2) {
347
+ const nvnv = v1[b1 - offset1] * v2[b2 - offset2];
348
+ if (nvnv !== 0 && !Number.isNaN(nvnv)) {
349
+ outBin1[n] = b1;
350
+ outBin2[n] = b2;
351
+ outCounts[n] = counts[i] / nvnv;
352
+ n++;
353
+ }
354
+ }
355
+ }
356
+ }
357
+ else {
358
+ for (let i = 0; i < len; i++) {
359
+ const b1 = bin1[i];
360
+ const b2 = bin2[i];
361
+ if (b1 >= x1 && b1 < x2 && b2 >= y1 && b2 < y2) {
362
+ outBin1[n] = b1;
363
+ outBin2[n] = b2;
364
+ outCounts[n] = counts[i];
365
+ n++;
366
+ }
367
+ }
368
+ }
369
+ }
370
+ // Views, not copies: a caller concatenating several pairs' results copies
371
+ // into its own exactly-sized arrays anyway.
372
+ const contactRecords = {
373
+ bin1: outBin1.subarray(0, n),
374
+ bin2: outBin2.subarray(0, n),
375
+ counts: outCounts.subarray(0, n),
376
+ };
377
+ // What was actually applied, which is not always what was asked for:
378
+ // normalization vectors are stored per (type, chr, unit, binsize), so a file
379
+ // can offer KR at 5kb and nothing at 2.5Mb. hic-straw warns to the console
380
+ // and silently hands back raw counts; reporting it lets a display tell the
381
+ // user which scheme they are actually looking at.
382
+ //
383
+ // `transposed` says the query was swapped, so `bin1` runs along `region2`.
384
+ // Reported rather than left for the caller to re-derive: it is decided here
385
+ // from this file's own alias table and chromosome indices, and a caller
386
+ // re-deriving it off a divergent chr-name scheme would silently un-swap the
387
+ // wrong axis.
388
+ return {
389
+ records: contactRecords,
390
+ appliedNormalization: norm ? normalization : 'NONE',
391
+ transposed: transpose,
392
+ };
393
+ }
394
+ async getNormVectors(normalization, r1, r2, units, binsize) {
395
+ let result;
396
+ if (normalization && normalization !== 'NONE') {
397
+ const chr1 = this.getFileChrName(r1.chr);
398
+ const chr2 = this.getFileChrName(r2.chr);
399
+ const offset1 = Math.floor(r1.start / binsize);
400
+ const offset2 = Math.floor(r2.start / binsize);
401
+ const nv1 = await this.getNormalizationVector(normalization, chr1, units, binsize);
402
+ const nv2 = chr1 === chr2
403
+ ? nv1
404
+ : await this.getNormalizationVector(normalization, chr2, units, binsize);
405
+ if (nv1 && nv2) {
406
+ result = {
407
+ v1: await nv1.getValues(offset1, Math.ceil(r1.end / binsize)),
408
+ v2: await nv2.getValues(offset2, Math.ceil(r2.end / binsize)),
409
+ offset1,
410
+ offset2,
411
+ };
412
+ }
413
+ }
414
+ return result;
415
+ }
416
+ async getBlocks(region1, region2, binSize) {
417
+ const blockKey = (blockNumber, zd) => `${zd.getKey()}_${blockNumber}`;
418
+ await this.init();
419
+ const chr1 = this.getFileChrName(region1.chr);
420
+ const chr2 = this.getFileChrName(region2.chr);
421
+ const idx1 = this.chromosomeIndexMap[chr1];
422
+ const idx2 = this.chromosomeIndexMap[chr2];
423
+ let blocks = [];
424
+ if (idx1 === undefined) {
425
+ console.warn(`No chromosome named: ${region1.chr}`);
426
+ }
427
+ else if (idx2 === undefined) {
428
+ console.warn(`No chromosome named: ${region2.chr}`);
429
+ }
430
+ else {
431
+ // A chr pair with no matrix at all is routine, not an anomaly: plenty of
432
+ // .hic files store no inter-chromosomal maps, and a multi-region view asks
433
+ // for every pair. Answering with no blocks, rather than warning once per
434
+ // pair per fetch, leaves that for the caller to notice or ignore.
435
+ const matrix = await this.getMatrix(idx1, idx2);
436
+ if (matrix) {
437
+ const zd = matrix.getZoomData(binSize);
438
+ if (!zd) {
439
+ throw new Error(`${NO_DATA_FOR_RESOLUTION}: ${binSize} for map ${region1.chr}-${region2.chr}`);
440
+ }
441
+ const blockNumbers = zd.getBlockNumbers(region1, region2, this.version);
442
+ const blockNumbersToQuery = [];
443
+ for (const num of blockNumbers) {
444
+ const cached = this.blockCache.get(blockKey(num, zd));
445
+ if (cached) {
446
+ blocks.push(cached);
447
+ }
448
+ else {
449
+ blockNumbersToQuery.push(num);
450
+ }
451
+ }
452
+ const newBlocks = await Promise.all(blockNumbersToQuery.map(blockNumber => this.readBlock(blockNumber, zd)));
453
+ for (const block of newBlocks) {
454
+ if (block) {
455
+ this.blockCache.set(blockKey(block.blockNumber, zd), block);
456
+ }
457
+ }
458
+ blocks = blocks.concat(newBlocks);
459
+ }
460
+ }
461
+ return blocks;
462
+ }
463
+ async readBlock(blockNumber, zd) {
464
+ const idx = zd.blockIndex[blockNumber];
465
+ let block;
466
+ if (idx) {
467
+ const data = await this.file.read(idx.filePosition, idx.size);
468
+ const plain = inflate(new Uint8Array(data), {});
469
+ const parser = new BinaryParser(new DataView(plain.buffer, plain.byteOffset, plain.byteLength));
470
+ // Total records in the block, whatever encoding follows — so every branch
471
+ // below knows its exact (or, for the dense encoding, upper-bound) size
472
+ // before it starts reading and never has to grow an array.
473
+ const nRecords = parser.getInt();
474
+ block = { blockNumber, records: this.parseBlockRecords(parser, nRecords) };
475
+ }
476
+ return block;
477
+ }
478
+ /**
479
+ * Decode one block's records straight into typed arrays.
480
+ *
481
+ * Every encoding here knows its length up front, so each array is allocated
482
+ * once and filled by a write cursor. Where a filter can drop records (the
483
+ * dense encoding's empty cells) the arrays are sized to the upper bound and
484
+ * copied down to the true length at the end — blocks are cached for the life
485
+ * of the session, so it is worth one memcpy not to pin an oversized buffer.
486
+ */
487
+ parseBlockRecords(parser, nRecords) {
488
+ if (this.version < 7) {
489
+ const bin1 = new Int32Array(nRecords);
490
+ const bin2 = new Int32Array(nRecords);
491
+ const counts = new Float32Array(nRecords);
492
+ for (let i = 0; i < nRecords; i++) {
493
+ bin1[i] = parser.getInt();
494
+ bin2[i] = parser.getInt();
495
+ counts[i] = parser.getFloat();
496
+ }
497
+ return { bin1, bin2, counts };
498
+ }
499
+ const binXOffset = parser.getInt();
500
+ const binYOffset = parser.getInt();
501
+ const useFloatContact = parser.getByte() === 1;
502
+ const useIntXPos = this.version < 9 ? false : parser.getByte() === 1;
503
+ const useIntYPos = this.version < 9 ? false : parser.getByte() === 1;
504
+ const type = parser.getByte();
505
+ if (type === 1) {
506
+ // List-of-rows representation. The rows partition the block's records, so
507
+ // `nRecords` sizes the arrays exactly; the overflow check is a
508
+ // corrupt-file guard, not a growth path.
509
+ const bin1 = new Int32Array(nRecords);
510
+ const bin2 = new Int32Array(nRecords);
511
+ const counts = new Float32Array(nRecords);
512
+ let n = 0;
513
+ const rowCount = useIntYPos ? parser.getInt() : parser.getShort();
514
+ for (let i = 0; i < rowCount; i++) {
515
+ const dy = useIntYPos ? parser.getInt() : parser.getShort();
516
+ const binY = binYOffset + dy;
517
+ const colCount = useIntXPos ? parser.getInt() : parser.getShort();
518
+ if (n + colCount > nRecords) {
519
+ throw new Error(`hic block declares ${nRecords} records but its rows hold more`);
520
+ }
521
+ for (let j = 0; j < colCount; j++) {
522
+ bin1[n] =
523
+ binXOffset + (useIntXPos ? parser.getInt() : parser.getShort());
524
+ bin2[n] = binY;
525
+ counts[n] = useFloatContact ? parser.getFloat() : parser.getShort();
526
+ n++;
527
+ }
528
+ }
529
+ return truncateRecords({ bin1, bin2, counts }, n);
530
+ }
531
+ if (type === 2) {
532
+ // Dense representation: `nPts` counts every cell of the w-wide rectangle,
533
+ // empty ones included, so it is an upper bound on the surviving records.
534
+ const nPts = parser.getInt();
535
+ const w = parser.getShort();
536
+ const bin1 = new Int32Array(nPts);
537
+ const bin2 = new Int32Array(nPts);
538
+ const counts = new Float32Array(nPts);
539
+ let n = 0;
540
+ for (let i = 0; i < nPts; i++) {
541
+ // read unconditionally: the parser advances a fixed stride per cell
542
+ // whether or not the cell holds a value
543
+ const c = useFloatContact ? parser.getFloat() : parser.getShort();
544
+ // NaN (float) and Short_MIN_VALUE (int) are the "no value" markers
545
+ if (useFloatContact ? !Number.isNaN(c) : c !== Short_MIN_VALUE) {
546
+ const row = Math.floor(i / w);
547
+ bin1[n] = binXOffset + (i - row * w);
548
+ bin2[n] = binYOffset + row;
549
+ counts[n] = c;
550
+ n++;
551
+ }
552
+ }
553
+ return truncateRecords({ bin1, bin2, counts }, n);
554
+ }
555
+ throw new Error(`Unknown block type: ${type}`);
556
+ }
557
+ async getNormalizationVector(type, chr, unit, binSize) {
558
+ await this.init();
559
+ const chrIdx = this.chromosomeIndexMap[this.getFileChrName(chr)];
560
+ if (chrIdx === undefined) {
561
+ return undefined;
562
+ }
563
+ const key = getNormalizationVectorKey(type, chrIdx, unit, binSize);
564
+ // Caching the promise is what keeps concurrent region pairs sharing one
565
+ // vector: a chromosome appears in every pair it takes part in, so with a
566
+ // result-only cache each of those pairs missed while the first was still in
567
+ // flight and read the header again — and then held its own
568
+ // `NormalizationVector`, whose value cache is per instance, so the whole
569
+ // vector was re-read too.
570
+ let p = this.normVectorCache.get(key);
571
+ if (!p) {
572
+ // A file with no vectors at all, or none for this (type, chr, unit,
573
+ // binsize), simply answers undefined and the caller falls back to raw
574
+ // counts. hic-straw warns to the console here; that fires once per
575
+ // chromosome per region pair per fetch, and the console is the wrong place
576
+ // for it anyway — `getContactRecords` reports the normalization it
577
+ // actually applied instead.
578
+ p = this.readNormalizationVector(key).catch((e) => {
579
+ this.normVectorCache.delete(key);
580
+ throw e;
581
+ });
582
+ this.normVectorCache.set(key, p);
583
+ }
584
+ return p;
585
+ }
586
+ async readNormalizationVector(key) {
587
+ const idx = (await this.getNormVectorIndex())?.[key];
588
+ if (!idx) {
589
+ return undefined;
590
+ }
591
+ const data = await this.file.read(idx.filePosition, 8);
592
+ const parser = new BinaryParser(new DataView(data));
593
+ const nValues = this.version < 9 ? parser.getInt() : parser.getLong();
594
+ const dataType = this.version < 9 ? DOUBLE : FLOAT;
595
+ const filePosition = this.version < 9 ? idx.filePosition + 4 : idx.filePosition + 8;
596
+ return new NormalizationVector(this.file, filePosition, nValues, dataType);
597
+ }
598
+ async getNormVectorIndex() {
599
+ // Before the version gate, not after it: `version` is 0 until the header is
600
+ // parsed, so calling this first — which `getNormalizationOptions` on its own
601
+ // does — answered "no index" for every file rather than for a v5 one. A
602
+ // caller that reads metadata first never sees it.
603
+ await this.init();
604
+ if (this.version >= 6) {
605
+ // Memoize the *attempt*, not just a populated result. A legal (if
606
+ // uncommon) v8 file with no norm vectors leaves `normVectorIndex`
607
+ // undefined, and the old `!this.normVectorIndex` guard then re-ran the
608
+ // discovery on every call — two calls per region pair per fetch, each
609
+ // walking the whole normalized-expected-values section with a chain of
610
+ // sequential range reads, only to rediscover there is nothing there.
611
+ // Cleared on failure (like `init`) so a transient read error retries
612
+ // rather than caching a rejection forever.
613
+ this.normVectorIndexP ??= this.loadNormVectorIndex().catch((e) => {
614
+ this.normVectorIndexP = undefined;
615
+ throw e;
616
+ });
617
+ await this.normVectorIndexP;
618
+ }
619
+ return this.normVectorIndex;
620
+ }
621
+ async loadNormVectorIndex() {
622
+ // If we know the position of the norm vector index, read it directly.
623
+ // This is the case for hic v9 files.
624
+ if (this.normVectorIndexPosition > 0 && this.normVectorIndexSize > 0) {
625
+ await this.readNormVectorIndex({
626
+ start: this.normVectorIndexPosition,
627
+ size: this.normVectorIndexSize,
628
+ });
629
+ }
630
+ else if (this.config.nvi) {
631
+ const nviArray = decodeURIComponent(this.config.nvi).split(',');
632
+ await this.readNormVectorIndex({
633
+ start: parseInt(nviArray[0]),
634
+ size: parseInt(nviArray[1]),
635
+ });
636
+ }
637
+ else {
638
+ try {
639
+ await this.readNormExpectedValuesAndNormVectorIndex();
640
+ }
641
+ catch (e) {
642
+ // Not "expected if the file has no norm vectors" — that case never
643
+ // arrives here. hic-straw's own IO threw a 416 for a read past EOF and
644
+ // this caught it; `generic-filehandle2` deliberately turns a 416 into an
645
+ // empty read so callers detect EOF from a short buffer instead of
646
+ // needing a stat, and LocalFile/BlobFile clamp for the same reason. So
647
+ // the read side owns that case now, and anything reaching here is a real
648
+ // failure worth printing.
649
+ console.error(e);
650
+ }
651
+ }
652
+ }
653
+ async getNormalizationOptions() {
654
+ // Normalization options are computed as a side effect of loading the
655
+ // index. A bit ugly but alternatives are worse.
656
+ await this.getNormVectorIndex();
657
+ return this.normalizationTypes;
658
+ }
659
+ async readNormVectorIndex(range) {
660
+ await this.init();
661
+ const data = await this.file.read(range.start, range.size);
662
+ const binaryParser = new BinaryParser(new DataView(data));
663
+ this.normVectorIndex = {};
664
+ let nEntries = binaryParser.getInt();
665
+ while (nEntries-- > 0) {
666
+ this.parseNormVectorEntry(binaryParser);
667
+ }
668
+ return this.normVectorIndex;
669
+ }
670
+ // Used when the position of the norm vector index is unknown: read through
671
+ // the expected values to find the index.
672
+ async readNormExpectedValuesAndNormVectorIndex() {
673
+ await this.init();
674
+ if (this.normExpectedValueVectorsPosition !== undefined) {
675
+ const nviStart = await this.skipExpectedValues(this.normExpectedValueVectorsPosition);
676
+ let byteCount = INT;
677
+ let data = await this.file.read(nviStart, INT);
678
+ // Possible if there are no norm vectors. A legal v8 file, though uncommon.
679
+ if (data.byteLength !== 0) {
680
+ const binaryParser = new BinaryParser(new DataView(data));
681
+ const nEntries = binaryParser.getInt();
682
+ const sizeEstimate = nEntries * 30;
683
+ data = await this.file.read(nviStart + byteCount, sizeEstimate);
684
+ this.normVectorIndex = {};
685
+ const processEntries = async (remaining, buf) => {
686
+ const parser = new BinaryParser(new DataView(buf));
687
+ let n = remaining;
688
+ while (n-- > 0) {
689
+ if (parser.available() < 100) {
690
+ n++; // Reset counter as entry is not processed
691
+ byteCount += parser.position;
692
+ const est = Math.max(1000, n * 30);
693
+ const more = await this.file.read(nviStart + byteCount, est);
694
+ await processEntries(n, more);
695
+ return;
696
+ }
697
+ this.parseNormVectorEntry(parser);
698
+ }
699
+ byteCount += parser.position;
700
+ };
701
+ await processEntries(nEntries, data);
702
+ this.config.nvi = `${nviStart},${byteCount}`;
703
+ }
704
+ }
705
+ }
706
+ // Used when the position of the norm vector index is unknown: skip the
707
+ // normalized expected values to find the index.
708
+ async skipExpectedValues(start) {
709
+ const version = this.version;
710
+ const file = new BufferedFile({ file: this.file, size: 256000 });
711
+ const data = await file.read(start, INT);
712
+ // A file with no normalization at all ends where this section would start,
713
+ // so the count this is about to read is past EOF. That is the same "no norm
714
+ // vectors" case `readNormExpectedValuesAndNormVectorIndex` guards its own
715
+ // read for, and it has to be caught HERE too, because this runs first:
716
+ // a v9 file that records no norm-vector-index position takes the v8
717
+ // discovery path, and that path starts by skipping a section that isn't
718
+ // there. Answering `start` leaves the caller's guard to do the rest.
719
+ if (data.byteLength < INT) {
720
+ return start;
721
+ }
722
+ const binaryParser = new BinaryParser(new DataView(data));
723
+ const nEntries = binaryParser.getInt(); // Total # of expected value chunks
724
+ const parseNext = async (chunkStart, remaining) => {
725
+ let chunkSize = 0;
726
+ const p0 = chunkStart;
727
+ let buf = await file.read(chunkStart, 500);
728
+ let parser = new BinaryParser(new DataView(buf));
729
+ parser.getString(); // type
730
+ parser.getString(); // unit
731
+ parser.getInt(); // binSize
732
+ const nValues = version < 9 ? parser.getInt() : parser.getLong();
733
+ chunkSize += parser.position + nValues * (version < 9 ? DOUBLE : FLOAT);
734
+ buf = await file.read(chunkStart + chunkSize, INT);
735
+ parser = new BinaryParser(new DataView(buf));
736
+ const nChrScaleFactors = parser.getInt();
737
+ chunkSize +=
738
+ INT + nChrScaleFactors * (INT + (version < 9 ? DOUBLE : FLOAT));
739
+ return remaining - 1 === 0
740
+ ? p0 + chunkSize
741
+ : parseNext(p0 + chunkSize, remaining - 1);
742
+ };
743
+ return nEntries === 0 ? start + INT : parseNext(start + INT, nEntries);
744
+ }
745
+ parseNormVectorEntry(binaryParser) {
746
+ const type = binaryParser.getString(); // 15
747
+ const chrIdx = binaryParser.getInt(); // 4
748
+ const unit = binaryParser.getString(); // 3
749
+ const binSize = binaryParser.getInt(); // 4
750
+ const filePosition = binaryParser.getLong(); // 8
751
+ const sizeInBytes = this.version < 9 ? binaryParser.getInt() : binaryParser.getLong(); // 4:8
752
+ const key = `${type}_${chrIdx}_${unit}_${binSize}`;
753
+ if (!this.normalizationTypes.includes(type)) {
754
+ this.normalizationTypes.push(type);
755
+ }
756
+ this.normVectorIndex[key] = { filePosition, size: sizeInBytes };
757
+ }
758
+ getFileChrName(chrAlias) {
759
+ return this.chrAliasTable[chrAlias] ?? chrAlias;
760
+ }
761
+ }
762
+ //# sourceMappingURL=hicFile.js.map