@gmod/gbz-base 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/subgraph.js CHANGED
@@ -1,26 +1,58 @@
1
- import { formatPathName } from "./db.js";
2
- import { ENDMARKER, edgeIsCanonical, encodeNode, entryOrientation, entrySide, exitOrientation, exitSide, flipSide, isReverse, nodeId, nodeOrientation, pathIsCanonical, } from "./gbwt/node.js";
1
+ import { ENDMARKER, edgeIsCanonical, encodeNode, entryOrientation, entrySide, exitOrientation, exitSide, flipNode, flipSide, isReverse, nodeId, nodeOrientation, pathIsCanonical, } from "./gbwt/node.js";
2
+ import { gfaHeaderLines, sha256Hex, subgraphName } from "./graphName.js";
3
3
  import { weightedLcs } from "./lcs.js";
4
+ import { formatPathName } from "./pathName.js";
5
+ function sideBefore(a, b) {
6
+ return (a[0] < b[0] ||
7
+ (a[0] === b[0] && (a[1] < b[1] || (a[1] === b[1] && a[2] < b[2]))));
8
+ }
4
9
  class SideQueue {
5
- items = [];
10
+ heap = [];
6
11
  push(distance, node, side) {
7
- this.items.push([distance, node, side]);
12
+ const heap = this.heap;
13
+ heap.push([distance, node, side]);
14
+ let i = heap.length - 1;
15
+ while (i > 0) {
16
+ const parent = (i - 1) >> 1;
17
+ if (sideBefore(heap[i], heap[parent])) {
18
+ ;
19
+ [heap[i], heap[parent]] = [heap[parent], heap[i]];
20
+ i = parent;
21
+ }
22
+ else {
23
+ break;
24
+ }
25
+ }
8
26
  }
9
27
  pop() {
10
- let best = 0;
11
- for (let i = 1; i < this.items.length; i++) {
12
- const a = this.items[i];
13
- const b = this.items[best];
14
- if (a[0] < b[0] ||
15
- (a[0] === b[0] && (a[1] < b[1] || (a[1] === b[1] && a[2] < b[2])))) {
16
- best = i;
28
+ const heap = this.heap;
29
+ const top = heap[0];
30
+ const last = heap.pop();
31
+ if (heap.length > 0 && last !== undefined) {
32
+ heap[0] = last;
33
+ let i = 0;
34
+ for (;;) {
35
+ const left = 2 * i + 1;
36
+ const right = left + 1;
37
+ let smallest = i;
38
+ if (left < heap.length && sideBefore(heap[left], heap[smallest])) {
39
+ smallest = left;
40
+ }
41
+ if (right < heap.length && sideBefore(heap[right], heap[smallest])) {
42
+ smallest = right;
43
+ }
44
+ if (smallest === i) {
45
+ break;
46
+ }
47
+ ;
48
+ [heap[i], heap[smallest]] = [heap[smallest], heap[i]];
49
+ i = smallest;
17
50
  }
18
51
  }
19
- const [item] = this.items.splice(best, 1);
20
- return item;
52
+ return top;
21
53
  }
22
54
  get size() {
23
- return this.items.length;
55
+ return this.heap.length;
24
56
  }
25
57
  }
26
58
  function posKey(pos) {
@@ -35,7 +67,6 @@ export class Subgraph {
35
67
  refInterval;
36
68
  refIndexCache;
37
69
  refPrefixCache;
38
- limit;
39
70
  stats = {
40
71
  orderedAlignments: 0,
41
72
  lcsAlignments: 0,
@@ -43,8 +74,12 @@ export class Subgraph {
43
74
  identificationFetches: 0,
44
75
  };
45
76
  db;
46
- constructor(db) {
77
+ limit;
78
+ signal;
79
+ constructor(db, opts = {}) {
47
80
  this.db = db;
81
+ this.limit = opts.limit;
82
+ this.signal = opts.signal;
48
83
  }
49
84
  get nodeCount() {
50
85
  return this.records.size / 2;
@@ -78,6 +113,7 @@ export class Subgraph {
78
113
  return [...this.records.keys()].sort((a, b) => a - b);
79
114
  }
80
115
  async addNode(id) {
116
+ this.signal?.throwIfAborted();
81
117
  if (this.limit !== undefined && this.nodeCount >= this.limit) {
82
118
  throw new Error(`Subgraph size limit of ${this.limit} nodes exceeded`);
83
119
  }
@@ -242,6 +278,139 @@ export class Subgraph {
242
278
  }
243
279
  return { inserted, removed: toRemove.size };
244
280
  }
281
+ async betweenNodes(start, end) {
282
+ this.clearPaths();
283
+ const active = [start, flipNode(end)];
284
+ const visited = new Set([nodeId(start), nodeId(end)]);
285
+ let inserted = 0;
286
+ while (active.length > 0) {
287
+ const curr = active.pop();
288
+ const id = nodeId(curr);
289
+ if (!this.hasNode(id)) {
290
+ await this.addNode(id);
291
+ inserted += 1;
292
+ }
293
+ for (const successor of this.record(curr).successors()) {
294
+ const successorId = nodeId(successor);
295
+ if (!visited.has(successorId)) {
296
+ active.push(successor, flipNode(successor));
297
+ visited.add(successorId);
298
+ }
299
+ }
300
+ }
301
+ return inserted;
302
+ }
303
+ async extractSnarls(snarls) {
304
+ let inserted = 0;
305
+ for (const [start, end] of await this.overlappingSnarls(snarls)) {
306
+ inserted += await this.betweenNodes(start, end);
307
+ }
308
+ return inserted;
309
+ }
310
+ async overlappingSnarls(snarls) {
311
+ const result = [];
312
+ if (snarls !== 'none') {
313
+ let foundLink = false;
314
+ for (const handle of this.sortedHandles()) {
315
+ const record = this.record(handle);
316
+ const next = record.next;
317
+ if (next !== undefined) {
318
+ foundLink = true;
319
+ if (this.hasHandle(next)) {
320
+ if (edgeIsCanonical(handle, next)) {
321
+ result.push([handle, next]);
322
+ }
323
+ }
324
+ else if (snarls === 'overlapping' &&
325
+ this.isSnarlEntryInSubgraph(record)) {
326
+ result.push([handle, next]);
327
+ }
328
+ }
329
+ }
330
+ if (!foundLink &&
331
+ snarls === 'overlapping' &&
332
+ (await this.db.hasChainLinks())) {
333
+ const covering = await this.findCoveringSnarl();
334
+ if (covering) {
335
+ result.push(covering);
336
+ }
337
+ }
338
+ }
339
+ return result;
340
+ }
341
+ isSnarlEntryInSubgraph(record) {
342
+ const successors = record.successors();
343
+ const first = successors.find(handle => this.hasHandle(handle));
344
+ return first === undefined
345
+ ? false
346
+ : successors.length > 1 ||
347
+ this.record(flipNode(first)).successors().length > 1;
348
+ }
349
+ recordReader(onFetch) {
350
+ const outside = new Map();
351
+ return async (handle) => {
352
+ const inside = this.records.get(handle);
353
+ if (inside) {
354
+ return inside;
355
+ }
356
+ let record = outside.get(handle);
357
+ if (!record) {
358
+ record = await this.db.getRecord(handle);
359
+ onFetch?.();
360
+ if (!record) {
361
+ throw new Error(`Node record ${handle} is missing from the database`);
362
+ }
363
+ outside.set(handle, record);
364
+ }
365
+ return record;
366
+ };
367
+ }
368
+ async findCoveringSnarl() {
369
+ const read = this.recordReader();
370
+ const isSnarlEntry = async (record) => {
371
+ const successors = record.successors();
372
+ const first = successors[0];
373
+ return first === undefined
374
+ ? false
375
+ : successors.length > 1 ||
376
+ (await read(flipNode(first))).successors().length > 1;
377
+ };
378
+ const classify = async (handle) => {
379
+ const reverse = await read(flipNode(handle));
380
+ return reverse.next !== undefined
381
+ ? (await isSnarlEntry(reverse))
382
+ ? { kind: 'snarl-exit', snarl: [flipNode(handle), reverse.next] }
383
+ : { kind: 'chain' }
384
+ : (await read(handle)).next !== undefined
385
+ ? { kind: 'chain' }
386
+ : { kind: 'regular' };
387
+ };
388
+ const visited = new Set();
389
+ const queue = this.sortedHandles().flatMap(handle => this.record(handle).successors());
390
+ let result;
391
+ let done = false;
392
+ while (!done && queue.length > 0) {
393
+ const handle = queue.shift();
394
+ const id = nodeId(handle);
395
+ if (!this.hasHandle(handle) && !visited.has(id)) {
396
+ visited.add(id);
397
+ const type = await classify(handle);
398
+ if (type.kind === 'snarl-exit') {
399
+ result = type.snarl;
400
+ done = true;
401
+ }
402
+ else if (type.kind === 'chain') {
403
+ done = true;
404
+ }
405
+ else {
406
+ for (const orientation of ['forward', 'reverse']) {
407
+ queue.push(...(await read(encodeNode(id, orientation))).successors());
408
+ }
409
+ }
410
+ }
411
+ }
412
+ return result;
413
+ }
245
414
  extractPaths(reference, output) {
246
415
  this.clearPaths();
247
416
  if (output === 'none') {
@@ -253,59 +422,65 @@ export class Subgraph {
253
422
  const handles = this.sortedHandles();
254
423
  const successors = new Map();
255
424
  for (const handle of handles) {
256
- successors.set(handle, this.record(handle)
257
- .gbwt()
258
- .decompress()
259
- .map(next => ({ next, hasPredecessor: false })));
425
+ const { nodes, offsets } = this.record(handle).gbwt().decompressArrays();
426
+ successors.set(handle, {
427
+ nodes,
428
+ offsets,
429
+ hasPredecessor: new Uint8Array(nodes.length),
430
+ });
260
431
  }
261
432
  for (const handle of handles) {
262
- for (const { next } of successors.get(handle)) {
263
- const entry = successors.get(next.node)?.[next.offset];
433
+ const { nodes, offsets } = successors.get(handle);
434
+ for (let i = 0; i < nodes.length; i++) {
435
+ const entry = successors.get(nodes[i]);
264
436
  if (entry) {
265
- entry.hasPredecessor = true;
437
+ entry.hasPredecessor[offsets[i]] = 1;
266
438
  }
267
439
  }
268
440
  }
269
441
  let refOffset;
270
442
  for (const handle of handles) {
271
443
  const entries = successors.get(handle);
272
- entries.forEach((entry, offset) => {
273
- if (entry.hasPredecessor) {
274
- return;
275
- }
276
- let curr = { node: handle, offset };
277
- let isRef = false;
278
- const path = [];
279
- const positions = [];
280
- let len = 0;
281
- while (curr) {
282
- if (curr.node === refPos?.handle &&
283
- curr.offset === refPos.gbwtOffset) {
284
- this.refId = this.paths.length;
285
- refOffset = path.length;
286
- isRef = true;
444
+ for (let offset = 0; offset < entries.nodes.length; offset++) {
445
+ if (entries.hasPredecessor[offset] === 0) {
446
+ let currNode = handle;
447
+ let currOffset = offset;
448
+ let isRef = false;
449
+ const path = [];
450
+ const positions = [];
451
+ let len = 0;
452
+ while (currNode !== undefined) {
453
+ if (currNode === refPos?.handle &&
454
+ currOffset === refPos.gbwtOffset) {
455
+ this.refId = this.paths.length;
456
+ refOffset = path.length;
457
+ isRef = true;
458
+ }
459
+ path.push(currNode);
460
+ positions.push({ node: currNode, offset: currOffset });
461
+ len += this.record(currNode).sequenceLen;
462
+ const step = successors.get(currNode);
463
+ const nextNode = step.nodes[currOffset];
464
+ const nextOffset = step.offsets[currOffset];
465
+ if (nextNode !== ENDMARKER && successors.has(nextNode)) {
466
+ currNode = nextNode;
467
+ currOffset = nextOffset;
468
+ }
469
+ else {
470
+ currNode = undefined;
471
+ }
472
+ }
473
+ if (isRef || pathIsCanonical(path)) {
474
+ this.paths.push({
475
+ path,
476
+ positions,
477
+ len,
478
+ weight: undefined,
479
+ identity: undefined,
480
+ });
287
481
  }
288
- path.push(curr.node);
289
- positions.push(curr);
290
- len += this.record(curr.node).sequenceLen;
291
- const step = successors.get(curr.node)?.[curr.offset];
292
- curr =
293
- step &&
294
- step.next.node !== ENDMARKER &&
295
- successors.has(step.next.node)
296
- ? step.next
297
- : undefined;
298
- }
299
- if (isRef || pathIsCanonical(path)) {
300
- this.paths.push({
301
- path,
302
- positions,
303
- len,
304
- weight: undefined,
305
- identity: undefined,
306
- });
307
482
  }
308
- });
483
+ }
309
484
  }
310
485
  if (refPos) {
311
486
  if (refOffset === undefined || this.refId === undefined) {
@@ -380,23 +555,9 @@ export class Subgraph {
380
555
  starts.set(posKey(first), index);
381
556
  }
382
557
  });
383
- const outside = new Map();
384
- const recordAt = async (handle) => {
385
- const inside = this.records.get(handle);
386
- if (inside) {
387
- return inside;
388
- }
389
- let record = outside.get(handle);
390
- if (!record) {
391
- record = await this.db.getRecord(handle);
392
- this.stats.identificationFetches += 1;
393
- if (!record) {
394
- throw new Error(`Node record ${handle} is missing from the database`);
395
- }
396
- outside.set(handle, record);
397
- }
398
- return record;
399
- };
558
+ const recordAt = this.recordReader(() => {
559
+ this.stats.identificationFetches += 1;
560
+ });
400
561
  const sampleAt = async (pos) => {
401
562
  if (pos.node >= minHandle && pos.node <= maxHandle) {
402
563
  return samples.get(posKey(pos));
@@ -451,6 +612,7 @@ export class Subgraph {
451
612
  let current = start;
452
613
  let pos;
453
614
  while (anchor === undefined) {
615
+ this.signal?.throwIfAborted();
454
616
  if (current !== undefined) {
455
617
  if (visited.has(current)) {
456
618
  break;
@@ -729,23 +891,16 @@ export class Subgraph {
729
891
  last -= 1;
730
892
  }
731
893
  const identity = info.identity;
732
- const strand = info.path.some(handle => isReverse(handle)) &&
894
+ const walkStrand = info.path.some(handle => isReverse(handle)) &&
733
895
  !info.path.some(handle => !isReverse(handle))
734
896
  ? '-'
735
897
  : '+';
736
- result.push({
737
- pathHandle: identity?.pathHandle,
738
- name: identity?.name,
898
+ const span = {
739
899
  strand: identity
740
900
  ? identity.orientation === 'forward'
741
901
  ? '+'
742
902
  : '-'
743
- : strand,
744
- hapStart: identity
745
- ? identity.name.fragment + identity.hapStart
746
- : undefined,
747
- hapEnd: identity ? identity.name.fragment + identity.hapEnd : undefined,
748
- start: info.positions[0],
903
+ : walkStrand,
749
904
  refStart: reference.start + leading,
750
905
  refEnd: reference.start + refTotal - trailing,
751
906
  cigar: edits
@@ -754,11 +909,115 @@ export class Subgraph {
754
909
  .join(''),
755
910
  weight: info.weight,
756
911
  path: info.path,
757
- });
912
+ start: info.positions[0],
913
+ };
914
+ if (identity) {
915
+ const hapStart = identity.name.fragment + identity.hapStart;
916
+ const hapEnd = identity.name.fragment + identity.hapEnd;
917
+ result.push({
918
+ ...span,
919
+ resolved: true,
920
+ name: identity.name,
921
+ label: formatPathName({ ...identity.name, fragment: hapStart }, hapEnd),
922
+ pathHandle: identity.pathHandle,
923
+ hapStart,
924
+ hapEnd,
925
+ });
926
+ }
927
+ else {
928
+ result.push({ ...span, resolved: false });
929
+ }
758
930
  });
759
931
  return result;
760
932
  }
761
- toJSON(cigar, opts = {}) {
933
+ canonicalEdges(id) {
934
+ const edges = [];
935
+ for (const orientation of ['forward', 'reverse']) {
936
+ const handle = encodeNode(id, orientation);
937
+ for (const successor of this.record(handle).successors()) {
938
+ if (this.hasHandle(successor) && edgeIsCanonical(handle, successor)) {
939
+ edges.push([
940
+ orientation === 'reverse' ? 1 : 0,
941
+ nodeId(successor),
942
+ isReverse(successor) ? 1 : 0,
943
+ ]);
944
+ }
945
+ }
946
+ }
947
+ edges.sort((a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2]);
948
+ return edges.filter((edge, i) => i === 0 ||
949
+ edge[0] !== edges[i - 1][0] ||
950
+ edge[1] !== edges[i - 1][1] ||
951
+ edge[2] !== edges[i - 1][2]);
952
+ }
953
+ async stableName() {
954
+ const encoder = new TextEncoder();
955
+ const chunks = [];
956
+ for (const handle of this.sortedHandles()) {
957
+ if (!isReverse(handle)) {
958
+ const id = nodeId(handle);
959
+ let text = `S\t${id}\t${this.record(handle).sequence}\n`;
960
+ for (const [fromReverse, toId, toReverse] of this.canonicalEdges(id)) {
961
+ text += `L\t${id}\t${fromReverse ? '-' : '+'}\t${toId}\t${toReverse ? '-' : '+'}\n`;
962
+ }
963
+ chunks.push(encoder.encode(text));
964
+ }
965
+ }
966
+ return sha256Hex(chunks);
967
+ }
968
+ async toGFA(opts = {}) {
969
+ const cigar = opts.cigar ?? false;
970
+ const lines = [
971
+ this.refPath ? `H\tVN:Z:1.1\tRS:Z:${this.refPath.sample}` : 'H\tVN:Z:1.1',
972
+ ...gfaHeaderLines(subgraphName(await this.stableName(), await this.db.graphName())),
973
+ ];
974
+ const handles = this.sortedHandles();
975
+ for (const handle of handles) {
976
+ if (!isReverse(handle)) {
977
+ lines.push(`S\t${nodeId(handle)}\t${this.record(handle).sequence}`);
978
+ }
979
+ }
980
+ const sign = (handle) => (isReverse(handle) ? '-' : '+');
981
+ for (const handle of handles) {
982
+ for (const successor of this.record(handle).successors()) {
983
+ if (this.hasHandle(successor) && edgeIsCanonical(handle, successor)) {
984
+ lines.push(`L\t${nodeId(handle)}\t${sign(handle)}\t${nodeId(successor)}\t${sign(successor)}\t0M`);
985
+ }
986
+ }
987
+ }
988
+ const walk = (info, name, end, cigarString) => {
989
+ const steps = info.path
990
+ .map(handle => `${isReverse(handle) ? '<' : '>'}${nodeId(handle)}`)
991
+ .join('');
992
+ const weight = info.weight === undefined ? '' : `\tWT:i:${info.weight}`;
993
+ const cg = cigarString === undefined ? '' : `\tCG:Z:${cigarString}`;
994
+ return `W\t${name.sample}\t${name.haplotype}\t${name.contig}\t${name.fragment}\t${end}\t${steps}${weight}${cg}`;
995
+ };
996
+ const contig = this.refPath?.contig ?? 'unknown';
997
+ if (this.refId !== undefined && this.refPath && this.refInterval) {
998
+ lines.push(walk(this.paths[this.refId], {
999
+ ...this.refPath,
1000
+ fragment: this.refPath.fragment + this.refInterval[0],
1001
+ }, this.refPath.fragment + this.refInterval[1], undefined));
1002
+ }
1003
+ let haplotype = 1;
1004
+ this.paths.forEach((info, index) => {
1005
+ if (index !== this.refId) {
1006
+ const resolved = opts.names === 'resolved' ? info.identity : undefined;
1007
+ const cigarString = cigar ? this.alignToRef(index) : undefined;
1008
+ lines.push(resolved
1009
+ ? walk(info, {
1010
+ ...resolved.name,
1011
+ fragment: resolved.name.fragment + resolved.hapStart,
1012
+ }, resolved.name.fragment + resolved.hapEnd, cigarString)
1013
+ : walk(info, { sample: 'unknown', contig, haplotype, fragment: 0 }, info.len, cigarString));
1014
+ haplotype += 1;
1015
+ }
1016
+ });
1017
+ return `${lines.join('\n')}\n`;
1018
+ }
1019
+ toSubgraphJson(opts = {}) {
1020
+ const cigar = opts.cigar ?? false;
762
1021
  const handles = this.sortedHandles();
763
1022
  const nodes = handles
764
1023
  .filter(handle => !isReverse(handle))