@gmod/gbz-base 1.1.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/src/query.ts CHANGED
@@ -1,29 +1,18 @@
1
- import { GENERIC_SAMPLE } from './db.ts'
1
+ import { pathNameFor } from './pathName.ts'
2
2
  import { Subgraph } from './subgraph.ts'
3
3
 
4
- import type { GBZBase, PathName } from './db.ts'
4
+ import type { GBZBase } from './db.ts'
5
+ import type { PathQuery } from './pathName.ts'
5
6
  import type { HaplotypeOutput, SnarlOutput } from './subgraph.ts'
6
7
 
7
- export interface QueryOptions {
8
- context?: number
9
- haplotypes?: HaplotypeOutput
10
- snarls?: SnarlOutput
11
- limit?: number
12
- }
13
-
14
- export interface PathQuery {
15
- sample?: string
16
- contig: string
17
- haplotype?: number
18
- }
8
+ export type { PathQuery } from './pathName.ts'
19
9
 
20
- function pathName(query: PathQuery, fragment: number): PathName {
21
- return {
22
- sample: query.sample ?? GENERIC_SAMPLE,
23
- contig: query.contig,
24
- haplotype: query.haplotype ?? 0,
25
- fragment,
26
- }
10
+ export interface QueryOptions {
11
+ context?: number | undefined
12
+ haplotypes?: HaplotypeOutput | undefined
13
+ snarls?: SnarlOutput | undefined
14
+ limit?: number | undefined
15
+ signal?: AbortSignal | undefined
27
16
  }
28
17
 
29
18
  export async function subgraphAtOffset(
@@ -32,9 +21,8 @@ export async function subgraphAtOffset(
32
21
  offset: number,
33
22
  opts: QueryOptions = {},
34
23
  ) {
35
- const subgraph = new Subgraph(db)
36
- subgraph.limit = opts.limit
37
- const reference = await subgraph.pathPosition(pathName(query, offset))
24
+ const subgraph = new Subgraph(db, opts)
25
+ const reference = await subgraph.pathPosition(pathNameFor(query, offset))
38
26
  await subgraph.aroundPosition(
39
27
  reference.position.handle,
40
28
  reference.position.nodeOffset,
@@ -52,9 +40,8 @@ export async function subgraphInInterval(
52
40
  end: number,
53
41
  opts: QueryOptions = {},
54
42
  ) {
55
- const subgraph = new Subgraph(db)
56
- subgraph.limit = opts.limit
57
- const reference = await subgraph.pathPosition(pathName(query, start))
43
+ const subgraph = new Subgraph(db, opts)
44
+ const reference = await subgraph.pathPosition(pathNameFor(query, start))
58
45
  await subgraph.aroundInterval(
59
46
  reference.position,
60
47
  end - start,
@@ -80,8 +67,7 @@ export async function subgraphAroundNodes(
80
67
  'Overlapping snarls cannot be extracted for a node-based query with multiple nodes',
81
68
  )
82
69
  }
83
- const subgraph = new Subgraph(db)
84
- subgraph.limit = opts.limit
70
+ const subgraph = new Subgraph(db, opts)
85
71
  await subgraph.aroundNodes(nodes, opts.context ?? 100)
86
72
  await subgraph.extractSnarls(snarls)
87
73
  subgraph.extractPaths(undefined, haplotypes)
@@ -92,14 +78,13 @@ export async function subgraphBetween(
92
78
  db: GBZBase,
93
79
  start: number,
94
80
  end: number,
95
- opts: Pick<QueryOptions, 'haplotypes' | 'limit'> = {},
81
+ opts: Pick<QueryOptions, 'haplotypes' | 'limit' | 'signal'> = {},
96
82
  ) {
97
83
  const haplotypes = opts.haplotypes ?? 'all'
98
84
  if (haplotypes === 'reference-only') {
99
85
  throw new Error('Cannot output a reference path in a node-based query')
100
86
  }
101
- const subgraph = new Subgraph(db)
102
- subgraph.limit = opts.limit
87
+ const subgraph = new Subgraph(db, opts)
103
88
  await subgraph.betweenNodes(start, end)
104
89
  subgraph.extractPaths(undefined, haplotypes)
105
90
  return subgraph
@@ -15,8 +15,20 @@ interface PageHeader {
15
15
  cellPointers: number
16
16
  }
17
17
 
18
+ function readUint16(page: Uint8Array, offset: number) {
19
+ return page[offset]! * 256 + page[offset + 1]!
20
+ }
21
+
22
+ function readUint32(page: Uint8Array, offset: number) {
23
+ return (
24
+ page[offset]! * 16777216 +
25
+ page[offset + 1]! * 65536 +
26
+ page[offset + 2]! * 256 +
27
+ page[offset + 3]!
28
+ )
29
+ }
30
+
18
31
  function readHeader(page: Uint8Array, start: number): PageHeader {
19
- const view = new DataView(page.buffer, page.byteOffset, page.byteLength)
20
32
  const type = page[start]
21
33
  if (
22
34
  type !== INTERIOR_INDEX &&
@@ -29,25 +41,27 @@ function readHeader(page: Uint8Array, start: number): PageHeader {
29
41
  const interior = type === INTERIOR_INDEX || type === INTERIOR_TABLE
30
42
  return {
31
43
  type,
32
- cellCount: view.getUint16(start + 3),
33
- rightChild: interior ? view.getUint32(start + 8) : 0,
44
+ cellCount: readUint16(page, start + 3),
45
+ rightChild: interior ? readUint32(page, start + 8) : 0,
34
46
  cellPointers: start + (interior ? 12 : 8),
35
47
  }
36
48
  }
37
49
 
38
50
  function cellOffset(page: Uint8Array, header: PageHeader, index: number) {
39
- const view = new DataView(page.buffer, page.byteOffset, page.byteLength)
40
- return view.getUint16(header.cellPointers + 2 * index)
51
+ return readUint16(page, header.cellPointers + 2 * index)
41
52
  }
42
53
 
43
- function readUint32(page: Uint8Array, offset: number) {
44
- const view = new DataView(page.buffer, page.byteOffset, page.byteLength)
45
- return view.getUint32(offset)
54
+ interface IndexCell {
55
+ values: SqlValue[]
56
+ leftChild: number
46
57
  }
47
58
 
59
+ const MAX_DECODED_INDEX_PAGES = 4096
60
+
48
61
  export class BTree {
49
62
  private readonly usable: number
50
63
  private pager: Pager
64
+ private decodedIndexPages = new Map<number, (IndexCell | undefined)[]>()
51
65
 
52
66
  constructor(pager: Pager, reservedBytes: number) {
53
67
  this.pager = pager
@@ -179,12 +193,42 @@ export class BTree {
179
193
  }
180
194
  }
181
195
 
182
- private async indexCell(page: Uint8Array, header: PageHeader, index: number) {
183
- const offset = cellOffset(page, header, index)
184
- const interior = header.type === INTERIOR_INDEX
185
- const [size, afterSize] = readVarint(page, interior ? offset + 4 : offset)
186
- const values = decodeRecord(await this.payload(page, afterSize, size, true))
187
- return { values, leftChild: interior ? readUint32(page, offset) : 0 }
196
+ private decodedCells(pageNumber: number, header: PageHeader) {
197
+ let cells = this.decodedIndexPages.get(pageNumber)
198
+ if (cells) {
199
+ this.decodedIndexPages.delete(pageNumber)
200
+ } else {
201
+ cells = new Array<IndexCell | undefined>(header.cellCount)
202
+ if (this.decodedIndexPages.size >= MAX_DECODED_INDEX_PAGES) {
203
+ const oldest = this.decodedIndexPages.keys().next().value
204
+ if (oldest !== undefined) {
205
+ this.decodedIndexPages.delete(oldest)
206
+ }
207
+ }
208
+ }
209
+ this.decodedIndexPages.set(pageNumber, cells)
210
+ return cells
211
+ }
212
+
213
+ private async indexCell(
214
+ pageNumber: number,
215
+ page: Uint8Array,
216
+ header: PageHeader,
217
+ index: number,
218
+ ): Promise<IndexCell> {
219
+ const cells = this.decodedCells(pageNumber, header)
220
+ let cell = cells[index]
221
+ if (!cell) {
222
+ const offset = cellOffset(page, header, index)
223
+ const interior = header.type === INTERIOR_INDEX
224
+ const [size, afterSize] = readVarint(page, interior ? offset + 4 : offset)
225
+ cell = {
226
+ values: decodeRecord(await this.payload(page, afterSize, size, true)),
227
+ leftChild: interior ? readUint32(page, offset) : 0,
228
+ }
229
+ cells[index] = cell
230
+ }
231
+ return cell
188
232
  }
189
233
 
190
234
  async *indexScanFrom(
@@ -196,7 +240,7 @@ export class BTree {
196
240
  let high = header.cellCount
197
241
  while (first < high) {
198
242
  const mid = (first + high) >> 1
199
- const cell = await this.indexCell(page, header, mid)
243
+ const cell = await this.indexCell(root, page, header, mid)
200
244
  if (compareKey(cell.values, low) < 0) {
201
245
  first = mid + 1
202
246
  } else {
@@ -204,7 +248,7 @@ export class BTree {
204
248
  }
205
249
  }
206
250
  for (let i = first; i < header.cellCount; i++) {
207
- const cell = await this.indexCell(page, header, i)
251
+ const cell = await this.indexCell(root, page, header, i)
208
252
  if (header.type === INTERIOR_INDEX) {
209
253
  yield* this.indexScanFrom(cell.leftChild, low)
210
254
  }
@@ -228,7 +272,7 @@ export class BTree {
228
272
  let child = 0
229
273
  while (low < high) {
230
274
  const mid = (low + high) >> 1
231
- const cell = await this.indexCell(page, header, mid)
275
+ const cell = await this.indexCell(pageNumber, page, header, mid)
232
276
  if (compareKey(cell.values, key) <= 0) {
233
277
  best = cell.values
234
278
  low = mid + 1
package/src/subgraph.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { formatPathName } from './db.ts'
2
1
  import {
3
2
  ENDMARKER,
4
3
  edgeIsCanonical,
@@ -16,16 +15,12 @@ import {
16
15
  } from './gbwt/node.ts'
17
16
  import { gfaHeaderLines, sha256Hex, subgraphName } from './graphName.ts'
18
17
  import { weightedLcs } from './lcs.ts'
18
+ import { formatPathName } from './pathName.ts'
19
19
 
20
- import type {
21
- GBZBase,
22
- GbzPath,
23
- GbzRecord,
24
- HaplotypeSample,
25
- PathName,
26
- } from './db.ts'
20
+ import type { GBZBase, GbzPath, GbzRecord, HaplotypeSample } from './db.ts'
27
21
  import type { NodeSide, Orientation } from './gbwt/node.ts'
28
22
  import type { Pos } from './gbwt/record.ts'
23
+ import type { PathName } from './pathName.ts'
29
24
 
30
25
  export type HaplotypeOutput = 'all' | 'distinct' | 'reference-only' | 'none'
31
26
 
@@ -86,12 +81,8 @@ export interface SubgraphJson {
86
81
  paths: SubgraphPath[]
87
82
  }
88
83
 
89
- export interface HaplotypeAlignment {
90
- pathHandle: number | undefined
91
- name: PathName | undefined
84
+ export interface AlignmentSpan {
92
85
  strand: '+' | '-'
93
- hapStart: number | undefined
94
- hapEnd: number | undefined
95
86
  refStart: number
96
87
  refEnd: number
97
88
  cigar: string
@@ -100,8 +91,27 @@ export interface HaplotypeAlignment {
100
91
  start: Pos
101
92
  }
102
93
 
103
- export interface ToJsonOptions {
104
- names?: 'anonymous' | 'resolved'
94
+ export type HaplotypeAlignment = AlignmentSpan &
95
+ (
96
+ | {
97
+ resolved: true
98
+ name: PathName
99
+ label: string
100
+ pathHandle: number
101
+ hapStart: number
102
+ hapEnd: number
103
+ }
104
+ | { resolved: false }
105
+ )
106
+
107
+ export interface SubgraphOutputOptions {
108
+ cigar?: boolean | undefined
109
+ names?: 'anonymous' | 'resolved' | undefined
110
+ }
111
+
112
+ export interface SubgraphOptions {
113
+ limit?: number | undefined
114
+ signal?: AbortSignal | undefined
105
115
  }
106
116
 
107
117
  function sideBefore(
@@ -183,7 +193,6 @@ export class Subgraph {
183
193
  private refInterval: [number, number] | undefined
184
194
  private refIndexCache: Map<number, number[]> | undefined
185
195
  private refPrefixCache: number[] | undefined
186
- limit: number | undefined
187
196
  readonly stats = {
188
197
  orderedAlignments: 0,
189
198
  lcsAlignments: 0,
@@ -192,9 +201,13 @@ export class Subgraph {
192
201
  }
193
202
 
194
203
  private db: GBZBase
204
+ private readonly limit: number | undefined
205
+ private readonly signal: AbortSignal | undefined
195
206
 
196
- constructor(db: GBZBase) {
207
+ constructor(db: GBZBase, opts: SubgraphOptions = {}) {
197
208
  this.db = db
209
+ this.limit = opts.limit
210
+ this.signal = opts.signal
198
211
  }
199
212
 
200
213
  get nodeCount() {
@@ -236,6 +249,7 @@ export class Subgraph {
236
249
  }
237
250
 
238
251
  private async addNode(id: number) {
252
+ this.signal?.throwIfAborted()
239
253
  if (this.limit !== undefined && this.nodeCount >= this.limit) {
240
254
  throw new Error(`Subgraph size limit of ${this.limit} nodes exceeded`)
241
255
  }
@@ -800,6 +814,7 @@ export class Subgraph {
800
814
  let current: number | undefined = start
801
815
  let pos: Pos | undefined
802
816
  while (anchor === undefined) {
817
+ this.signal?.throwIfAborted()
803
818
  if (current !== undefined) {
804
819
  if (visited.has(current)) {
805
820
  break
@@ -1094,24 +1109,17 @@ export class Subgraph {
1094
1109
  last -= 1
1095
1110
  }
1096
1111
  const identity = info.identity
1097
- const strand =
1112
+ const walkStrand =
1098
1113
  info.path.some(handle => isReverse(handle)) &&
1099
1114
  !info.path.some(handle => !isReverse(handle))
1100
1115
  ? '-'
1101
1116
  : '+'
1102
- result.push({
1103
- pathHandle: identity?.pathHandle,
1104
- name: identity?.name,
1117
+ const span: AlignmentSpan = {
1105
1118
  strand: identity
1106
1119
  ? identity.orientation === 'forward'
1107
1120
  ? '+'
1108
1121
  : '-'
1109
- : strand,
1110
- hapStart: identity
1111
- ? identity.name.fragment + identity.hapStart
1112
- : undefined,
1113
- hapEnd: identity ? identity.name.fragment + identity.hapEnd : undefined,
1114
- start: info.positions[0]!,
1122
+ : walkStrand,
1115
1123
  refStart: reference.start + leading,
1116
1124
  refEnd: reference.start + refTotal - trailing,
1117
1125
  cigar: edits
@@ -1120,7 +1128,26 @@ export class Subgraph {
1120
1128
  .join(''),
1121
1129
  weight: info.weight,
1122
1130
  path: info.path,
1123
- })
1131
+ start: info.positions[0]!,
1132
+ }
1133
+ if (identity) {
1134
+ const hapStart = identity.name.fragment + identity.hapStart
1135
+ const hapEnd = identity.name.fragment + identity.hapEnd
1136
+ result.push({
1137
+ ...span,
1138
+ resolved: true,
1139
+ name: identity.name,
1140
+ label: formatPathName(
1141
+ { ...identity.name, fragment: hapStart },
1142
+ hapEnd,
1143
+ ),
1144
+ pathHandle: identity.pathHandle,
1145
+ hapStart,
1146
+ hapEnd,
1147
+ })
1148
+ } else {
1149
+ result.push({ ...span, resolved: false })
1150
+ }
1124
1151
  })
1125
1152
  return result
1126
1153
  }
@@ -1165,7 +1192,8 @@ export class Subgraph {
1165
1192
  return sha256Hex(chunks)
1166
1193
  }
1167
1194
 
1168
- async toGFA(cigar: boolean, opts: ToJsonOptions = {}) {
1195
+ async toGFA(opts: SubgraphOutputOptions = {}) {
1196
+ const cigar = opts.cigar ?? false
1169
1197
  const lines = [
1170
1198
  this.refPath ? `H\tVN:Z:1.1\tRS:Z:${this.refPath.sample}` : 'H\tVN:Z:1.1',
1171
1199
  ...gfaHeaderLines(
@@ -1244,7 +1272,8 @@ export class Subgraph {
1244
1272
  return `${lines.join('\n')}\n`
1245
1273
  }
1246
1274
 
1247
- toJSON(cigar: boolean, opts: ToJsonOptions = {}): SubgraphJson {
1275
+ toSubgraphJson(opts: SubgraphOutputOptions = {}): SubgraphJson {
1276
+ const cigar = opts.cigar ?? false
1248
1277
  const handles = this.sortedHandles()
1249
1278
  const nodes = handles
1250
1279
  .filter(handle => !isReverse(handle))
@@ -208,15 +208,22 @@ fn orientations(args: &Args) -> Vec<Orientation> {
208
208
  }
209
209
  }
210
210
 
211
- fn walk_paths(source: &dyn PathSource, handles: std::ops::Range<usize>, args: &Args) -> (Vec<Sample>, Vec<(usize, usize)>) {
211
+ fn walk_paths(source: &dyn PathSource, handles: std::ops::Range<usize>, args: &Args, label: &str) -> (Vec<Sample>, Vec<(usize, usize)>) {
212
212
  let mut samples = Vec::new();
213
213
  let mut lengths = Vec::new();
214
- for path_handle in handles {
214
+ let started = Instant::now();
215
+ let total = handles.len();
216
+ let mut walked_bp: usize = 0;
217
+ for (done, path_handle) in handles.enumerate() {
215
218
  let mut length = 0;
216
219
  for &orientation in orientations(args).iter() {
217
220
  length = walk(source, path_handle, orientation, args.interval, &mut samples);
218
221
  }
222
+ walked_bp += length;
219
223
  lengths.push((path_handle, length));
224
+ if (done + 1) % 500 == 0 || done + 1 == total {
225
+ eprintln!("{}: {} / {} paths, {:.2} Gbp, {} samples, {:.0} s", label, done + 1, total, walked_bp as f64 / 1e9, samples.len(), started.elapsed().as_secs_f64());
226
+ }
220
227
  }
221
228
  (samples, lengths)
222
229
  }
@@ -230,8 +237,9 @@ fn walk_gbz(graph: &GBZ, paths: usize, args: &Args) -> (Vec<Sample>, Vec<(usize,
230
237
  let range = (t * chunk).min(paths)..((t + 1) * chunk).min(paths);
231
238
  scope.spawn(move || {
232
239
  let source = GbzSource { graph };
233
- let result = walk_paths(&source, range.clone(), args);
234
- eprintln!("thread {} walked paths {}..{} ({} samples) in {:.0} s", t, range.start, range.end, result.0.len(), started.elapsed().as_secs_f64());
240
+ let label = format!("thread {} (paths {}..{})", t, range.start, range.end);
241
+ let result = walk_paths(&source, range.clone(), args, &label);
242
+ eprintln!("{} done in {:.0} s", label, started.elapsed().as_secs_f64());
235
243
  result
236
244
  })
237
245
  })
@@ -341,7 +349,7 @@ fn main() {
341
349
  });
342
350
  let interface = GraphInterface::new(&database).unwrap();
343
351
  let source = DbSource { interface: std::cell::RefCell::new(interface) };
344
- let (samples, lengths) = walk_paths(&source, 0..paths, &args);
352
+ let (samples, lengths) = walk_paths(&source, 0..paths, &args, "database walk");
345
353
  (samples, lengths, paths)
346
354
  }
347
355
  };