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