@gmod/hic 1.3.1 → 2.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.
@@ -4,9 +4,16 @@ import type { BaseOpts, Reader } from './types.ts'
4
4
 
5
5
  const DOUBLE = 8
6
6
 
7
+ interface CachedValues {
8
+ start: number
9
+ end: number
10
+ values: Promise<Float64Array>
11
+ }
12
+
7
13
  export default class NormalizationVector {
8
- private cache:
9
- { start: number; end: number; values: Float64Array } | undefined
14
+ // Holds the in-flight read, so the two concurrent `getValues` calls of a
15
+ // same-chromosome pair share one read instead of both missing.
16
+ private cache: CachedValues | undefined
10
17
 
11
18
  private file: Reader
12
19
  private filePosition: number
@@ -25,50 +32,77 @@ export default class NormalizationVector {
25
32
  this.dataType = dataType
26
33
  }
27
34
 
28
- async getValues(start: number, end: number, opts?: BaseOpts) {
29
- // The read below clamps to `nValues`, so the cache can never cover past it.
30
- // Compare against the clamped bound or a request that runs off the end of
31
- // the vector — reachable when the assembly's refseq is longer than the size
32
- // the `.hic` recorded for that chromosome — leaves `end > cache.end` true
33
- // forever, and every call re-reads the same range from the file.
35
+ async getValues(
36
+ start: number,
37
+ end: number,
38
+ opts?: BaseOpts,
39
+ ): Promise<Float64Array> {
40
+ // The read clamps to `nValues`, so the cache can never cover past it.
41
+ // Comparing against the clamped bound keeps a request running off the end
42
+ // of the vector (an assembly refseq longer than the `.hic` recorded) from
43
+ // missing forever.
34
44
  const wanted = Math.min(end, this.nValues)
35
- if (!this.cache || start < this.cache.start || wanted > this.cache.end) {
36
- const adjustedEnd = Math.min(this.nValues, end + 1000)
37
- // Clamped against `adjustedEnd`, and not merely against zero, or a window
38
- // starting past the vector produces a NEGATIVE `n` — and a negative read
39
- // length reaches the reader as `new Uint8Array(-8)`, a RangeError that
40
- // fails the whole fetch instead of the empty answer the caller below
41
- // already handles. The padding is what sets how far past is far enough:
42
- // more than 1000 bins, which at a fine binsize is not far at all.
43
- //
44
- // Reachable exactly where the comment above says: the assembly's refseq
45
- // is longer than the size the `.hic` recorded for that chromosome, so a
46
- // view of the tail of it asks for bins the vector never had.
47
- const adjustedStart = Math.min(Math.max(0, start - 1000), adjustedEnd)
48
- const startPosition = this.filePosition + adjustedStart * this.dataType
49
- const n = adjustedEnd - adjustedStart
50
- // Nothing to read when the window is entirely past the vector, and on a
51
- // remote file a zero-length range request is still a round trip — one per
52
- // region pair, on every fetch, for as long as the view stays there.
53
- const data =
54
- n > 0
55
- ? await this.file.read(startPosition, n * this.dataType, opts)
56
- : new ArrayBuffer(0)
57
- const parser = new BinaryParser(new DataView(data))
58
- const values = new Float64Array(n)
59
- for (let i = 0; i < n; i++) {
60
- values[i] =
61
- this.dataType === DOUBLE ? parser.getDouble() : parser.getFloat()
45
+ let cache = this.cache
46
+ const joined = !!cache && start >= cache.start && wanted <= cache.end
47
+ if (!cache || !joined) {
48
+ cache = this.readRange(start, end, opts)
49
+ this.cache = cache
50
+ const pending = cache
51
+ pending.values.catch(() => {
52
+ if (this.cache === pending) {
53
+ this.cache = undefined
54
+ }
55
+ })
56
+ }
57
+ let values: Float64Array
58
+ try {
59
+ values = await cache.values
60
+ } catch (e) {
61
+ // The pair that started a joined read passed its own signal, so that
62
+ // pair aborting says nothing about this caller.
63
+ if (!joined || opts?.signal?.aborted) {
64
+ throw e
62
65
  }
63
- this.cache = { start: adjustedStart, end: adjustedEnd, values }
66
+ return this.getValues(start, end, opts)
67
+ }
68
+
69
+ // A view, not a copy. Both this and `subarray` clamp a request running past
70
+ // the end; an out-of-range index reads undefined, the product goes NaN, and
71
+ // the record filter drops it.
72
+ const sliceStart = start - cache.start
73
+ return values.subarray(sliceStart, sliceStart + (end - start))
74
+ }
75
+
76
+ private readRange(start: number, end: number, opts?: BaseOpts): CachedValues {
77
+ const adjustedEnd = Math.min(this.nValues, end + 1000)
78
+ // Clamped against `adjustedEnd` too, or a window starting past the vector
79
+ // gives a negative read length, which the reader turns into a RangeError
80
+ // instead of the empty answer the caller handles.
81
+ const adjustedStart = Math.min(Math.max(0, start - 1000), adjustedEnd)
82
+ const n = adjustedEnd - adjustedStart
83
+ return {
84
+ start: adjustedStart,
85
+ end: adjustedEnd,
86
+ values: this.readValues(adjustedStart, n, opts),
64
87
  }
88
+ }
65
89
 
66
- // A view, not a copy: the caller only reads it, and a fetch asks for one of
67
- // these per chromosome per region pair. Both this and `slice` clamp a
68
- // request running past the end to what exists, which the caller already
69
- // handles — an out-of-range index reads undefined, making the product NaN,
70
- // which the record filter drops.
71
- const sliceStart = start - this.cache.start
72
- return this.cache.values.subarray(sliceStart, sliceStart + (end - start))
90
+ private async readValues(start: number, n: number, opts?: BaseOpts) {
91
+ // A zero-length range request on a remote file is still a round trip.
92
+ if (n <= 0) {
93
+ return new Float64Array(0)
94
+ }
95
+ const data = await this.file.read(
96
+ this.filePosition + start * this.dataType,
97
+ n * this.dataType,
98
+ opts,
99
+ )
100
+ const parser = new BinaryParser(new DataView(data))
101
+ const values = new Float64Array(n)
102
+ for (let i = 0; i < n; i++) {
103
+ values[i] =
104
+ this.dataType === DOUBLE ? parser.getDouble() : parser.getFloat()
105
+ }
106
+ return values
73
107
  }
74
108
  }
package/dist/lru.d.ts DELETED
@@ -1,29 +0,0 @@
1
- /**
2
- * Optional second bound, for a cache whose entries differ wildly in size.
3
- *
4
- * An entry cap is a memory bound only while entries are interchangeable, and the
5
- * block cache's are not: a block holds every contact in its bin square, so one
6
- * can be a few thousand contacts or a few hundred thousand depending on binsize
7
- * and distance from the diagonal. Capping entries therefore forced one number to
8
- * answer two questions — how many blocks a fetch needs at once, and how much
9
- * memory the biggest of them may hold — and the memory question won, which is
10
- * what left the cache too small to serve a multi-region fetch (see hicFile.ts).
11
- *
12
- * Splitting them lets the entry cap track the working set while `maxBytes`
13
- * stays the backstop it was standing in for.
14
- */
15
- interface WeightOpts<V> {
16
- maxBytes: number;
17
- weigh: (value: V) => number;
18
- }
19
- export default class LRU<K, V> {
20
- private max;
21
- private map;
22
- private weight;
23
- private weightOpts;
24
- constructor(max?: number, weightOpts?: WeightOpts<V>);
25
- get(key: K): V | undefined;
26
- set(key: K, val: V): void;
27
- delete(key: K): void;
28
- }
29
- export {};
package/dist/lru.js DELETED
@@ -1,53 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- class LRU {
4
- max;
5
- map = new Map();
6
- weight = 0;
7
- weightOpts;
8
- constructor(max = 10, weightOpts) {
9
- this.max = max;
10
- this.weightOpts = weightOpts;
11
- }
12
- get(key) {
13
- const item = this.map.get(key);
14
- if (item !== undefined) {
15
- // refresh key
16
- this.map.delete(key);
17
- this.map.set(key, item);
18
- }
19
- return item;
20
- }
21
- set(key, val) {
22
- if (this.map.has(key)) {
23
- // refresh key, and drop its old weight — `set` on an existing key
24
- // replaces the value, so the weight it contributed goes with it
25
- this.delete(key);
26
- }
27
- this.map.set(key, val);
28
- this.weight += this.weightOpts?.weigh(val) ?? 0;
29
- // Entry cap first, then the byte budget. `size > 1` keeps a single entry
30
- // larger than the whole budget rather than evicting it and looping forever
31
- // on an empty map — a cache that can't hold the thing just asked for should
32
- // still answer the caller that asked.
33
- while (this.map.size > this.max ||
34
- (this.weightOpts !== undefined &&
35
- this.weight > this.weightOpts.maxBytes &&
36
- this.map.size > 1)) {
37
- const oldest = this.map.keys().next().value;
38
- if (oldest === undefined) {
39
- break;
40
- }
41
- this.delete(oldest);
42
- }
43
- }
44
- delete(key) {
45
- const existing = this.map.get(key);
46
- if (existing !== undefined) {
47
- this.weight -= this.weightOpts?.weigh(existing) ?? 0;
48
- }
49
- this.map.delete(key);
50
- }
51
- }
52
- exports.default = LRU;
53
- //# sourceMappingURL=lru.js.map
package/dist/lru.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"lru.js","sourceRoot":"","sources":["../src/lru.ts"],"names":[],"mappings":";;AAmBA,MAAqB,GAAG;IACd,GAAG,CAAQ;IACX,GAAG,GAAG,IAAI,GAAG,EAAQ,CAAA;IACrB,MAAM,GAAG,CAAC,CAAA;IACV,UAAU,CAA2B;IAE7C,YAAY,GAAG,GAAG,EAAE,EAAE,UAA0B;QAC9C,IAAI,CAAC,GAAG,GAAG,GAAG,CAAA;QACd,IAAI,CAAC,UAAU,GAAG,UAAU,CAAA;IAC9B,CAAC;IAED,GAAG,CAAC,GAAM;QACR,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC9B,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,cAAc;YACd,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YACpB,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;QACzB,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,GAAG,CAAC,GAAM,EAAE,GAAM;QAChB,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACtB,kEAAkE;YAClE,gEAAgE;YAChE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAClB,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;QACtB,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QAE/C,yEAAyE;QACzE,2EAA2E;QAC3E,4EAA4E;QAC5E,sCAAsC;QACtC,OACE,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG;YACxB,CAAC,IAAI,CAAC,UAAU,KAAK,SAAS;gBAC5B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,QAAQ;gBACtC,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,EACpB,CAAC;YACD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAA;YAC3C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBACzB,MAAK;YACP,CAAC;YACD,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QACrB,CAAC;IACH,CAAC;IAED,MAAM,CAAC,GAAM;QACX,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAClC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;QACtD,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IACtB,CAAC;CACF;AAvDD,sBAuDC"}
package/esm/lru.d.ts DELETED
@@ -1,29 +0,0 @@
1
- /**
2
- * Optional second bound, for a cache whose entries differ wildly in size.
3
- *
4
- * An entry cap is a memory bound only while entries are interchangeable, and the
5
- * block cache's are not: a block holds every contact in its bin square, so one
6
- * can be a few thousand contacts or a few hundred thousand depending on binsize
7
- * and distance from the diagonal. Capping entries therefore forced one number to
8
- * answer two questions — how many blocks a fetch needs at once, and how much
9
- * memory the biggest of them may hold — and the memory question won, which is
10
- * what left the cache too small to serve a multi-region fetch (see hicFile.ts).
11
- *
12
- * Splitting them lets the entry cap track the working set while `maxBytes`
13
- * stays the backstop it was standing in for.
14
- */
15
- interface WeightOpts<V> {
16
- maxBytes: number;
17
- weigh: (value: V) => number;
18
- }
19
- export default class LRU<K, V> {
20
- private max;
21
- private map;
22
- private weight;
23
- private weightOpts;
24
- constructor(max?: number, weightOpts?: WeightOpts<V>);
25
- get(key: K): V | undefined;
26
- set(key: K, val: V): void;
27
- delete(key: K): void;
28
- }
29
- export {};
package/esm/lru.js DELETED
@@ -1,50 +0,0 @@
1
- export default class LRU {
2
- max;
3
- map = new Map();
4
- weight = 0;
5
- weightOpts;
6
- constructor(max = 10, weightOpts) {
7
- this.max = max;
8
- this.weightOpts = weightOpts;
9
- }
10
- get(key) {
11
- const item = this.map.get(key);
12
- if (item !== undefined) {
13
- // refresh key
14
- this.map.delete(key);
15
- this.map.set(key, item);
16
- }
17
- return item;
18
- }
19
- set(key, val) {
20
- if (this.map.has(key)) {
21
- // refresh key, and drop its old weight — `set` on an existing key
22
- // replaces the value, so the weight it contributed goes with it
23
- this.delete(key);
24
- }
25
- this.map.set(key, val);
26
- this.weight += this.weightOpts?.weigh(val) ?? 0;
27
- // Entry cap first, then the byte budget. `size > 1` keeps a single entry
28
- // larger than the whole budget rather than evicting it and looping forever
29
- // on an empty map — a cache that can't hold the thing just asked for should
30
- // still answer the caller that asked.
31
- while (this.map.size > this.max ||
32
- (this.weightOpts !== undefined &&
33
- this.weight > this.weightOpts.maxBytes &&
34
- this.map.size > 1)) {
35
- const oldest = this.map.keys().next().value;
36
- if (oldest === undefined) {
37
- break;
38
- }
39
- this.delete(oldest);
40
- }
41
- }
42
- delete(key) {
43
- const existing = this.map.get(key);
44
- if (existing !== undefined) {
45
- this.weight -= this.weightOpts?.weigh(existing) ?? 0;
46
- }
47
- this.map.delete(key);
48
- }
49
- }
50
- //# sourceMappingURL=lru.js.map
package/esm/lru.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"lru.js","sourceRoot":"","sources":["../src/lru.ts"],"names":[],"mappings":"AAmBA,MAAM,CAAC,OAAO,OAAO,GAAG;IACd,GAAG,CAAQ;IACX,GAAG,GAAG,IAAI,GAAG,EAAQ,CAAA;IACrB,MAAM,GAAG,CAAC,CAAA;IACV,UAAU,CAA2B;IAE7C,YAAY,GAAG,GAAG,EAAE,EAAE,UAA0B;QAC9C,IAAI,CAAC,GAAG,GAAG,GAAG,CAAA;QACd,IAAI,CAAC,UAAU,GAAG,UAAU,CAAA;IAC9B,CAAC;IAED,GAAG,CAAC,GAAM;QACR,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC9B,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,cAAc;YACd,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YACpB,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;QACzB,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,GAAG,CAAC,GAAM,EAAE,GAAM;QAChB,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACtB,kEAAkE;YAClE,gEAAgE;YAChE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAClB,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;QACtB,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QAE/C,yEAAyE;QACzE,2EAA2E;QAC3E,4EAA4E;QAC5E,sCAAsC;QACtC,OACE,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG;YACxB,CAAC,IAAI,CAAC,UAAU,KAAK,SAAS;gBAC5B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,QAAQ;gBACtC,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,EACpB,CAAC;YACD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAA;YAC3C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBACzB,MAAK;YACP,CAAC;YACD,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QACrB,CAAC;IACH,CAAC;IAED,MAAM,CAAC,GAAM;QACX,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAClC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;QACtD,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IACtB,CAAC;CACF"}
package/src/lru.ts DELETED
@@ -1,75 +0,0 @@
1
- /**
2
- * Optional second bound, for a cache whose entries differ wildly in size.
3
- *
4
- * An entry cap is a memory bound only while entries are interchangeable, and the
5
- * block cache's are not: a block holds every contact in its bin square, so one
6
- * can be a few thousand contacts or a few hundred thousand depending on binsize
7
- * and distance from the diagonal. Capping entries therefore forced one number to
8
- * answer two questions — how many blocks a fetch needs at once, and how much
9
- * memory the biggest of them may hold — and the memory question won, which is
10
- * what left the cache too small to serve a multi-region fetch (see hicFile.ts).
11
- *
12
- * Splitting them lets the entry cap track the working set while `maxBytes`
13
- * stays the backstop it was standing in for.
14
- */
15
- interface WeightOpts<V> {
16
- maxBytes: number
17
- weigh: (value: V) => number
18
- }
19
-
20
- export default class LRU<K, V> {
21
- private max: number
22
- private map = new Map<K, V>()
23
- private weight = 0
24
- private weightOpts: WeightOpts<V> | undefined
25
-
26
- constructor(max = 10, weightOpts?: WeightOpts<V>) {
27
- this.max = max
28
- this.weightOpts = weightOpts
29
- }
30
-
31
- get(key: K) {
32
- const item = this.map.get(key)
33
- if (item !== undefined) {
34
- // refresh key
35
- this.map.delete(key)
36
- this.map.set(key, item)
37
- }
38
- return item
39
- }
40
-
41
- set(key: K, val: V) {
42
- if (this.map.has(key)) {
43
- // refresh key, and drop its old weight — `set` on an existing key
44
- // replaces the value, so the weight it contributed goes with it
45
- this.delete(key)
46
- }
47
- this.map.set(key, val)
48
- this.weight += this.weightOpts?.weigh(val) ?? 0
49
-
50
- // Entry cap first, then the byte budget. `size > 1` keeps a single entry
51
- // larger than the whole budget rather than evicting it and looping forever
52
- // on an empty map — a cache that can't hold the thing just asked for should
53
- // still answer the caller that asked.
54
- while (
55
- this.map.size > this.max ||
56
- (this.weightOpts !== undefined &&
57
- this.weight > this.weightOpts.maxBytes &&
58
- this.map.size > 1)
59
- ) {
60
- const oldest = this.map.keys().next().value
61
- if (oldest === undefined) {
62
- break
63
- }
64
- this.delete(oldest)
65
- }
66
- }
67
-
68
- delete(key: K) {
69
- const existing = this.map.get(key)
70
- if (existing !== undefined) {
71
- this.weight -= this.weightOpts?.weigh(existing) ?? 0
72
- }
73
- this.map.delete(key)
74
- }
75
- }