@jbrowse/graph-walks 0.1.0 → 1.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/README.md +31 -19
- package/esm/index.d.ts +6 -5
- package/esm/index.js +5 -4
- package/esm/refNames.d.ts +12 -0
- package/esm/refNames.js +35 -0
- package/esm/util.d.ts +2 -0
- package/esm/util.js +13 -0
- package/esm/{walkFormat2.d.ts → walkChunk.d.ts} +5 -4
- package/esm/{walkFormat2.js → walkChunk.js} +25 -5
- package/esm/walkReader.d.ts +25 -13
- package/esm/walkReader.js +81 -92
- package/esm/walkRows.d.ts +17 -23
- package/esm/walkRows.js +31 -121
- package/package.json +18 -22
package/README.md
CHANGED
|
@@ -4,11 +4,13 @@ Cuts a window out of a pangenome graph stored as
|
|
|
4
4
|
[gfa-to-tabix](https://github.com/GMOD/gfa-to-tabix) walk files, by range
|
|
5
5
|
requests.
|
|
6
6
|
|
|
7
|
-
- **In:** a `gfa-to-tabix --walks` file
|
|
7
|
+
- **In:** a `gfa-to-tabix --walks` file, `<prefix>.<sample>.walks.bed.gz` with
|
|
8
|
+
its `.tbi`, and a region on its reference
|
|
8
9
|
- **Out:** the reference over the region, every haplotype's walk through it, and
|
|
9
10
|
the nodes and links they visit, as typed arrays
|
|
10
|
-
- Reads format
|
|
11
|
-
format
|
|
11
|
+
- Reads walk format 2, which gfa-to-tabix writes from 1.0. The reader refuses a
|
|
12
|
+
format 1 file, from gfa-to-tabix 0.12 or earlier, with a `WalkFormatError`
|
|
13
|
+
asking for a rebuild
|
|
12
14
|
- Has no runtime dependencies: it reads through any object with
|
|
13
15
|
[@gmod/tabix](https://github.com/GMOD/tabix-js)'s `TabixIndexedFile` methods
|
|
14
16
|
|
|
@@ -25,29 +27,22 @@ npm install @jbrowse/graph-walks @gmod/tabix
|
|
|
25
27
|
|
|
26
28
|
```ts
|
|
27
29
|
import { TabixIndexedFile } from '@gmod/tabix'
|
|
28
|
-
import {
|
|
29
|
-
WalkReader,
|
|
30
|
-
panSNHaplotype,
|
|
31
|
-
walkNameFilter,
|
|
32
|
-
} from '@jbrowse/graph-walks'
|
|
30
|
+
import { WalkReader, walksKept } from '@jbrowse/graph-walks'
|
|
33
31
|
import { RemoteFile } from 'generic-filehandle2'
|
|
34
32
|
|
|
35
|
-
const
|
|
33
|
+
const url = 'https://example.org/hprc-v2.1-mc-grch38.GRCh38.walks.bed.gz'
|
|
34
|
+
const reader = new WalkReader(
|
|
36
35
|
new TabixIndexedFile({
|
|
37
36
|
filehandle: new RemoteFile(url),
|
|
38
37
|
tbiFilehandle: new RemoteFile(`${url}.tbi`),
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
const reader = new WalkReader({
|
|
42
|
-
walks: file(`${prefix}.walks.bed.gz`),
|
|
43
|
-
nodes: file(`${prefix}.nodes.bed.gz`),
|
|
44
|
-
})
|
|
38
|
+
}),
|
|
39
|
+
)
|
|
45
40
|
|
|
46
41
|
const refName = 'GRCh38#0#chr6'
|
|
47
42
|
const { graph, kept, fragments } = await reader.cut(
|
|
48
43
|
refName,
|
|
49
44
|
{ start: 160_614_798, end: 160_647_758 },
|
|
50
|
-
{ keep:
|
|
45
|
+
{ keep: walksKept(['HG00097', 'HG00133'], refName) },
|
|
51
46
|
)
|
|
52
47
|
const tables = graph.tables(kept, fragments)
|
|
53
48
|
```
|
|
@@ -68,18 +63,35 @@ const tables = graph.tables(kept, fragments)
|
|
|
68
63
|
| `context` | bp of reference kept past each end of the region; default 1000 |
|
|
69
64
|
| `stepBudget` | The most steps a cut keeps; no limit by default |
|
|
70
65
|
| `byteBudget` | The most compressed bytes a cut fetches; no limit by default |
|
|
71
|
-
| `links` | `false`
|
|
72
|
-
| `sequences` | Keep each node's bases in `graph.sequences`, from `--sequences`
|
|
66
|
+
| `links` | `false` lists no links, for a caller that draws no edges |
|
|
67
|
+
| `sequences` | Keep each node's bases in `graph.sequences`, from a `--sequences` file |
|
|
73
68
|
| `signal` | An `AbortSignal` that stops the reads |
|
|
74
69
|
|
|
75
70
|
A cut over a budget throws a `RegionTooLargeError`. Its `fitsBp` holds about how
|
|
76
71
|
wide a window would fit, and JBrowse shows it as a zoom-in notice.
|
|
72
|
+
`WALK_BUDGETS` holds the step and byte budgets the JBrowse plugin and BandageJS
|
|
73
|
+
cut by.
|
|
74
|
+
|
|
75
|
+
## Naming the window
|
|
76
|
+
|
|
77
|
+
- `reader.sequenceFor(sample, refName)` gives the walk file's sequence for a
|
|
78
|
+
contig on a sample: `chr6` on `GRCh38` is `GRCh38#0#chr6`, and a pggb file's
|
|
79
|
+
`chm13#chr1` is `chr1` on `chm13`
|
|
80
|
+
- `walksKept(haplotypes, refName)` is the `keep` filter for a cut on `refName`:
|
|
81
|
+
its own walk and those of the haplotypes named
|
|
82
|
+
- `refNameLookup` and `resolveRefName` resolve a contig the same way against any
|
|
83
|
+
tabix file's sequence names
|
|
77
84
|
|
|
78
85
|
## How a cut works
|
|
79
86
|
|
|
87
|
+
- A chunk holds the reference's path through it once, each way a haplotype
|
|
88
|
+
leaves that path once, and every other path's piece as the list of ways it
|
|
89
|
+
takes; gfa-to-tabix's
|
|
90
|
+
[walks-format.md](https://github.com/GMOD/gfa-to-tabix/blob/main/docs/walks-format.md)
|
|
91
|
+
describes the rows
|
|
80
92
|
- A cut reads every chunk under the region, and the chunks before it as far as
|
|
81
93
|
the header's `maxnode` reaches, since a node is filed under the chunk holding
|
|
82
|
-
its start
|
|
94
|
+
its start, and decodes each chunk once
|
|
83
95
|
- A haplotype that leaves the reference near the region is followed to where it
|
|
84
96
|
rejoins, and the reference is cut out as far as those rejoins reach
|
|
85
97
|
- A walk the read cuts off goes on in the chunk its row names (`pv:`/`nx:`
|
package/esm/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export { BUDGET_NAMES, WalkGraph, byteBudgetError, chunkQueryStart,
|
|
1
|
+
export { BUDGET_NAMES, WalkGraph, byteBudgetError, chunkQueryStart, joinPieces, keptStepsError, lookbackChunks, stepBudgetError, walkCut, walkHeader, walkNameFilter, walksKept, } from './walkRows.ts';
|
|
2
2
|
export type { BudgetNames, WalkChunk, WalkFragment, WalkHeader, WalkNode, WalkRow, WalkRun, WalkWindow, } from './walkRows.ts';
|
|
3
|
-
export { WalkReader } from './walkReader.ts';
|
|
4
|
-
export {
|
|
5
|
-
export
|
|
6
|
-
export {
|
|
3
|
+
export { WALK_BUDGETS, WalkFormatError, WalkReader, checkRange, } from './walkReader.ts';
|
|
4
|
+
export { refNameLookup, resolveRefName } from './refNames.ts';
|
|
5
|
+
export { referenceNodes } from './walkChunk.ts';
|
|
6
|
+
export type { TabixSource, WalkCutOptions, WalkReaderHeader, } from './walkReader.ts';
|
|
7
|
+
export { RegionTooLargeError, bpString, panSNContig, panSNHaplotype, panSNMatchesPrefix, panSNSample, } from './util.ts';
|
|
7
8
|
export type { GraphTables } from './util.ts';
|
package/esm/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { BUDGET_NAMES, WalkGraph, byteBudgetError, chunkQueryStart,
|
|
2
|
-
export { WalkReader } from "./walkReader.js";
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
1
|
+
export { BUDGET_NAMES, WalkGraph, byteBudgetError, chunkQueryStart, joinPieces, keptStepsError, lookbackChunks, stepBudgetError, walkCut, walkHeader, walkNameFilter, walksKept, } from "./walkRows.js";
|
|
2
|
+
export { WALK_BUDGETS, WalkFormatError, WalkReader, checkRange, } from "./walkReader.js";
|
|
3
|
+
export { refNameLookup, resolveRefName } from "./refNames.js";
|
|
4
|
+
export { referenceNodes } from "./walkChunk.js";
|
|
5
|
+
export { RegionTooLargeError, bpString, panSNContig, panSNHaplotype, panSNMatchesPrefix, panSNSample, } from "./util.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A file's sequence names under each spelling a caller may ask by: the file's
|
|
3
|
+
* own (`GRCh38#0#chr6`), and a PanSN name's contig under its sample (`chr6`
|
|
4
|
+
* on `GRCh38`) and under its haplotype (`chr6` on `GRCh38#0`). A pggb name
|
|
5
|
+
* with no haplotype field, `chm13#chr1`, is `chr1` on `chm13`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function refNameLookup(names: string[]): Map<string, string>;
|
|
8
|
+
/**
|
|
9
|
+
* The file's name for `refName` on `assemblyName`, a sample or a haplotype,
|
|
10
|
+
* else for `refName` as the file spells it; undefined when it holds neither
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveRefName(lookup: Map<string, string>, assemblyName: string, refName: string): string | undefined;
|
package/esm/refNames.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { panSNContig, panSNHaplotype, panSNSample } from "./util.js";
|
|
2
|
+
// A tab can occur in neither half: both come out of BED columns
|
|
3
|
+
function qualifiedKey(assemblyName, refName) {
|
|
4
|
+
return `${assemblyName}\t${refName}`;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* A file's sequence names under each spelling a caller may ask by: the file's
|
|
8
|
+
* own (`GRCh38#0#chr6`), and a PanSN name's contig under its sample (`chr6`
|
|
9
|
+
* on `GRCh38`) and under its haplotype (`chr6` on `GRCh38#0`). A pggb name
|
|
10
|
+
* with no haplotype field, `chm13#chr1`, is `chr1` on `chm13`.
|
|
11
|
+
*/
|
|
12
|
+
export function refNameLookup(names) {
|
|
13
|
+
const lookup = new Map();
|
|
14
|
+
for (const name of names) {
|
|
15
|
+
lookup.set(name, name);
|
|
16
|
+
const sample = panSNSample(name);
|
|
17
|
+
if (sample !== name) {
|
|
18
|
+
const contig = panSNContig(name);
|
|
19
|
+
lookup.set(qualifiedKey(sample, contig), name);
|
|
20
|
+
const haplotype = panSNHaplotype(name);
|
|
21
|
+
if (haplotype !== undefined) {
|
|
22
|
+
lookup.set(qualifiedKey(haplotype, contig), name);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return lookup;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The file's name for `refName` on `assemblyName`, a sample or a haplotype,
|
|
30
|
+
* else for `refName` as the file spells it; undefined when it holds neither
|
|
31
|
+
*/
|
|
32
|
+
export function resolveRefName(lookup, assemblyName, refName) {
|
|
33
|
+
var _a;
|
|
34
|
+
return (_a = lookup.get(qualifiedKey(assemblyName, refName))) !== null && _a !== void 0 ? _a : lookup.get(refName);
|
|
35
|
+
}
|
package/esm/util.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export declare class RegionTooLargeError extends Error {
|
|
|
34
34
|
readonly fitsBp: number;
|
|
35
35
|
constructor(message: string, limit: number, windowBp: number, fitsBp: number);
|
|
36
36
|
}
|
|
37
|
+
export declare function panSNSample(refName: string): string;
|
|
38
|
+
export declare function panSNContig(refName: string): string;
|
|
37
39
|
export declare function panSNHaplotype(refName: string): string | undefined;
|
|
38
40
|
export declare function panSNMatchesPrefix(refName: string, prefix: string | undefined): boolean;
|
|
39
41
|
export declare function bpString(bp: number): string;
|
package/esm/util.js
CHANGED
|
@@ -14,6 +14,19 @@ export class RegionTooLargeError extends Error {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
const SEP = '#';
|
|
17
|
+
// `grape#1#chr1` -> `grape`
|
|
18
|
+
export function panSNSample(refName) {
|
|
19
|
+
return refName.split(SEP)[0];
|
|
20
|
+
}
|
|
21
|
+
// `grape#1#chr1` and `grape#chr1` -> `chr1`
|
|
22
|
+
export function panSNContig(refName) {
|
|
23
|
+
const parts = refName.split(SEP);
|
|
24
|
+
return parts.length >= 3
|
|
25
|
+
? parts.slice(2).join(SEP)
|
|
26
|
+
: parts.length === 2
|
|
27
|
+
? parts[1]
|
|
28
|
+
: refName;
|
|
29
|
+
}
|
|
17
30
|
export function panSNHaplotype(refName) {
|
|
18
31
|
const parts = refName.split(SEP);
|
|
19
32
|
return parts.length >= 3 ? `${parts[0]}${SEP}${parts[1]}` : undefined;
|
|
@@ -24,7 +24,7 @@ export declare class RefPath {
|
|
|
24
24
|
}
|
|
25
25
|
export declare class WalkRowError extends Error {
|
|
26
26
|
}
|
|
27
|
-
export interface
|
|
27
|
+
export interface Chunk {
|
|
28
28
|
path: RefPath;
|
|
29
29
|
routes: Route[];
|
|
30
30
|
walks: {
|
|
@@ -34,6 +34,7 @@ export interface Format2Chunk {
|
|
|
34
34
|
lengths: Map<number, number>;
|
|
35
35
|
}
|
|
36
36
|
export interface DecodedChunk {
|
|
37
|
+
sequences: Map<number, string>;
|
|
37
38
|
rows: WalkRow[];
|
|
38
39
|
nodes: Map<number, PlacedNode>;
|
|
39
40
|
ends: {
|
|
@@ -43,13 +44,13 @@ export interface DecodedChunk {
|
|
|
43
44
|
first: number;
|
|
44
45
|
last: number;
|
|
45
46
|
}[];
|
|
46
|
-
chunk:
|
|
47
|
+
chunk: Chunk;
|
|
47
48
|
}
|
|
48
49
|
/**
|
|
49
50
|
* Decodes one chunk's rows, each split on tabs. `keep` names the walk rows
|
|
50
51
|
* whose steps a cut wants, undefined for every one.
|
|
51
52
|
*/
|
|
52
|
-
export declare function decodeChunk(rows: string[][], keep: ((name: string) => boolean) | undefined): DecodedChunk;
|
|
53
|
+
export declare function decodeChunk(rows: string[][], keep: ((name: string) => boolean) | undefined, withSequences?: boolean): DecodedChunk;
|
|
53
54
|
/**
|
|
54
55
|
* The positions of the cut's nodes off R, and the links among the cut's
|
|
55
56
|
* nodes, from the chunks a cut read: `kept` holds the node ids the cut draws.
|
|
@@ -57,7 +58,7 @@ export declare function decodeChunk(rows: string[][], keep: ((name: string) => b
|
|
|
57
58
|
* those visiting it in the chunks read, at its earliest offset, as format 1's
|
|
58
59
|
* node rows had it. Links are R's and the routes', both ends in the cut.
|
|
59
60
|
*/
|
|
60
|
-
export declare function placeAndLink(chunks:
|
|
61
|
+
export declare function placeAndLink(chunks: Chunk[], kept: Set<number>, place: (id: number, node: PlacedNode) => void, link: (a: number, b: number) => void): void;
|
|
61
62
|
export declare function referenceNodes(rows: string[][]): (PlacedNode & {
|
|
62
63
|
id: number;
|
|
63
64
|
})[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
// docs/walks-format.md). A chunk lists its `ref` rows, which joined in
|
|
1
|
+
// A chunk of gfa-to-tabix's walk format 2, one file per reference (that
|
|
2
|
+
// repo's docs/walks-format.md). A chunk lists its `ref` rows, which joined in
|
|
3
3
|
// hapOffset order are the chunk's reference path R; a `nodes` row with the
|
|
4
4
|
// lengths of the nodes its routes visit; `route` rows, each a way off R; and a
|
|
5
5
|
// `walk` row per piece of every other path, naming the routes it takes in
|
|
@@ -286,16 +286,31 @@ function decodeWalk(path, routes, cols, choices, n) {
|
|
|
286
286
|
}
|
|
287
287
|
return { ids, rev };
|
|
288
288
|
}
|
|
289
|
+
// The bases an SQ:Z: tag at or after column `from` joins, one node's per
|
|
290
|
+
// length, as `ids` names them
|
|
291
|
+
function sliceBases(cols, from, ids, lengths, out) {
|
|
292
|
+
const tag = cols.slice(from).find(c => c.startsWith('SQ:Z:'));
|
|
293
|
+
if (tag === undefined) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
let at = 5;
|
|
297
|
+
for (let k = 0; k < ids.length; k++) {
|
|
298
|
+
const length = lengths[k];
|
|
299
|
+
out.set(ids[k], tag.slice(at, at + length));
|
|
300
|
+
at += length;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
289
303
|
/**
|
|
290
304
|
* Decodes one chunk's rows, each split on tabs. `keep` names the walk rows
|
|
291
305
|
* whose steps a cut wants, undefined for every one.
|
|
292
306
|
*/
|
|
293
|
-
export function decodeChunk(rows, keep) {
|
|
307
|
+
export function decodeChunk(rows, keep, withSequences = false) {
|
|
294
308
|
var _a, _b, _c;
|
|
295
309
|
const path = new RefPath();
|
|
296
310
|
const lengths = new Map();
|
|
297
311
|
const routes = [];
|
|
298
312
|
const out = {
|
|
313
|
+
sequences: new Map(),
|
|
299
314
|
rows: [],
|
|
300
315
|
nodes: new Map(),
|
|
301
316
|
ends: [],
|
|
@@ -309,6 +324,9 @@ export function decodeChunk(rows, keep) {
|
|
|
309
324
|
const nodeLengths = numbers(cols[10]);
|
|
310
325
|
const hapOffset = +cols[6];
|
|
311
326
|
path.add(handles, nodeLengths);
|
|
327
|
+
if (withSequences) {
|
|
328
|
+
sliceBases(cols, 11, handles.map(idOf), nodeLengths, out.sequences);
|
|
329
|
+
}
|
|
312
330
|
let start = hapOffset;
|
|
313
331
|
for (let k = 0; k < handles.length; k++) {
|
|
314
332
|
const id = idOf(handles[k]);
|
|
@@ -339,7 +357,6 @@ export function decodeChunk(rows, keep) {
|
|
|
339
357
|
hapOffset,
|
|
340
358
|
piece: +cols[7],
|
|
341
359
|
n: handles.length,
|
|
342
|
-
enc: '',
|
|
343
360
|
steps: {
|
|
344
361
|
ids: Int32Array.from(handles, idOf),
|
|
345
362
|
rev: Uint8Array.from(handles, h => h & 1),
|
|
@@ -355,8 +372,12 @@ export function decodeChunk(rows, keep) {
|
|
|
355
372
|
let id = 0;
|
|
356
373
|
for (let k = 0; k < ids.length; k++) {
|
|
357
374
|
id += ids[k];
|
|
375
|
+
ids[k] = id;
|
|
358
376
|
lengths.set(id, nodeLengths[k]);
|
|
359
377
|
}
|
|
378
|
+
if (withSequences) {
|
|
379
|
+
sliceBases(cols, 6, ids, nodeLengths, out.sequences);
|
|
380
|
+
}
|
|
360
381
|
}
|
|
361
382
|
else if (cols[3] === 'route') {
|
|
362
383
|
const entry = tokens(cols[4], previous, []);
|
|
@@ -423,7 +444,6 @@ export function decodeChunk(rows, keep) {
|
|
|
423
444
|
hapOffset: +cols[6],
|
|
424
445
|
piece: +cols[7],
|
|
425
446
|
n,
|
|
426
|
-
enc: '',
|
|
427
447
|
steps: decodeWalk(path, routes, cols, choices, n),
|
|
428
448
|
...tags(cols, 12, cols[0]),
|
|
429
449
|
});
|
package/esm/walkReader.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { WalkGraph } from './walkRows.ts';
|
|
2
2
|
import type { BudgetNames, WalkHeader } from './walkRows.ts';
|
|
3
|
+
export declare const WALK_BUDGETS: {
|
|
4
|
+
stepBudget: number;
|
|
5
|
+
byteBudget: number;
|
|
6
|
+
};
|
|
3
7
|
/**
|
|
4
8
|
* A Tabix-indexed file as the reader uses one: @gmod/tabix's
|
|
5
9
|
* TabixIndexedFile is one. `bytesForRegions` estimates the compressed bytes a
|
|
@@ -24,11 +28,10 @@ export interface TabixSource {
|
|
|
24
28
|
signal?: AbortSignal;
|
|
25
29
|
}): Promise<number>;
|
|
26
30
|
}
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
nodes?: TabixSource;
|
|
30
|
-
links?: TabixSource;
|
|
31
|
+
export declare class WalkFormatError extends Error {
|
|
32
|
+
name: string;
|
|
31
33
|
}
|
|
34
|
+
export declare function checkRange(refName: string, start: number, end: number): void;
|
|
32
35
|
export interface WalkCutOptions {
|
|
33
36
|
signal?: AbortSignal;
|
|
34
37
|
keep?: (name: string) => boolean;
|
|
@@ -44,24 +47,33 @@ export type WalkReaderHeader = WalkHeader & {
|
|
|
44
47
|
lookback: number;
|
|
45
48
|
};
|
|
46
49
|
/**
|
|
47
|
-
* A
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
50
|
+
* A `gfa-to-tabix --walks` file, `<prefix>.<sample>.walks.bed.gz`, read a
|
|
51
|
+
* window at a time: one read over whole chunks, from the lookback chunks
|
|
52
|
+
* before the window to its end, each chunk decoded once. The index first
|
|
53
|
+
* estimates the bytes the read fetches, and a window past the byte budget
|
|
54
|
+
* reads no row. Only the walks `keep` accepts are decoded. The step budget
|
|
55
|
+
* bounds the steps the cut keeps, and rows holding READ_STEPS_PER_KEPT times
|
|
56
|
+
* as many stop the read.
|
|
53
57
|
*/
|
|
54
58
|
export declare class WalkReader {
|
|
55
59
|
private headerRead;
|
|
56
|
-
private
|
|
57
|
-
private readonly
|
|
58
|
-
constructor(
|
|
60
|
+
private lookupRead;
|
|
61
|
+
private readonly walks;
|
|
62
|
+
constructor(walks: TabixSource);
|
|
59
63
|
header(opts?: {
|
|
60
64
|
signal?: AbortSignal;
|
|
61
65
|
}): Promise<WalkReaderHeader>;
|
|
62
66
|
referenceSequences(opts?: {
|
|
63
67
|
signal?: AbortSignal;
|
|
64
68
|
}): Promise<string[]>;
|
|
69
|
+
/**
|
|
70
|
+
* The walk file's sequence for `refName` on `sample`, or for `refName` as
|
|
71
|
+
* the file spells it: `chr6` on `GRCh38` is `GRCh38#0#chr6`. Undefined when
|
|
72
|
+
* the file holds neither.
|
|
73
|
+
*/
|
|
74
|
+
sequenceFor(sample: string, refName: string, opts?: {
|
|
75
|
+
signal?: AbortSignal;
|
|
76
|
+
}): Promise<string | undefined>;
|
|
65
77
|
queryStart(start: number, opts?: {
|
|
66
78
|
signal?: AbortSignal;
|
|
67
79
|
}): Promise<number>;
|
package/esm/walkReader.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { refNameLookup, resolveRefName } from "./refNames.js";
|
|
2
|
+
import { decodeChunk, pieceLinks } from "./walkChunk.js";
|
|
3
|
+
import { WalkGraph, byteBudgetError, chunkQueryStart, joinPieces, keptStepsError, lookbackChunks, stepBudgetError, walkCut, walkHeader, } from "./walkRows.js";
|
|
3
4
|
// A window reads whole chunks and keeps the steps on it, so a narrow window
|
|
4
5
|
// reads several times what it keeps: 464 haplotypes across the 1.5 kb ABCA7
|
|
5
6
|
// repeat read 5.0 M steps and keep 1.16 M, and the cut takes 0.4 s from warm
|
|
@@ -12,17 +13,22 @@ const FOLLOW_ROUNDS = 3;
|
|
|
12
13
|
const FOLLOW_CHUNKS = 4;
|
|
13
14
|
// bp of reference a cut keeps past each end of the window
|
|
14
15
|
const WALK_CONTEXT = 1000;
|
|
16
|
+
// The budgets the JBrowse graph plugin and BandageJS cut a browser's window by
|
|
17
|
+
export const WALK_BUDGETS = { stepBudget: 4000000, byteBudget: 8000000 };
|
|
18
|
+
// A walk file gfa-to-tabix 0.12 or earlier wrote, which the reader refuses
|
|
19
|
+
export class WalkFormatError extends Error {
|
|
20
|
+
constructor() {
|
|
21
|
+
super(...arguments);
|
|
22
|
+
this.name = 'WalkFormatError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
15
25
|
// @gmod/tabix loops forever on a NaN end and reads nothing for a NaN start
|
|
16
26
|
// (GMOD/tabix-js#157), so every query refuses a range that is not finite first
|
|
17
|
-
function checkRange(refName, start, end) {
|
|
27
|
+
export function checkRange(refName, start, end) {
|
|
18
28
|
if (!Number.isFinite(start) || !Number.isFinite(end)) {
|
|
19
29
|
throw new Error(`${refName}:${start}-${end} is not a finite range to query the graph's index for`);
|
|
20
30
|
}
|
|
21
31
|
}
|
|
22
|
-
function getLines(file, refName, start, end, opts) {
|
|
23
|
-
checkRange(refName, start, end);
|
|
24
|
-
return file.getLines(refName, start, end, opts);
|
|
25
|
-
}
|
|
26
32
|
// Settles like `promise` unless `signal` aborts first
|
|
27
33
|
function raceAbort(promise, signal) {
|
|
28
34
|
if (!signal) {
|
|
@@ -42,26 +48,29 @@ function raceAbort(promise, signal) {
|
|
|
42
48
|
});
|
|
43
49
|
}
|
|
44
50
|
/**
|
|
45
|
-
* A
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
+
* A `gfa-to-tabix --walks` file, `<prefix>.<sample>.walks.bed.gz`, read a
|
|
52
|
+
* window at a time: one read over whole chunks, from the lookback chunks
|
|
53
|
+
* before the window to its end, each chunk decoded once. The index first
|
|
54
|
+
* estimates the bytes the read fetches, and a window past the byte budget
|
|
55
|
+
* reads no row. Only the walks `keep` accepts are decoded. The step budget
|
|
56
|
+
* bounds the steps the cut keeps, and rows holding READ_STEPS_PER_KEPT times
|
|
57
|
+
* as many stop the read.
|
|
51
58
|
*/
|
|
52
59
|
export class WalkReader {
|
|
53
|
-
constructor(
|
|
54
|
-
this.
|
|
55
|
-
this.chunkWithoutHeader = chunkWithoutHeader;
|
|
60
|
+
constructor(walks) {
|
|
61
|
+
this.walks = walks;
|
|
56
62
|
}
|
|
57
63
|
// The header, read once for every caller: one caller giving up stops only
|
|
58
64
|
// its own wait, and a read that fails is tried again by the next caller
|
|
59
65
|
header(opts = {}) {
|
|
60
66
|
if (!this.headerRead) {
|
|
61
|
-
const read = this.
|
|
67
|
+
const read = this.walks.getHeaderLines().then(lines => {
|
|
62
68
|
var _a;
|
|
63
69
|
const header = walkHeader(lines);
|
|
64
|
-
|
|
70
|
+
if (header.format !== 2) {
|
|
71
|
+
throw new WalkFormatError('This walk file is format 1, from gfa-to-tabix 0.12 or earlier; rebuild it with gfa-to-tabix 1.0 or later');
|
|
72
|
+
}
|
|
73
|
+
const chunk = (_a = header.chunk) !== null && _a !== void 0 ? _a : 65536;
|
|
65
74
|
return {
|
|
66
75
|
...header,
|
|
67
76
|
chunk,
|
|
@@ -79,36 +88,40 @@ export class WalkReader {
|
|
|
79
88
|
}
|
|
80
89
|
// The reference sequences the walk file holds windows on
|
|
81
90
|
referenceSequences(opts = {}) {
|
|
82
|
-
return this.
|
|
91
|
+
return this.walks.getReferenceSequenceNames(opts);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The walk file's sequence for `refName` on `sample`, or for `refName` as
|
|
95
|
+
* the file spells it: `chr6` on `GRCh38` is `GRCh38#0#chr6`. Undefined when
|
|
96
|
+
* the file holds neither.
|
|
97
|
+
*/
|
|
98
|
+
async sequenceFor(sample, refName, opts = {}) {
|
|
99
|
+
if (!this.lookupRead) {
|
|
100
|
+
const read = this.walks.getReferenceSequenceNames().then(refNameLookup);
|
|
101
|
+
read.catch(() => {
|
|
102
|
+
if (this.lookupRead === read) {
|
|
103
|
+
this.lookupRead = undefined;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
this.lookupRead = read;
|
|
107
|
+
}
|
|
108
|
+
const lookup = await raceAbort(this.lookupRead, opts.signal);
|
|
109
|
+
return resolveRefName(lookup, sample, refName);
|
|
83
110
|
}
|
|
84
111
|
async queryStart(start, opts = {}) {
|
|
85
112
|
const { chunk, lookback } = await this.header(opts);
|
|
86
113
|
return chunkQueryStart(start, chunk, lookback);
|
|
87
114
|
}
|
|
88
115
|
async cut(refName, region, opts = {}) {
|
|
89
|
-
|
|
90
|
-
const {
|
|
91
|
-
|
|
92
|
-
const format2 = format === 2;
|
|
93
|
-
const nodeFile = this.files.nodes;
|
|
94
|
-
const linkFile = opts.links === false ? undefined : this.files.links;
|
|
95
|
-
if (!format2 && !nodeFile) {
|
|
96
|
-
throw new Error('A format 1 walk file needs its node file: open it with `nodes`');
|
|
97
|
-
}
|
|
98
|
-
const links = format2 ? opts.links !== false : linkFile !== undefined;
|
|
116
|
+
var _a;
|
|
117
|
+
const { signal, keep, context = WALK_CONTEXT, stepBudget = Infinity, byteBudget = Infinity, links = true, } = opts;
|
|
118
|
+
const { chunk, lookback } = await this.header({ signal });
|
|
99
119
|
const from = chunkQueryStart(region.start, chunk, lookback);
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const bytesFor = async (spans) => {
|
|
106
|
-
if (!budgeted) {
|
|
107
|
-
return 0;
|
|
108
|
-
}
|
|
109
|
-
const sizes = await Promise.all(files.map(file => file.bytesForRegions(spans.map(span => ({ refName, ...span })), { signal })));
|
|
110
|
-
return sizes.reduce((sum, bytes) => sum + bytes, 0);
|
|
111
|
-
};
|
|
120
|
+
const bytesForRegions = (_a = this.walks.bytesForRegions) === null || _a === void 0 ? void 0 : _a.bind(this.walks);
|
|
121
|
+
const budgeted = Number.isFinite(byteBudget) && bytesForRegions !== undefined;
|
|
122
|
+
const bytesFor = async (spans) => budgeted
|
|
123
|
+
? bytesForRegions(spans.map(span => ({ refName, ...span })), { signal })
|
|
124
|
+
: 0;
|
|
112
125
|
const tooLarge = budgeted
|
|
113
126
|
? await byteBudgetError(async (end) => {
|
|
114
127
|
checkRange(refName, from, end);
|
|
@@ -133,49 +146,39 @@ export class WalkReader {
|
|
|
133
146
|
context,
|
|
134
147
|
sequences: opts.sequences,
|
|
135
148
|
});
|
|
136
|
-
const read = (file, lineCallback, start = from, end = region.end) => getLines(file, refName, start, end, {
|
|
137
|
-
signal: reads.signal,
|
|
138
|
-
lineCallback,
|
|
139
|
-
});
|
|
140
|
-
const addRow = (line) => {
|
|
141
|
-
if (keep === undefined || keep(walkRowName(line))) {
|
|
142
|
-
rows.push(parseWalkRow(line));
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
const addNode = (line) => {
|
|
146
|
-
graph.addNode(line);
|
|
147
|
-
};
|
|
148
|
-
const addLink = (line) => {
|
|
149
|
-
graph.addLink(line);
|
|
150
|
-
};
|
|
151
149
|
const decoded = new Set();
|
|
152
150
|
const ends = [];
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
const readChunks = async (start
|
|
151
|
+
// each chunk read is decoded once, into its rows, nodes and the links
|
|
152
|
+
// between pieces
|
|
153
|
+
const readChunks = async (start, end) => {
|
|
154
|
+
checkRange(refName, start, end);
|
|
156
155
|
const byChunk = new Map();
|
|
157
|
-
await
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
held.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
156
|
+
await this.walks.getLines(refName, start, end, {
|
|
157
|
+
signal: reads.signal,
|
|
158
|
+
lineCallback: line => {
|
|
159
|
+
const cols = line.split('\t');
|
|
160
|
+
const cs = +cols[1];
|
|
161
|
+
const held = byChunk.get(cs);
|
|
162
|
+
if (held) {
|
|
163
|
+
held.push(cols);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
byChunk.set(cs, [cols]);
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
});
|
|
168
170
|
for (const [cs, chunkRows] of byChunk) {
|
|
169
171
|
if (decoded.has(cs)) {
|
|
170
172
|
continue;
|
|
171
173
|
}
|
|
172
174
|
decoded.add(cs);
|
|
173
|
-
const chunkCut = decodeChunk(chunkRows, keep);
|
|
175
|
+
const chunkCut = decodeChunk(chunkRows, keep, opts.sequences);
|
|
174
176
|
rows.push(...chunkCut.rows);
|
|
175
177
|
for (const [id, node] of chunkCut.nodes) {
|
|
176
178
|
graph.placeNode(id, node);
|
|
177
179
|
}
|
|
178
|
-
graph.
|
|
180
|
+
graph.addSequences(chunkCut.sequences);
|
|
181
|
+
graph.addChunk(chunkCut.chunk);
|
|
179
182
|
ends.push(...chunkCut.ends);
|
|
180
183
|
}
|
|
181
184
|
if (links) {
|
|
@@ -184,20 +187,12 @@ export class WalkReader {
|
|
|
184
187
|
}
|
|
185
188
|
}
|
|
186
189
|
};
|
|
187
|
-
const checkSteps = () => {
|
|
188
|
-
const error = stepBudgetError(rows, stepBudget, READ_STEPS_PER_KEPT, region, chunk, keep !== undefined, opts.budgetNames);
|
|
189
|
-
if (error) {
|
|
190
|
-
throw error;
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
190
|
try {
|
|
194
|
-
await (
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
...(linkFile ? [read(linkFile, addLink)] : []),
|
|
200
|
-
]));
|
|
191
|
+
await readChunks(from, region.end);
|
|
192
|
+
const tooManyRead = stepBudgetError(rows, stepBudget, READ_STEPS_PER_KEPT, region, chunk, keep !== undefined, opts.budgetNames);
|
|
193
|
+
if (tooManyRead) {
|
|
194
|
+
throw tooManyRead;
|
|
195
|
+
}
|
|
201
196
|
// the chunks read, first to last, which a follow widens to the
|
|
202
197
|
// farthest chunk a cut-off walk goes on in, so the reference between
|
|
203
198
|
// is read too
|
|
@@ -224,13 +219,7 @@ export class WalkReader {
|
|
|
224
219
|
}
|
|
225
220
|
first = Math.min(first, ...starts);
|
|
226
221
|
last = Math.max(last, ...starts);
|
|
227
|
-
await Promise.all(spans.
|
|
228
|
-
? [readChunks(start, end)]
|
|
229
|
-
: [
|
|
230
|
-
read(this.files.walks, addRow, start, end),
|
|
231
|
-
read(nodeFile, addNode, start, end),
|
|
232
|
-
...(linkFile ? [read(linkFile, addLink, start, end)] : []),
|
|
233
|
-
]));
|
|
222
|
+
await Promise.all(spans.map(({ start, end }) => readChunks(start, end)));
|
|
234
223
|
cut = walkCut(joinPieces(rows), graph.nodes, refName);
|
|
235
224
|
}
|
|
236
225
|
}
|
package/esm/walkRows.d.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { RegionTooLargeError } from './util.ts';
|
|
2
2
|
import type { GraphTables } from './util.ts';
|
|
3
|
-
import type {
|
|
3
|
+
import type { Chunk } from './walkChunk.ts';
|
|
4
4
|
export interface WalkRow {
|
|
5
5
|
name: string;
|
|
6
6
|
fragStart: number;
|
|
7
7
|
hapOffset: number;
|
|
8
8
|
piece: number;
|
|
9
9
|
n: number;
|
|
10
|
-
|
|
11
|
-
steps?: {
|
|
10
|
+
steps: {
|
|
12
11
|
ids: Int32Array;
|
|
13
12
|
rev: Uint8Array;
|
|
14
13
|
};
|
|
@@ -34,8 +33,6 @@ export interface WalkWindow {
|
|
|
34
33
|
context: number;
|
|
35
34
|
sequences?: boolean;
|
|
36
35
|
}
|
|
37
|
-
export declare function walkRowName(line: string): string;
|
|
38
|
-
export declare function parseWalkRow(line: string): WalkRow;
|
|
39
36
|
export interface WalkHeader {
|
|
40
37
|
format?: number;
|
|
41
38
|
chunk?: number;
|
|
@@ -44,11 +41,10 @@ export interface WalkHeader {
|
|
|
44
41
|
haplotypes: string[];
|
|
45
42
|
}
|
|
46
43
|
/**
|
|
47
|
-
* What a walk file's header states:
|
|
48
|
-
*
|
|
49
|
-
* `#reference\tGRCh38` line
|
|
50
|
-
* `#haplotype\tHG002#1` line per other haplotype with rows
|
|
51
|
-
* the longest node in bp as `maxnode:i:1024` beside the chunk size
|
|
44
|
+
* What a walk file's header states: a `#walks` line with the format, the
|
|
45
|
+
* chunk size and the longest node in bp, `format:i:2\tchunk:i:65536\tmaxnode:i:1024`,
|
|
46
|
+
* a `#reference\tGRCh38` line naming the reference sample and a
|
|
47
|
+
* `#haplotype\tHG002#1` line per other haplotype with rows
|
|
52
48
|
*/
|
|
53
49
|
export declare function walkHeader(lines: string[]): WalkHeader;
|
|
54
50
|
/**
|
|
@@ -62,10 +58,12 @@ export declare function walkHeader(lines: string[]): WalkHeader;
|
|
|
62
58
|
export declare function lookbackChunks(maxNode: number | undefined, chunk: number): number;
|
|
63
59
|
export declare function chunkQueryStart(start: number, chunk: number, lookback?: number): number;
|
|
64
60
|
export declare function walkNameFilter(wanted: string[] | undefined, referenceHaplotype: string | undefined): ((name: string) => boolean) | undefined;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
/**
|
|
62
|
+
* The walks a cut on `refName` decodes: its own and those `wanted` names. A
|
|
63
|
+
* pggb P-line names a reference with no haplotype field (`chm13#chr1`), so the
|
|
64
|
+
* filter matches the reference by its whole name.
|
|
65
|
+
*/
|
|
66
|
+
export declare function walksKept(wanted: string[] | undefined, refName: string): ((name: string) => boolean) | undefined;
|
|
69
67
|
export interface WalkRun {
|
|
70
68
|
name: string;
|
|
71
69
|
hapOffset: number;
|
|
@@ -103,24 +101,20 @@ export declare function walkCut(runs: WalkRun[], nodes: Map<number, WalkNode>, r
|
|
|
103
101
|
open: WalkChunk[];
|
|
104
102
|
};
|
|
105
103
|
/**
|
|
106
|
-
* The
|
|
107
|
-
*
|
|
108
|
-
* arrives twice and is read once; columns past a node row's eighth (LN:i:,
|
|
109
|
-
* SQ:Z: and whatever follows) are read only for the bases a window asks for.
|
|
104
|
+
* The nodes and links of the chunks a cut reads, and the tables of the cut
|
|
105
|
+
* they make with its walks. A node in two chunks is placed once.
|
|
110
106
|
*/
|
|
111
107
|
export declare class WalkGraph {
|
|
112
108
|
readonly nodes: Map<number, WalkNode>;
|
|
113
109
|
readonly sequences: Map<number, string>;
|
|
114
110
|
private links;
|
|
115
|
-
private readonly
|
|
116
|
-
private readonly t;
|
|
111
|
+
private readonly chunks;
|
|
117
112
|
private readonly window;
|
|
118
113
|
constructor(window: WalkWindow);
|
|
119
|
-
addNode(line: string): void;
|
|
120
114
|
placeNode(id: number, node: Omit<WalkNode, 'onWindow'>): void;
|
|
121
115
|
addLinkEnds(source: number, target: number): void;
|
|
122
|
-
|
|
123
|
-
|
|
116
|
+
addChunk(chunk: Chunk): void;
|
|
117
|
+
addSequences(sequences: Map<number, string>): void;
|
|
124
118
|
/**
|
|
125
119
|
* The cut as typed arrays, which the worker transfers rather than copies:
|
|
126
120
|
* nodes by reference position, each link once in the order its L line
|
package/esm/walkRows.js
CHANGED
|
@@ -1,52 +1,14 @@
|
|
|
1
|
-
import { RegionTooLargeError, bpString, panSNMatchesPrefix } from "./util.js";
|
|
2
|
-
import { placeAndLink } from "./
|
|
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
|
-
}
|
|
1
|
+
import { RegionTooLargeError, bpString, panSNHaplotype, panSNMatchesPrefix, } from "./util.js";
|
|
2
|
+
import { placeAndLink } from "./walkChunk.js";
|
|
40
3
|
function integerTag(line, tag) {
|
|
41
4
|
const match = new RegExp(`(?:^|\\t)${tag}:i:(\\d+)`).exec(line);
|
|
42
5
|
return match ? +match[1] : undefined;
|
|
43
6
|
}
|
|
44
7
|
/**
|
|
45
|
-
* What a walk file's header states:
|
|
46
|
-
*
|
|
47
|
-
* `#reference\tGRCh38` line
|
|
48
|
-
* `#haplotype\tHG002#1` line per other haplotype with rows
|
|
49
|
-
* the longest node in bp as `maxnode:i:1024` beside the chunk size
|
|
8
|
+
* What a walk file's header states: a `#walks` line with the format, the
|
|
9
|
+
* chunk size and the longest node in bp, `format:i:2\tchunk:i:65536\tmaxnode:i:1024`,
|
|
10
|
+
* a `#reference\tGRCh38` line naming the reference sample and a
|
|
11
|
+
* `#haplotype\tHG002#1` line per other haplotype with rows
|
|
50
12
|
*/
|
|
51
13
|
export function walkHeader(lines) {
|
|
52
14
|
var _a, _b, _c;
|
|
@@ -91,30 +53,14 @@ export function walkNameFilter(wanted, referenceHaplotype) {
|
|
|
91
53
|
: (name) => panSNMatchesPrefix(name, referenceHaplotype) ||
|
|
92
54
|
wanted.some(prefix => panSNMatchesPrefix(name, prefix));
|
|
93
55
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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 };
|
|
56
|
+
/**
|
|
57
|
+
* The walks a cut on `refName` decodes: its own and those `wanted` names. A
|
|
58
|
+
* pggb P-line names a reference with no haplotype field (`chm13#chr1`), so the
|
|
59
|
+
* filter matches the reference by its whole name.
|
|
60
|
+
*/
|
|
61
|
+
export function walksKept(wanted, refName) {
|
|
62
|
+
var _a;
|
|
63
|
+
return walkNameFilter(wanted, (_a = panSNHaplotype(refName)) !== null && _a !== void 0 ? _a : refName);
|
|
118
64
|
}
|
|
119
65
|
export function joinPieces(rows) {
|
|
120
66
|
const byPath = new Map();
|
|
@@ -148,13 +94,8 @@ function decodeRun(pieces) {
|
|
|
148
94
|
const rev = new Uint8Array(n);
|
|
149
95
|
let at = 0;
|
|
150
96
|
for (const p of pieces) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
rev.set(p.steps.rev, at);
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
decodeSteps(p.enc, p.n, ids, rev, at);
|
|
157
|
-
}
|
|
97
|
+
ids.set(p.steps.ids, at);
|
|
98
|
+
rev.set(p.steps.rev, at);
|
|
158
99
|
at += p.n;
|
|
159
100
|
}
|
|
160
101
|
const first = pieces[0];
|
|
@@ -308,50 +249,19 @@ export function walkCut(runs, nodes, refName) {
|
|
|
308
249
|
}
|
|
309
250
|
return { kept, fragments, open: [...open.values()] };
|
|
310
251
|
}
|
|
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
252
|
/**
|
|
321
|
-
* The
|
|
322
|
-
*
|
|
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.
|
|
253
|
+
* The nodes and links of the chunks a cut reads, and the tables of the cut
|
|
254
|
+
* they make with its walks. A node in two chunks is placed once.
|
|
325
255
|
*/
|
|
326
256
|
export class WalkGraph {
|
|
327
257
|
constructor(window) {
|
|
328
258
|
this.nodes = new Map();
|
|
329
259
|
this.sequences = new Map();
|
|
330
260
|
this.links = [];
|
|
331
|
-
//
|
|
332
|
-
this.
|
|
333
|
-
this.t = [];
|
|
261
|
+
// the chunks read, whose nodes off R and links tables() fills in
|
|
262
|
+
this.chunks = [];
|
|
334
263
|
this.window = window;
|
|
335
264
|
}
|
|
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
265
|
// A node with its position, unless the cut already holds it
|
|
356
266
|
placeNode(id, node) {
|
|
357
267
|
if (this.nodes.has(id)) {
|
|
@@ -370,13 +280,16 @@ export class WalkGraph {
|
|
|
370
280
|
addLinkEnds(source, target) {
|
|
371
281
|
this.links.push(source, target);
|
|
372
282
|
}
|
|
373
|
-
|
|
374
|
-
this.
|
|
283
|
+
addChunk(chunk) {
|
|
284
|
+
this.chunks.push(chunk);
|
|
375
285
|
}
|
|
376
|
-
//
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
286
|
+
// Bases of nodes a chunk's rows give, for a window that asks for them
|
|
287
|
+
addSequences(sequences) {
|
|
288
|
+
if (this.window.sequences) {
|
|
289
|
+
for (const [id, bases] of sequences) {
|
|
290
|
+
this.sequences.set(id, bases);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
380
293
|
}
|
|
381
294
|
/**
|
|
382
295
|
* The cut as typed arrays, which the worker transfers rather than copies:
|
|
@@ -386,8 +299,8 @@ export class WalkGraph {
|
|
|
386
299
|
* for the same rows.
|
|
387
300
|
*/
|
|
388
301
|
tables(kept, fragments) {
|
|
389
|
-
if (this.
|
|
390
|
-
placeAndLink(this.
|
|
302
|
+
if (this.chunks.length > 0) {
|
|
303
|
+
placeAndLink(this.chunks, kept, (id, node) => {
|
|
391
304
|
const held = this.nodes.get(id);
|
|
392
305
|
if (held) {
|
|
393
306
|
this.nodes.set(id, { ...held, ...node });
|
|
@@ -498,9 +411,6 @@ export class WalkGraph {
|
|
|
498
411
|
(a.name < b.name ? -1 : a.name > b.name ? 1 : a.hapStart - b.hapStart));
|
|
499
412
|
}
|
|
500
413
|
}
|
|
501
|
-
function endpoint(line, from, to) {
|
|
502
|
-
return (+line.slice(from, to - 1) * 2 + (line.charCodeAt(to - 1) === 45 ? 1 : 0));
|
|
503
|
-
}
|
|
504
414
|
export const BUDGET_NAMES = {
|
|
505
415
|
steps: 'the step budget',
|
|
506
416
|
bytes: 'a byte budget of',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jbrowse/graph-walks",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
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
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -20,39 +20,25 @@
|
|
|
20
20
|
"haplotype"
|
|
21
21
|
],
|
|
22
22
|
"type": "module",
|
|
23
|
-
"packageManager": "pnpm@11.20.0",
|
|
24
23
|
"exports": {
|
|
25
|
-
".":
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./esm/index.d.ts",
|
|
26
|
+
"default": "./esm/index.js"
|
|
27
|
+
}
|
|
26
28
|
},
|
|
27
29
|
"publishConfig": {
|
|
28
|
-
"access": "public"
|
|
29
|
-
"exports": {
|
|
30
|
-
".": {
|
|
31
|
-
"types": "./esm/index.d.ts",
|
|
32
|
-
"default": "./esm/index.js"
|
|
33
|
-
}
|
|
34
|
-
}
|
|
30
|
+
"access": "public"
|
|
35
31
|
},
|
|
36
32
|
"files": [
|
|
37
33
|
"esm"
|
|
38
34
|
],
|
|
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
35
|
"engines": {
|
|
51
36
|
"node": ">=20"
|
|
52
37
|
},
|
|
53
38
|
"devDependencies": {
|
|
54
39
|
"@eslint/js": "^10.0.1",
|
|
55
40
|
"@gmod/tabix": "^3.10.1",
|
|
41
|
+
"@types/node": "^26.6.5",
|
|
56
42
|
"eslint": "^10.12.0",
|
|
57
43
|
"eslint-plugin-import-x": "^4.17.1",
|
|
58
44
|
"generic-filehandle2": "^2.5.0",
|
|
@@ -60,5 +46,15 @@
|
|
|
60
46
|
"typescript": "^6.0.3",
|
|
61
47
|
"typescript-eslint": "^8.71.1",
|
|
62
48
|
"vitest": "^5.0.3"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "rm -rf esm && tsc -p tsconfig.build.json",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"lint": "eslint --report-unused-disable-directives --max-warnings 0",
|
|
54
|
+
"format": "prettier --write .",
|
|
55
|
+
"format:check": "prettier --check .",
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"preversion": "pnpm lint && pnpm format:check && pnpm typecheck && pnpm test && pnpm build",
|
|
58
|
+
"postversion": "git push --follow-tags"
|
|
63
59
|
}
|
|
64
|
-
}
|
|
60
|
+
}
|