@jbrowse/graph-walks 0.1.0 → 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.
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 set and a region on its reference
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 1 (`.walks.bed.gz` with `.nodes.bed.gz` and `.links.bed.gz`) and
11
- format 2 (`.walks.bed.gz` alone), each with its `.tbi`
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
 
@@ -32,16 +34,13 @@ import {
32
34
  } from '@jbrowse/graph-walks'
33
35
  import { RemoteFile } from 'generic-filehandle2'
34
36
 
35
- const file = (url: string) =>
37
+ const url = 'https://example.org/hprc-v2.1-mc-grch38.GRCh38.walks.bed.gz'
38
+ const reader = new WalkReader(
36
39
  new TabixIndexedFile({
37
40
  filehandle: new RemoteFile(url),
38
41
  tbiFilehandle: new RemoteFile(`${url}.tbi`),
39
- })
40
- const prefix = 'https://jbrowse.org/demos/hprc/hprc-v2.1-mc-grch38.GRCh38'
41
- const reader = new WalkReader({
42
- walks: file(`${prefix}.walks.bed.gz`),
43
- nodes: file(`${prefix}.nodes.bed.gz`),
44
- })
42
+ }),
43
+ )
45
44
 
46
45
  const refName = 'GRCh38#0#chr6'
47
46
  const { graph, kept, fragments } = await reader.cut(
@@ -68,8 +67,8 @@ const tables = graph.tables(kept, fragments)
68
67
  | `context` | bp of reference kept past each end of the region; default 1000 |
69
68
  | `stepBudget` | The most steps a cut keeps; no limit by default |
70
69
  | `byteBudget` | The most compressed bytes a cut fetches; no limit by default |
71
- | `links` | `false` leaves the link file unread |
72
- | `sequences` | Keep each node's bases in `graph.sequences`, from `--sequences` files |
70
+ | `links` | `false` lists no links, for a caller that draws no edges |
71
+ | `sequences` | Keep each node's bases in `graph.sequences`, from a `--sequences` file |
73
72
  | `signal` | An `AbortSignal` that stops the reads |
74
73
 
75
74
  A cut over a budget throws a `RegionTooLargeError`. Its `fitsBp` holds about how
@@ -77,9 +76,14 @@ wide a window would fit, and JBrowse shows it as a zoom-in notice.
77
76
 
78
77
  ## How a cut works
79
78
 
79
+ - A chunk holds the reference's path through it once, each way a haplotype
80
+ leaves that path once, and every other path's piece as the list of ways it
81
+ takes; gfa-to-tabix's
82
+ [walks-format.md](https://github.com/GMOD/gfa-to-tabix/blob/main/docs/walks-format.md)
83
+ describes the rows
80
84
  - A cut reads every chunk under the region, and the chunks before it as far as
81
85
  the header's `maxnode` reaches, since a node is filed under the chunk holding
82
- its start
86
+ its start, and decodes each chunk once
83
87
  - A haplotype that leaves the reference near the region is followed to where it
84
88
  rejoins, and the reference is cut out as far as those rejoins reach
85
89
  - 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,7 @@
1
- export { BUDGET_NAMES, WalkGraph, byteBudgetError, chunkQueryStart, decodeSteps, joinPieces, keptStepsError, lookbackChunks, parseWalkRow, stepBudgetError, walkCut, walkHeader, walkNameFilter, walkRowName, } from './walkRows.ts';
1
+ export { BUDGET_NAMES, WalkGraph, byteBudgetError, chunkQueryStart, joinPieces, keptStepsError, lookbackChunks, stepBudgetError, walkCut, walkHeader, walkNameFilter, } 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 { referenceNodes } from './walkFormat2.ts';
5
- export type { TabixSource, WalkCutOptions, WalkFileSet, WalkReaderHeader, } from './walkReader.ts';
3
+ export { WalkFormatError, WalkReader } from './walkReader.ts';
4
+ export { referenceNodes } from './walkChunk.ts';
5
+ export type { TabixSource, WalkCutOptions, WalkReaderHeader, } from './walkReader.ts';
6
6
  export { RegionTooLargeError, bpString, panSNHaplotype, panSNMatchesPrefix, } from './util.ts';
7
7
  export type { GraphTables } from './util.ts';
package/esm/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { BUDGET_NAMES, WalkGraph, byteBudgetError, chunkQueryStart, decodeSteps, joinPieces, keptStepsError, lookbackChunks, parseWalkRow, stepBudgetError, walkCut, walkHeader, walkNameFilter, walkRowName, } from "./walkRows.js";
2
- export { WalkReader } from "./walkReader.js";
3
- export { referenceNodes } from "./walkFormat2.js";
1
+ export { BUDGET_NAMES, WalkGraph, byteBudgetError, chunkQueryStart, joinPieces, keptStepsError, lookbackChunks, stepBudgetError, walkCut, walkHeader, walkNameFilter, } from "./walkRows.js";
2
+ export { WalkFormatError, WalkReader } from "./walkReader.js";
3
+ export { referenceNodes } from "./walkChunk.js";
4
4
  export { RegionTooLargeError, bpString, panSNHaplotype, panSNMatchesPrefix, } from "./util.js";
@@ -24,7 +24,7 @@ export declare class RefPath {
24
24
  }
25
25
  export declare class WalkRowError extends Error {
26
26
  }
27
- export interface Format2Chunk {
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: Format2Chunk;
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: Format2Chunk[], kept: Set<number>, place: (id: number, node: PlacedNode) => void, link: (a: number, b: number) => void): void;
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
- // Format 2 of gfa-to-tabix --walks, one file per reference (that repo's
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
  });
@@ -24,10 +24,8 @@ export interface TabixSource {
24
24
  signal?: AbortSignal;
25
25
  }): Promise<number>;
26
26
  }
27
- export interface WalkFileSet {
28
- walks: TabixSource;
29
- nodes?: TabixSource;
30
- links?: TabixSource;
27
+ export declare class WalkFormatError extends Error {
28
+ name: string;
31
29
  }
32
30
  export interface WalkCutOptions {
33
31
  signal?: AbortSignal;
@@ -44,18 +42,18 @@ export type WalkReaderHeader = WalkHeader & {
44
42
  lookback: number;
45
43
  };
46
44
  /**
47
- * A walk-indexed graph's three files, read a window at a time: one read of
48
- * each over whole chunks, from the lookback chunks before the window to its
49
- * end. The indexes first estimate the bytes those reads fetch, and a window
50
- * past the byte budget reads no row. Only the walks `keep` accepts are
51
- * decoded. The step budget bounds the steps the cut keeps, and rows holding
52
- * READ_STEPS_PER_KEPT times as many stop the reads, which go out together.
45
+ * A `gfa-to-tabix --walks` file, `<prefix>.<sample>.walks.bed.gz`, read a
46
+ * window at a time: one read over whole chunks, from the lookback chunks
47
+ * before the window to its end, each chunk decoded once. The index first
48
+ * estimates the bytes the read fetches, and a window past the byte budget
49
+ * reads no row. Only the walks `keep` accepts are decoded. The step budget
50
+ * bounds the steps the cut keeps, and rows holding READ_STEPS_PER_KEPT times
51
+ * as many stop the read.
53
52
  */
54
53
  export declare class WalkReader {
55
54
  private headerRead;
56
- private readonly files;
57
- private readonly chunkWithoutHeader;
58
- constructor(files: WalkFileSet, chunkWithoutHeader?: number);
55
+ private readonly walks;
56
+ constructor(walks: TabixSource);
59
57
  header(opts?: {
60
58
  signal?: AbortSignal;
61
59
  }): Promise<WalkReaderHeader>;
package/esm/walkReader.js CHANGED
@@ -1,5 +1,5 @@
1
- import { decodeChunk, pieceLinks } from "./walkFormat2.js";
2
- import { WalkGraph, byteBudgetError, chunkQueryStart, joinPieces, keptStepsError, lookbackChunks, parseWalkRow, stepBudgetError, walkCut, walkHeader, walkRowName, } from "./walkRows.js";
1
+ import { decodeChunk, pieceLinks } from "./walkChunk.js";
2
+ import { WalkGraph, byteBudgetError, chunkQueryStart, joinPieces, keptStepsError, lookbackChunks, stepBudgetError, walkCut, walkHeader, } from "./walkRows.js";
3
3
  // A window reads whole chunks and keeps the steps on it, so a narrow window
4
4
  // reads several times what it keeps: 464 haplotypes across the 1.5 kb ABCA7
5
5
  // repeat read 5.0 M steps and keep 1.16 M, and the cut takes 0.4 s from warm
@@ -12,6 +12,13 @@ const FOLLOW_ROUNDS = 3;
12
12
  const FOLLOW_CHUNKS = 4;
13
13
  // bp of reference a cut keeps past each end of the window
14
14
  const WALK_CONTEXT = 1000;
15
+ // A walk file gfa-to-tabix 0.12 or earlier wrote, which the reader refuses
16
+ export class WalkFormatError extends Error {
17
+ constructor() {
18
+ super(...arguments);
19
+ this.name = 'WalkFormatError';
20
+ }
21
+ }
15
22
  // @gmod/tabix loops forever on a NaN end and reads nothing for a NaN start
16
23
  // (GMOD/tabix-js#157), so every query refuses a range that is not finite first
17
24
  function checkRange(refName, start, end) {
@@ -19,10 +26,6 @@ function checkRange(refName, start, end) {
19
26
  throw new Error(`${refName}:${start}-${end} is not a finite range to query the graph's index for`);
20
27
  }
21
28
  }
22
- function getLines(file, refName, start, end, opts) {
23
- checkRange(refName, start, end);
24
- return file.getLines(refName, start, end, opts);
25
- }
26
29
  // Settles like `promise` unless `signal` aborts first
27
30
  function raceAbort(promise, signal) {
28
31
  if (!signal) {
@@ -42,26 +45,29 @@ function raceAbort(promise, signal) {
42
45
  });
43
46
  }
44
47
  /**
45
- * A walk-indexed graph's three files, read a window at a time: one read of
46
- * each over whole chunks, from the lookback chunks before the window to its
47
- * end. The indexes first estimate the bytes those reads fetch, and a window
48
- * past the byte budget reads no row. Only the walks `keep` accepts are
49
- * decoded. The step budget bounds the steps the cut keeps, and rows holding
50
- * READ_STEPS_PER_KEPT times as many stop the reads, which go out together.
48
+ * A `gfa-to-tabix --walks` file, `<prefix>.<sample>.walks.bed.gz`, read a
49
+ * window at a time: one read over whole chunks, from the lookback chunks
50
+ * before the window to its end, each chunk decoded once. The index first
51
+ * estimates the bytes the read fetches, and a window past the byte budget
52
+ * reads no row. Only the walks `keep` accepts are decoded. The step budget
53
+ * bounds the steps the cut keeps, and rows holding READ_STEPS_PER_KEPT times
54
+ * as many stop the read.
51
55
  */
52
56
  export class WalkReader {
53
- constructor(files, chunkWithoutHeader = 65536) {
54
- this.files = files;
55
- this.chunkWithoutHeader = chunkWithoutHeader;
57
+ constructor(walks) {
58
+ this.walks = walks;
56
59
  }
57
60
  // The header, read once for every caller: one caller giving up stops only
58
61
  // its own wait, and a read that fails is tried again by the next caller
59
62
  header(opts = {}) {
60
63
  if (!this.headerRead) {
61
- const read = this.files.walks.getHeaderLines().then(lines => {
64
+ const read = this.walks.getHeaderLines().then(lines => {
62
65
  var _a;
63
66
  const header = walkHeader(lines);
64
- const chunk = (_a = header.chunk) !== null && _a !== void 0 ? _a : this.chunkWithoutHeader;
67
+ if (header.format !== 2) {
68
+ 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');
69
+ }
70
+ const chunk = (_a = header.chunk) !== null && _a !== void 0 ? _a : 65536;
65
71
  return {
66
72
  ...header,
67
73
  chunk,
@@ -79,36 +85,22 @@ export class WalkReader {
79
85
  }
80
86
  // The reference sequences the walk file holds windows on
81
87
  referenceSequences(opts = {}) {
82
- return this.files.walks.getReferenceSequenceNames(opts);
88
+ return this.walks.getReferenceSequenceNames(opts);
83
89
  }
84
90
  async queryStart(start, opts = {}) {
85
91
  const { chunk, lookback } = await this.header(opts);
86
92
  return chunkQueryStart(start, chunk, lookback);
87
93
  }
88
94
  async cut(refName, region, opts = {}) {
89
- const { signal, keep, context = WALK_CONTEXT, stepBudget = Infinity, byteBudget = Infinity, } = opts;
90
- const { chunk, lookback, format } = await this.header({ signal });
91
- // format 2 holds the whole cut in its walk file
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;
95
+ var _a;
96
+ const { signal, keep, context = WALK_CONTEXT, stepBudget = Infinity, byteBudget = Infinity, links = true, } = opts;
97
+ const { chunk, lookback } = await this.header({ signal });
99
98
  const from = chunkQueryStart(region.start, chunk, lookback);
100
- const files = format2
101
- ? [this.files.walks]
102
- : [this.files.walks, nodeFile, ...(linkFile ? [linkFile] : [])];
103
- const budgeted = Number.isFinite(byteBudget) &&
104
- files.every(file => file.bytesForRegions !== undefined);
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
- };
99
+ const bytesForRegions = (_a = this.walks.bytesForRegions) === null || _a === void 0 ? void 0 : _a.bind(this.walks);
100
+ const budgeted = Number.isFinite(byteBudget) && bytesForRegions !== undefined;
101
+ const bytesFor = async (spans) => budgeted
102
+ ? bytesForRegions(spans.map(span => ({ refName, ...span })), { signal })
103
+ : 0;
112
104
  const tooLarge = budgeted
113
105
  ? await byteBudgetError(async (end) => {
114
106
  checkRange(refName, from, end);
@@ -133,49 +125,39 @@ export class WalkReader {
133
125
  context,
134
126
  sequences: opts.sequences,
135
127
  });
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
128
  const decoded = new Set();
152
129
  const ends = [];
153
- // Format 2: each chunk read is decoded once, into the rows, nodes and
154
- // links format 1 would have filed, and the links between pieces read
155
- const readChunks = async (start = from, end = region.end) => {
130
+ // each chunk read is decoded once, into its rows, nodes and the links
131
+ // between pieces
132
+ const readChunks = async (start, end) => {
133
+ checkRange(refName, start, end);
156
134
  const byChunk = new Map();
157
- await read(this.files.walks, line => {
158
- const cols = line.split('\t');
159
- const cs = +cols[1];
160
- const held = byChunk.get(cs);
161
- if (held) {
162
- held.push(cols);
163
- }
164
- else {
165
- byChunk.set(cs, [cols]);
166
- }
167
- }, start, end);
135
+ await this.walks.getLines(refName, start, end, {
136
+ signal: reads.signal,
137
+ lineCallback: line => {
138
+ const cols = line.split('\t');
139
+ const cs = +cols[1];
140
+ const held = byChunk.get(cs);
141
+ if (held) {
142
+ held.push(cols);
143
+ }
144
+ else {
145
+ byChunk.set(cs, [cols]);
146
+ }
147
+ },
148
+ });
168
149
  for (const [cs, chunkRows] of byChunk) {
169
150
  if (decoded.has(cs)) {
170
151
  continue;
171
152
  }
172
153
  decoded.add(cs);
173
- const chunkCut = decodeChunk(chunkRows, keep);
154
+ const chunkCut = decodeChunk(chunkRows, keep, opts.sequences);
174
155
  rows.push(...chunkCut.rows);
175
156
  for (const [id, node] of chunkCut.nodes) {
176
157
  graph.placeNode(id, node);
177
158
  }
178
- graph.addFormat2Chunk(chunkCut.chunk);
159
+ graph.addSequences(chunkCut.sequences);
160
+ graph.addChunk(chunkCut.chunk);
179
161
  ends.push(...chunkCut.ends);
180
162
  }
181
163
  if (links) {
@@ -184,20 +166,12 @@ export class WalkReader {
184
166
  }
185
167
  }
186
168
  };
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
169
  try {
194
- await (format2
195
- ? readChunks().then(checkSteps)
196
- : Promise.all([
197
- read(this.files.walks, addRow).then(checkSteps),
198
- read(nodeFile, addNode),
199
- ...(linkFile ? [read(linkFile, addLink)] : []),
200
- ]));
170
+ await readChunks(from, region.end);
171
+ const tooManyRead = stepBudgetError(rows, stepBudget, READ_STEPS_PER_KEPT, region, chunk, keep !== undefined, opts.budgetNames);
172
+ if (tooManyRead) {
173
+ throw tooManyRead;
174
+ }
201
175
  // the chunks read, first to last, which a follow widens to the
202
176
  // farthest chunk a cut-off walk goes on in, so the reference between
203
177
  // is read too
@@ -224,13 +198,7 @@ export class WalkReader {
224
198
  }
225
199
  first = Math.min(first, ...starts);
226
200
  last = Math.max(last, ...starts);
227
- await Promise.all(spans.flatMap(({ start, end }) => format2
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
- ]));
201
+ await Promise.all(spans.map(({ start, end }) => readChunks(start, end)));
234
202
  cut = walkCut(joinPieces(rows), graph.nodes, refName);
235
203
  }
236
204
  }
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 { Format2Chunk } from './walkFormat2.ts';
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
- enc: string;
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: its chunk size in a
48
- * `#walks\tchunk:i:65536` line, from gfa-to-tabix 0.4.0 a
49
- * `#reference\tGRCh38` line per reference sample and a
50
- * `#haplotype\tHG002#1` line per other haplotype with rows, and from 0.5.0
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,6 @@ 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
- export declare function decodeSteps(enc: string, n: number, ids?: Int32Array<ArrayBuffer>, rev?: Uint8Array<ArrayBuffer>, at?: number): {
66
- ids: Int32Array<ArrayBuffer>;
67
- rev: Uint8Array<ArrayBuffer>;
68
- };
69
61
  export interface WalkRun {
70
62
  name: string;
71
63
  hapOffset: number;
@@ -103,24 +95,20 @@ export declare function walkCut(runs: WalkRun[], nodes: Map<number, WalkNode>, r
103
95
  open: WalkChunk[];
104
96
  };
105
97
  /**
106
- * The node and link rows a walk cut reads, parsed as they arrive, and the
107
- * tables of the cut they make with its walks. A node filed under two chunks
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.
98
+ * The nodes and links of the chunks a cut reads, and the tables of the cut
99
+ * they make with its walks. A node in two chunks is placed once.
110
100
  */
111
101
  export declare class WalkGraph {
112
102
  readonly nodes: Map<number, WalkNode>;
113
103
  readonly sequences: Map<number, string>;
114
104
  private links;
115
- private readonly format2;
116
- private readonly t;
105
+ private readonly chunks;
117
106
  private readonly window;
118
107
  constructor(window: WalkWindow);
119
- addNode(line: string): void;
120
108
  placeNode(id: number, node: Omit<WalkNode, 'onWindow'>): void;
121
109
  addLinkEnds(source: number, target: number): void;
122
- addFormat2Chunk(chunk: Format2Chunk): void;
123
- addLink(line: string): void;
110
+ addChunk(chunk: Chunk): void;
111
+ addSequences(sequences: Map<number, string>): void;
124
112
  /**
125
113
  * The cut as typed arrays, which the worker transfers rather than copies:
126
114
  * nodes by reference position, each link once in the order its L line
package/esm/walkRows.js CHANGED
@@ -1,52 +1,14 @@
1
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
- }
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: 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
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,31 +53,6 @@ export function walkNameFilter(wanted, referenceHaplotype) {
91
53
  : (name) => panSNMatchesPrefix(name, referenceHaplotype) ||
92
54
  wanted.some(prefix => panSNMatchesPrefix(name, prefix));
93
55
  }
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
56
  export function joinPieces(rows) {
120
57
  const byPath = new Map();
121
58
  for (const row of rows) {
@@ -148,13 +85,8 @@ function decodeRun(pieces) {
148
85
  const rev = new Uint8Array(n);
149
86
  let at = 0;
150
87
  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
- }
88
+ ids.set(p.steps.ids, at);
89
+ rev.set(p.steps.rev, at);
158
90
  at += p.n;
159
91
  }
160
92
  const first = pieces[0];
@@ -308,50 +240,19 @@ export function walkCut(runs, nodes, refName) {
308
240
  }
309
241
  return { kept, fragments, open: [...open.values()] };
310
242
  }
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
243
  /**
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.
244
+ * The nodes and links of the chunks a cut reads, and the tables of the cut
245
+ * they make with its walks. A node in two chunks is placed once.
325
246
  */
326
247
  export class WalkGraph {
327
248
  constructor(window) {
328
249
  this.nodes = new Map();
329
250
  this.sequences = new Map();
330
251
  this.links = [];
331
- // format 2's chunks, whose nodes off R and links tables() fills in
332
- this.format2 = [];
333
- this.t = [];
252
+ // the chunks read, whose nodes off R and links tables() fills in
253
+ this.chunks = [];
334
254
  this.window = window;
335
255
  }
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
256
  // A node with its position, unless the cut already holds it
356
257
  placeNode(id, node) {
357
258
  if (this.nodes.has(id)) {
@@ -370,13 +271,16 @@ export class WalkGraph {
370
271
  addLinkEnds(source, target) {
371
272
  this.links.push(source, target);
372
273
  }
373
- addFormat2Chunk(chunk) {
374
- this.format2.push(chunk);
274
+ addChunk(chunk) {
275
+ this.chunks.push(chunk);
375
276
  }
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]));
277
+ // Bases of nodes a chunk's rows give, for a window that asks for them
278
+ addSequences(sequences) {
279
+ if (this.window.sequences) {
280
+ for (const [id, bases] of sequences) {
281
+ this.sequences.set(id, bases);
282
+ }
283
+ }
380
284
  }
381
285
  /**
382
286
  * The cut as typed arrays, which the worker transfers rather than copies:
@@ -386,8 +290,8 @@ export class WalkGraph {
386
290
  * for the same rows.
387
291
  */
388
292
  tables(kept, fragments) {
389
- if (this.format2.length > 0) {
390
- placeAndLink(this.format2, kept, (id, node) => {
293
+ if (this.chunks.length > 0) {
294
+ placeAndLink(this.chunks, kept, (id, node) => {
391
295
  const held = this.nodes.get(id);
392
296
  if (held) {
393
297
  this.nodes.set(id, { ...held, ...node });
@@ -498,9 +402,6 @@ export class WalkGraph {
498
402
  (a.name < b.name ? -1 : a.name > b.name ? 1 : a.hapStart - b.hapStart));
499
403
  }
500
404
  }
501
- function endpoint(line, from, to) {
502
- return (+line.slice(from, to - 1) * 2 + (line.charCodeAt(to - 1) === 45 ? 1 : 0));
503
- }
504
405
  export const BUDGET_NAMES = {
505
406
  steps: 'the step budget',
506
407
  bytes: 'a byte budget of',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/graph-walks",
3
- "version": "0.1.0",
3
+ "version": "1.0.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
- ".": "./src/index.ts"
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
+ }