@jbrowse/graph-walks 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +89 -2
- package/esm/index.d.ts +7 -0
- package/esm/index.js +4 -0
- package/esm/util.d.ts +39 -0
- package/esm/util.js +34 -0
- package/esm/walkFormat2.d.ts +64 -0
- package/esm/walkFormat2.js +589 -0
- package/esm/walkReader.d.ts +77 -0
- package/esm/walkReader.js +250 -0
- package/esm/walkRows.d.ts +172 -0
- package/esm/walkRows.js +579 -0
- package/package.json +62 -4
package/esm/walkRows.js
ADDED
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
import { RegionTooLargeError, bpString, panSNMatchesPrefix } from "./util.js";
|
|
2
|
+
import { placeAndLink } from "./walkFormat2.js";
|
|
3
|
+
// The name column, read without splitting the steps, so a row the cut does
|
|
4
|
+
// not want costs a few indexOf calls
|
|
5
|
+
export function walkRowName(line) {
|
|
6
|
+
let at = -1;
|
|
7
|
+
for (let k = 0; k < 3; k++) {
|
|
8
|
+
at = line.indexOf('\t', at + 1);
|
|
9
|
+
}
|
|
10
|
+
return line.slice(at + 1, line.indexOf('\t', at + 1));
|
|
11
|
+
}
|
|
12
|
+
export function parseWalkRow(line) {
|
|
13
|
+
const c = line.split('\t');
|
|
14
|
+
const row = {
|
|
15
|
+
chunkStart: +c[1],
|
|
16
|
+
name: c[3],
|
|
17
|
+
fragStart: +c[4],
|
|
18
|
+
hapOffset: +c[5],
|
|
19
|
+
piece: +c[6],
|
|
20
|
+
n: +c[7],
|
|
21
|
+
enc: c[8],
|
|
22
|
+
};
|
|
23
|
+
for (let k = 9; k < c.length; k++) {
|
|
24
|
+
const tag = c[k];
|
|
25
|
+
const side = tag.startsWith('pv:')
|
|
26
|
+
? 'prev'
|
|
27
|
+
: tag.startsWith('nx:')
|
|
28
|
+
? 'next'
|
|
29
|
+
: undefined;
|
|
30
|
+
if (side) {
|
|
31
|
+
const at = tag.lastIndexOf(':');
|
|
32
|
+
row[side] = {
|
|
33
|
+
refName: tag[3] === 'Z' ? tag.slice(5, at) : c[0],
|
|
34
|
+
start: +tag.slice(at + 1),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return row;
|
|
39
|
+
}
|
|
40
|
+
function integerTag(line, tag) {
|
|
41
|
+
const match = new RegExp(`(?:^|\\t)${tag}:i:(\\d+)`).exec(line);
|
|
42
|
+
return match ? +match[1] : undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* What a walk file's header states: its chunk size in a
|
|
46
|
+
* `#walks\tchunk:i:65536` line, from gfa-to-tabix 0.4.0 a
|
|
47
|
+
* `#reference\tGRCh38` line per reference sample and a
|
|
48
|
+
* `#haplotype\tHG002#1` line per other haplotype with rows, and from 0.5.0
|
|
49
|
+
* the longest node in bp as `maxnode:i:1024` beside the chunk size
|
|
50
|
+
*/
|
|
51
|
+
export function walkHeader(lines) {
|
|
52
|
+
var _a, _b, _c;
|
|
53
|
+
const header = { references: [], haplotypes: [] };
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
const [key, value = ''] = line.split('\t');
|
|
56
|
+
if (key === '#reference') {
|
|
57
|
+
header.references.push(value);
|
|
58
|
+
}
|
|
59
|
+
else if (key === '#haplotype') {
|
|
60
|
+
header.haplotypes.push(value);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
(_a = header.format) !== null && _a !== void 0 ? _a : (header.format = integerTag(line, 'format'));
|
|
64
|
+
(_b = header.chunk) !== null && _b !== void 0 ? _b : (header.chunk = integerTag(line, 'chunk'));
|
|
65
|
+
(_c = header.maxNode) !== null && _c !== void 0 ? _c : (header.maxNode = integerTag(line, 'maxnode'));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return header;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* How many chunks before the window's own a cut reads. A node is filed under
|
|
72
|
+
* the chunk holding its start, so a node `maxNode` bp long reaching into the
|
|
73
|
+
* window starts at most ceil(maxNode / chunk) chunks back. At least one, since
|
|
74
|
+
* the chunk before carries the detour a haplotype takes into the window's
|
|
75
|
+
* first reference node; one when the header gives no `maxnode:i:`, which holds
|
|
76
|
+
* for vg's graphs, chopped to nodes of at most 1,024 bp.
|
|
77
|
+
*/
|
|
78
|
+
export function lookbackChunks(maxNode, chunk) {
|
|
79
|
+
return maxNode === undefined ? 1 : Math.max(1, Math.ceil(maxNode / chunk));
|
|
80
|
+
}
|
|
81
|
+
// The first base a cut queries: the start of the chunk `lookback` chunks
|
|
82
|
+
// before the one holding the window's start
|
|
83
|
+
export function chunkQueryStart(start, chunk, lookback = 1) {
|
|
84
|
+
return Math.max(0, (Math.floor(start / chunk) - lookback) * chunk);
|
|
85
|
+
}
|
|
86
|
+
// Which walks a cut decodes: the reference's, and those `wanted` names by
|
|
87
|
+
// PanSN prefix at sample or haplotype depth; undefined decodes every one
|
|
88
|
+
export function walkNameFilter(wanted, referenceHaplotype) {
|
|
89
|
+
return wanted === undefined || wanted.length === 0
|
|
90
|
+
? undefined
|
|
91
|
+
: (name) => panSNMatchesPrefix(name, referenceHaplotype) ||
|
|
92
|
+
wanted.some(prefix => panSNMatchesPrefix(name, prefix));
|
|
93
|
+
}
|
|
94
|
+
export function decodeSteps(enc, n, ids = new Int32Array(n), rev = new Uint8Array(n), at = 0) {
|
|
95
|
+
let prev = 0;
|
|
96
|
+
let i = at;
|
|
97
|
+
let v = 0;
|
|
98
|
+
let neg = false;
|
|
99
|
+
for (let p = 0; p <= enc.length; p++) {
|
|
100
|
+
const ch = p < enc.length ? enc.charCodeAt(p) : 44;
|
|
101
|
+
if (ch === 44) {
|
|
102
|
+
const d = neg ? -v : v;
|
|
103
|
+
const r = d & 1;
|
|
104
|
+
rev[i] = r;
|
|
105
|
+
prev += (d - r) / 2;
|
|
106
|
+
ids[i++] = prev;
|
|
107
|
+
v = 0;
|
|
108
|
+
neg = false;
|
|
109
|
+
}
|
|
110
|
+
else if (ch === 45) {
|
|
111
|
+
neg = true;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
v = v * 10 + (ch - 48);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { ids, rev };
|
|
118
|
+
}
|
|
119
|
+
export function joinPieces(rows) {
|
|
120
|
+
const byPath = new Map();
|
|
121
|
+
for (const row of rows) {
|
|
122
|
+
const key = `${row.name}\t${row.fragStart}`;
|
|
123
|
+
const list = byPath.get(key);
|
|
124
|
+
if (list) {
|
|
125
|
+
list.push(row);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
byPath.set(key, [row]);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const runs = [];
|
|
132
|
+
for (const pieces of byPath.values()) {
|
|
133
|
+
pieces.sort((a, b) => a.piece - b.piece);
|
|
134
|
+
let from = 0;
|
|
135
|
+
for (let i = 1; i <= pieces.length; i++) {
|
|
136
|
+
if (i === pieces.length ||
|
|
137
|
+
pieces[i].piece !== pieces[i - 1].piece + 1) {
|
|
138
|
+
runs.push(decodeRun(pieces.slice(from, i)));
|
|
139
|
+
from = i;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return runs;
|
|
144
|
+
}
|
|
145
|
+
function decodeRun(pieces) {
|
|
146
|
+
const n = pieces.reduce((sum, p) => sum + p.n, 0);
|
|
147
|
+
const ids = new Int32Array(n);
|
|
148
|
+
const rev = new Uint8Array(n);
|
|
149
|
+
let at = 0;
|
|
150
|
+
for (const p of pieces) {
|
|
151
|
+
if (p.steps) {
|
|
152
|
+
ids.set(p.steps.ids, at);
|
|
153
|
+
rev.set(p.steps.rev, at);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
decodeSteps(p.enc, p.n, ids, rev, at);
|
|
157
|
+
}
|
|
158
|
+
at += p.n;
|
|
159
|
+
}
|
|
160
|
+
const first = pieces[0];
|
|
161
|
+
return {
|
|
162
|
+
name: first.name,
|
|
163
|
+
hapOffset: first.hapOffset,
|
|
164
|
+
ids,
|
|
165
|
+
rev,
|
|
166
|
+
prev: first.prev,
|
|
167
|
+
next: pieces.at(-1).next,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The cut a window draws from its walks: the reference inside the window plus
|
|
172
|
+
* its context, and each run from its first to its last step on that stretch.
|
|
173
|
+
* A run that leaves the reference there, through steps off it or across a
|
|
174
|
+
* deletion, is followed to the reference step where it rejoins, when the read
|
|
175
|
+
* holds it, and otherwise through the steps off the reference it holds: at
|
|
176
|
+
* the ABCA7 VNTR four haplotypes leave inside the repeat and rejoin 1.8 kb
|
|
177
|
+
* past the window, and cut at the context they measured as partial walks. The reference is cut out as far as those rejoins reach, and
|
|
178
|
+
* a run then runs on outward while the next step's node is already in the
|
|
179
|
+
* cut. Without the run-on a haplotype whose alternate allele straddles the
|
|
180
|
+
* edge stopped short of a node other walks had brought into the cut, and that
|
|
181
|
+
* node drew without it. The run-on adds no node, so one pass is enough, but
|
|
182
|
+
* it reaches through nodes other walks brought in, so with a window edge inside
|
|
183
|
+
* a collapsed array a row's length depends on which haplotypes are cut.
|
|
184
|
+
*/
|
|
185
|
+
export function walkCut(runs, nodes, refName) {
|
|
186
|
+
const onWindow = (id) => { var _a; return ((_a = nodes.get(id)) === null || _a === void 0 ? void 0 : _a.onWindow) === true; };
|
|
187
|
+
const reference = (id) => {
|
|
188
|
+
const node = nodes.get(id);
|
|
189
|
+
return (node === null || node === void 0 ? void 0 : node.rank) === 0 ? node : undefined;
|
|
190
|
+
};
|
|
191
|
+
// the chunks walks cut off at the end of a run go on in
|
|
192
|
+
const open = new Map();
|
|
193
|
+
const goesOn = (chunk) => {
|
|
194
|
+
if (chunk) {
|
|
195
|
+
open.set(`${chunk.refName}:${chunk.start}`, chunk);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
// Where a walk stops being followed from step `at`, going by `dir`: the
|
|
199
|
+
// reference step it rejoins at past any steps off the reference, or across
|
|
200
|
+
// a deletion; else its last step off the reference before the run ends,
|
|
201
|
+
// noting the chunk the path goes on in, or reaches another reference
|
|
202
|
+
// sequence
|
|
203
|
+
const rejoin = (run, at, dir) => {
|
|
204
|
+
const { ids } = run;
|
|
205
|
+
let i = at + dir;
|
|
206
|
+
while (i >= 0 && i < ids.length && reference(ids[i]) === undefined) {
|
|
207
|
+
i += dir;
|
|
208
|
+
}
|
|
209
|
+
if (i < 0 || i >= ids.length) {
|
|
210
|
+
goesOn(dir < 0 ? run.prev : run.next);
|
|
211
|
+
return i - dir;
|
|
212
|
+
}
|
|
213
|
+
const from = reference(ids[at]);
|
|
214
|
+
const to = reference(ids[i]);
|
|
215
|
+
if (to.refName !== refName) {
|
|
216
|
+
return i - dir;
|
|
217
|
+
}
|
|
218
|
+
const adjacent = i === at + dir && (to.start === from.end || to.end === from.start);
|
|
219
|
+
return adjacent ? at : i;
|
|
220
|
+
};
|
|
221
|
+
let reachStart = Infinity;
|
|
222
|
+
let reachEnd = -Infinity;
|
|
223
|
+
const spans = [];
|
|
224
|
+
for (const run of runs) {
|
|
225
|
+
const { ids } = run;
|
|
226
|
+
let first = -1;
|
|
227
|
+
let last = -1;
|
|
228
|
+
for (let i = 0; i < ids.length; i++) {
|
|
229
|
+
if (onWindow(ids[i])) {
|
|
230
|
+
if (first < 0) {
|
|
231
|
+
first = i;
|
|
232
|
+
}
|
|
233
|
+
last = i;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (first >= 0) {
|
|
237
|
+
if (run.name !== refName) {
|
|
238
|
+
first = rejoin(run, first, -1);
|
|
239
|
+
last = rejoin(run, last, 1);
|
|
240
|
+
for (const i of [first, last]) {
|
|
241
|
+
const node = reference(ids[i]);
|
|
242
|
+
if (node) {
|
|
243
|
+
reachStart = Math.min(reachStart, node.start);
|
|
244
|
+
reachEnd = Math.max(reachEnd, node.end);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
spans.push({ run, first, last });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const inReach = (id) => {
|
|
252
|
+
const node = reference(id);
|
|
253
|
+
return (onWindow(id) ||
|
|
254
|
+
((node === null || node === void 0 ? void 0 : node.refName) === refName &&
|
|
255
|
+
node.end > reachStart &&
|
|
256
|
+
node.start < reachEnd));
|
|
257
|
+
};
|
|
258
|
+
const kept = new Set();
|
|
259
|
+
for (const [id, node] of nodes) {
|
|
260
|
+
if (node.onWindow) {
|
|
261
|
+
kept.add(id);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
for (const span of spans) {
|
|
265
|
+
const { ids } = span.run;
|
|
266
|
+
if (span.run.name === refName) {
|
|
267
|
+
while (span.first > 0 && inReach(ids[span.first - 1])) {
|
|
268
|
+
span.first--;
|
|
269
|
+
}
|
|
270
|
+
while (span.last < ids.length - 1 && inReach(ids[span.last + 1])) {
|
|
271
|
+
span.last++;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
for (let i = span.first; i <= span.last; i++) {
|
|
275
|
+
kept.add(ids[i]);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const length = (id) => {
|
|
279
|
+
const node = nodes.get(id);
|
|
280
|
+
return node === undefined ? 0 : node.end - node.start;
|
|
281
|
+
};
|
|
282
|
+
const fragments = [];
|
|
283
|
+
for (const { run, first: f, last: l } of spans) {
|
|
284
|
+
const { ids, rev } = run;
|
|
285
|
+
let first = f;
|
|
286
|
+
let last = l;
|
|
287
|
+
while (first > 0 && kept.has(ids[first - 1])) {
|
|
288
|
+
first--;
|
|
289
|
+
}
|
|
290
|
+
while (last < ids.length - 1 && kept.has(ids[last + 1])) {
|
|
291
|
+
last++;
|
|
292
|
+
}
|
|
293
|
+
let hapStart = run.hapOffset;
|
|
294
|
+
for (let i = 0; i < first; i++) {
|
|
295
|
+
hapStart += length(ids[i]);
|
|
296
|
+
}
|
|
297
|
+
let hapEnd = hapStart;
|
|
298
|
+
for (let i = first; i <= last; i++) {
|
|
299
|
+
hapEnd += length(ids[i]);
|
|
300
|
+
}
|
|
301
|
+
fragments.push({
|
|
302
|
+
name: run.name,
|
|
303
|
+
hapStart,
|
|
304
|
+
hapEnd,
|
|
305
|
+
ids: ids.subarray(first, last + 1),
|
|
306
|
+
rev: rev.subarray(first, last + 1),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
return { kept, fragments, open: [...open.values()] };
|
|
310
|
+
}
|
|
311
|
+
// The positions of a line's first `n` tabs, -1 past the last
|
|
312
|
+
function tabs(line, n, out) {
|
|
313
|
+
let at = -1;
|
|
314
|
+
for (let k = 0; k < n; k++) {
|
|
315
|
+
at = at === -1 && k > 0 ? -1 : line.indexOf('\t', at + 1);
|
|
316
|
+
out[k] = at;
|
|
317
|
+
}
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* The node and link rows a walk cut reads, parsed as they arrive, and the
|
|
322
|
+
* tables of the cut they make with its walks. A node filed under two chunks
|
|
323
|
+
* arrives twice and is read once; columns past a node row's eighth (LN:i:,
|
|
324
|
+
* SQ:Z: and whatever follows) are read only for the bases a window asks for.
|
|
325
|
+
*/
|
|
326
|
+
export class WalkGraph {
|
|
327
|
+
constructor(window) {
|
|
328
|
+
this.nodes = new Map();
|
|
329
|
+
this.sequences = new Map();
|
|
330
|
+
this.links = [];
|
|
331
|
+
// format 2's chunks, whose nodes off R and links tables() fills in
|
|
332
|
+
this.format2 = [];
|
|
333
|
+
this.t = [];
|
|
334
|
+
this.window = window;
|
|
335
|
+
}
|
|
336
|
+
addNode(line) {
|
|
337
|
+
const t = tabs(line, 8, this.t);
|
|
338
|
+
const id = +line.slice(t[2] + 1, t[3]);
|
|
339
|
+
if (!this.nodes.has(id)) {
|
|
340
|
+
if (this.window.sequences) {
|
|
341
|
+
const at = line.indexOf('\tSQ:Z:');
|
|
342
|
+
if (at >= 0) {
|
|
343
|
+
const end = line.indexOf('\t', at + 1);
|
|
344
|
+
this.sequences.set(id, line.slice(at + 6, end < 0 ? undefined : end));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
this.placeNode(id, {
|
|
348
|
+
rank: +line.slice(t[3] + 1, t[4]),
|
|
349
|
+
refName: line.slice(t[4] + 1, t[5]),
|
|
350
|
+
start: +line.slice(t[5] + 1, t[6]),
|
|
351
|
+
end: +line.slice(t[6] + 1, t[7] === -1 ? undefined : t[7]),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// A node with its position, unless the cut already holds it
|
|
356
|
+
placeNode(id, node) {
|
|
357
|
+
if (this.nodes.has(id)) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const { window } = this;
|
|
361
|
+
this.nodes.set(id, {
|
|
362
|
+
...node,
|
|
363
|
+
onWindow: node.rank === 0 &&
|
|
364
|
+
node.refName === window.refName &&
|
|
365
|
+
node.end > window.start - window.context &&
|
|
366
|
+
node.start < window.end + window.context,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
// A link between two endpoints, each id * 2 + 1 when reversed
|
|
370
|
+
addLinkEnds(source, target) {
|
|
371
|
+
this.links.push(source, target);
|
|
372
|
+
}
|
|
373
|
+
addFormat2Chunk(chunk) {
|
|
374
|
+
this.format2.push(chunk);
|
|
375
|
+
}
|
|
376
|
+
// An endpoint is held as id * 2 + 1 when reversed
|
|
377
|
+
addLink(line) {
|
|
378
|
+
const t = tabs(line, 5, this.t);
|
|
379
|
+
this.links.push(endpoint(line, t[2] + 1, t[3]), endpoint(line, t[3] + 1, t[4] === -1 ? line.length : t[4]));
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* The cut as typed arrays, which the worker transfers rather than copies:
|
|
383
|
+
* nodes by reference position, each link once in the order its L line
|
|
384
|
+
* sorts, then a walk per fragment, the reference's first and the rest by
|
|
385
|
+
* name. graphTablesGFA writes them as the GFA they stand for, the same bytes
|
|
386
|
+
* for the same rows.
|
|
387
|
+
*/
|
|
388
|
+
tables(kept, fragments) {
|
|
389
|
+
if (this.format2.length > 0) {
|
|
390
|
+
placeAndLink(this.format2, kept, (id, node) => {
|
|
391
|
+
const held = this.nodes.get(id);
|
|
392
|
+
if (held) {
|
|
393
|
+
this.nodes.set(id, { ...held, ...node });
|
|
394
|
+
}
|
|
395
|
+
}, (a, b) => {
|
|
396
|
+
this.links.push(a, b);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
const held = [];
|
|
400
|
+
for (const id of kept) {
|
|
401
|
+
const node = this.nodes.get(id);
|
|
402
|
+
if (node) {
|
|
403
|
+
held.push({ ...node, id });
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
held.sort((x, y) => x.refName < y.refName
|
|
407
|
+
? -1
|
|
408
|
+
: x.refName > y.refName
|
|
409
|
+
? 1
|
|
410
|
+
: x.start - y.start || x.id - y.id);
|
|
411
|
+
const index = new Map();
|
|
412
|
+
const names = [];
|
|
413
|
+
const refNames = [];
|
|
414
|
+
const refIndex = new Map();
|
|
415
|
+
const lengths = new Int32Array(held.length);
|
|
416
|
+
const refs = new Int32Array(held.length);
|
|
417
|
+
const starts = new Float64Array(held.length);
|
|
418
|
+
const ranks = new Int32Array(held.length);
|
|
419
|
+
held.forEach(({ id, refName, start, end, rank }, i) => {
|
|
420
|
+
index.set(id, i);
|
|
421
|
+
names.push(String(id));
|
|
422
|
+
let ref = refIndex.get(refName);
|
|
423
|
+
if (ref === undefined) {
|
|
424
|
+
ref = refNames.length;
|
|
425
|
+
refIndex.set(refName, ref);
|
|
426
|
+
refNames.push(refName);
|
|
427
|
+
}
|
|
428
|
+
lengths[i] = end - start;
|
|
429
|
+
refs[i] = ref;
|
|
430
|
+
starts[i] = start;
|
|
431
|
+
ranks[i] = rank;
|
|
432
|
+
});
|
|
433
|
+
// a kept node with no row of its own is named, and not drawn
|
|
434
|
+
const nodeIndex = (id) => {
|
|
435
|
+
let i = index.get(id);
|
|
436
|
+
if (i === undefined) {
|
|
437
|
+
i = names.length;
|
|
438
|
+
index.set(id, i);
|
|
439
|
+
names.push(String(id));
|
|
440
|
+
}
|
|
441
|
+
return i;
|
|
442
|
+
};
|
|
443
|
+
const byLine = new Map();
|
|
444
|
+
for (let i = 0; i < this.links.length; i += 2) {
|
|
445
|
+
const a = this.links[i];
|
|
446
|
+
const b = this.links[i + 1];
|
|
447
|
+
const source = Math.floor(a / 2);
|
|
448
|
+
const target = Math.floor(b / 2);
|
|
449
|
+
if (kept.has(source) && kept.has(target)) {
|
|
450
|
+
byLine.set(`${source}\t${a % 2 ? '-' : '+'}\t${target}\t${b % 2 ? '-' : '+'}`, i);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const lines = [...byLine.keys()].sort();
|
|
454
|
+
const from = new Int32Array(lines.length);
|
|
455
|
+
const to = new Int32Array(lines.length);
|
|
456
|
+
const strands = new Uint8Array(lines.length);
|
|
457
|
+
lines.forEach((line, k) => {
|
|
458
|
+
const i = byLine.get(line);
|
|
459
|
+
const a = this.links[i];
|
|
460
|
+
const b = this.links[i + 1];
|
|
461
|
+
from[k] = nodeIndex(Math.floor(a / 2));
|
|
462
|
+
to[k] = nodeIndex(Math.floor(b / 2));
|
|
463
|
+
strands[k] = (a % 2) | ((b % 2) << 1);
|
|
464
|
+
});
|
|
465
|
+
const walks = this.referenceFirst(fragments);
|
|
466
|
+
const offsets = new Int32Array(walks.length + 1);
|
|
467
|
+
walks.forEach(({ ids }, p) => {
|
|
468
|
+
offsets[p + 1] = offsets[p] + ids.length;
|
|
469
|
+
});
|
|
470
|
+
const steps = new Int32Array(offsets[walks.length]);
|
|
471
|
+
const reversed = new Uint8Array(steps.length);
|
|
472
|
+
walks.forEach(({ ids, rev }, p) => {
|
|
473
|
+
const at = offsets[p];
|
|
474
|
+
for (let j = 0; j < ids.length; j++) {
|
|
475
|
+
steps[at + j] = nodeIndex(ids[j]);
|
|
476
|
+
}
|
|
477
|
+
reversed.set(rev, at);
|
|
478
|
+
});
|
|
479
|
+
return {
|
|
480
|
+
nodes: { names, lengths, refs, starts, ranks, refNames },
|
|
481
|
+
links: { from, to, strands },
|
|
482
|
+
walks: {
|
|
483
|
+
names: walks.map(f => f.name),
|
|
484
|
+
starts: Float64Array.from(walks, f => f.hapStart),
|
|
485
|
+
ends: Float64Array.from(walks, f => f.hapEnd),
|
|
486
|
+
offsets,
|
|
487
|
+
steps,
|
|
488
|
+
reversed,
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
// The view anchors on the first walk when no walk names the assembly, as
|
|
493
|
+
// gbz-base's cut has it; in file order that was whichever haplotype the
|
|
494
|
+
// chunk listed first, CHM13 on a GRCh38 window
|
|
495
|
+
referenceFirst(fragments) {
|
|
496
|
+
const { refName } = this.window;
|
|
497
|
+
return [...fragments].sort((a, b) => Number(b.name === refName) - Number(a.name === refName) ||
|
|
498
|
+
(a.name < b.name ? -1 : a.name > b.name ? 1 : a.hapStart - b.hapStart));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function endpoint(line, from, to) {
|
|
502
|
+
return (+line.slice(from, to - 1) * 2 + (line.charCodeAt(to - 1) === 45 ? 1 : 0));
|
|
503
|
+
}
|
|
504
|
+
export const BUDGET_NAMES = {
|
|
505
|
+
steps: 'the step budget',
|
|
506
|
+
bytes: 'a byte budget of',
|
|
507
|
+
};
|
|
508
|
+
export function stepBudgetError(rows, stepBudget, readPerKept, window, chunk, filtered, names = BUDGET_NAMES) {
|
|
509
|
+
var _a;
|
|
510
|
+
let total = 0;
|
|
511
|
+
const perChunk = new Map();
|
|
512
|
+
const walks = new Set();
|
|
513
|
+
for (const row of rows) {
|
|
514
|
+
total += row.n;
|
|
515
|
+
perChunk.set(row.chunkStart, ((_a = perChunk.get(row.chunkStart)) !== null && _a !== void 0 ? _a : 0) + row.n);
|
|
516
|
+
walks.add(row.name);
|
|
517
|
+
}
|
|
518
|
+
const budget = stepBudget * readPerKept;
|
|
519
|
+
if (total <= budget) {
|
|
520
|
+
return undefined;
|
|
521
|
+
}
|
|
522
|
+
const windowBp = window.end - window.start;
|
|
523
|
+
let read = 0;
|
|
524
|
+
let fitsBp = 0;
|
|
525
|
+
for (const cs of [...perChunk.keys()].sort((a, b) => a - b)) {
|
|
526
|
+
read += perChunk.get(cs);
|
|
527
|
+
if (read > budget) {
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
fitsBp = Math.max(fitsBp, cs + chunk - window.start);
|
|
531
|
+
}
|
|
532
|
+
const fewerFit = !filtered && walks.size > 2 && (2 * total) / walks.size <= budget;
|
|
533
|
+
return new RegionTooLargeError(fitsBp > 0
|
|
534
|
+
? `Zoom in to about ${bpString(fitsBp)}${fewerFit ? ', or choose fewer haplotypes,' : ''} to see the graph`
|
|
535
|
+
: `Too many haplotype steps here to draw ${filtered ? 'these haplotypes' : 'every haplotype'} (${read.toLocaleString()} steps read, over ${readPerKept} × ${names.steps} of ${stepBudget.toLocaleString()}); choose fewer haplotypes`, budget, windowBp, Math.max(fitsBp, 1));
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* The zoom-in notice for a cut that keeps more steps than `budget`, or
|
|
539
|
+
* undefined when it fits. The span that fits is the window scaled by how far
|
|
540
|
+
* over the cut is.
|
|
541
|
+
*/
|
|
542
|
+
export function keptStepsError(fragments, budget, window, filtered) {
|
|
543
|
+
const total = fragments.reduce((sum, f) => sum + f.ids.length, 0);
|
|
544
|
+
if (total <= budget) {
|
|
545
|
+
return undefined;
|
|
546
|
+
}
|
|
547
|
+
const windowBp = window.end - window.start;
|
|
548
|
+
const fitsBp = Math.max(1, Math.floor((windowBp * budget) / total));
|
|
549
|
+
return new RegionTooLargeError(`Zoom in to about ${bpString(fitsBp)}${filtered ? '' : ', or choose fewer haplotypes,'} to see the graph`, budget, windowBp, fitsBp);
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* The zoom-in notice for a cut whose three files would fetch more than
|
|
553
|
+
* `budget` compressed bytes, or undefined when they fit. `bytesTo(end)` is the
|
|
554
|
+
* indexes' estimate for a read from the window's first query base to `end`;
|
|
555
|
+
* the span that fits grows a chunk at a time from the chunk holding the
|
|
556
|
+
* window's start, and when that chunk and those before it are over on their
|
|
557
|
+
* own, no zoom fits.
|
|
558
|
+
*/
|
|
559
|
+
export async function byteBudgetError(bytesTo, budget, window, chunk, names = BUDGET_NAMES) {
|
|
560
|
+
const bytes = await bytesTo(window.end);
|
|
561
|
+
if (bytes <= budget) {
|
|
562
|
+
return undefined;
|
|
563
|
+
}
|
|
564
|
+
let fitsBp = 0;
|
|
565
|
+
let least;
|
|
566
|
+
for (let cs = Math.floor(window.start / chunk) * chunk; cs < window.end; cs += chunk) {
|
|
567
|
+
const read = await bytesTo(cs + 1);
|
|
568
|
+
least !== null && least !== void 0 ? least : (least = read);
|
|
569
|
+
if (read > budget) {
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
fitsBp = cs + chunk - window.start;
|
|
573
|
+
}
|
|
574
|
+
const windowBp = window.end - window.start;
|
|
575
|
+
return new RegionTooLargeError(fitsBp > 0
|
|
576
|
+
? `Zoom in to about ${bpString(fitsBp)} to see the graph`
|
|
577
|
+
: `Too much graph here to fetch (${megabytes(least !== null && least !== void 0 ? least : bytes)} against ${names.bytes} ${megabytes(budget)})`, budget, windowBp, Math.max(fitsBp, 1));
|
|
578
|
+
}
|
|
579
|
+
const megabytes = (bytes) => `${(bytes / 1e6).toFixed(1)} MB`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jbrowse/graph-walks",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Read gfa-to-tabix walk files by range requests and cut a window of a pangenome graph: the reference, every haplotype's walk through it, and the nodes and links they visit",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Colin Diesh",
|
|
8
|
+
"email": "colin.diesh@gmail.com",
|
|
9
|
+
"url": "https://github.com/cmdcolin"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/GMOD/graph-walks.git"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"pangenome",
|
|
17
|
+
"graph",
|
|
18
|
+
"gfa",
|
|
19
|
+
"tabix",
|
|
20
|
+
"haplotype"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"packageManager": "pnpm@11.20.0",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./src/index.ts"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./esm/index.d.ts",
|
|
32
|
+
"default": "./esm/index.js"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"esm"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "rm -rf esm && tsc -p tsconfig.build.json",
|
|
41
|
+
"typecheck": "tsc --noEmit",
|
|
42
|
+
"lint": "eslint --report-unused-disable-directives --max-warnings 0",
|
|
43
|
+
"format": "prettier --write .",
|
|
44
|
+
"format:check": "prettier --check .",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"prepack": "pnpm build",
|
|
47
|
+
"preversion": "pnpm lint && pnpm format:check && pnpm typecheck && pnpm test && pnpm build",
|
|
48
|
+
"postversion": "git push --follow-tags"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=20"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@eslint/js": "^10.0.1",
|
|
55
|
+
"@gmod/tabix": "^3.10.1",
|
|
56
|
+
"eslint": "^10.12.0",
|
|
57
|
+
"eslint-plugin-import-x": "^4.17.1",
|
|
58
|
+
"generic-filehandle2": "^2.5.0",
|
|
59
|
+
"prettier": "^3.9.10",
|
|
60
|
+
"typescript": "^6.0.3",
|
|
61
|
+
"typescript-eslint": "^8.71.1",
|
|
62
|
+
"vitest": "^5.0.3"
|
|
63
|
+
}
|
|
64
|
+
}
|