@gmod/gbz-base 1.1.0 → 2.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/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
@@ -1030,13 +1045,19 @@ export class Subgraph {
1030
1045
  appendEdit(edits, 'M', suffix)
1031
1046
  }
1032
1047
 
1033
- private edits(pathIndex: number): Edit[] | undefined {
1034
- const info = this.paths[pathIndex]
1035
- if (this.refId === undefined || pathIndex === this.refId || !info) {
1036
- return undefined
1048
+ private sharedWeight(path: number[], ref: number[]) {
1049
+ const index = this.refIndex(ref)
1050
+ let weight = 0
1051
+ for (const handle of path) {
1052
+ if (index.has(handle)) {
1053
+ weight += this.record(handle).sequenceLen
1054
+ }
1037
1055
  }
1038
- const ref = this.paths[this.refId]!.path
1039
- const ordered = this.orderedMatches(info.path, ref)
1056
+ return weight
1057
+ }
1058
+
1059
+ private editsAgainst(path: number[], ref: number[]) {
1060
+ const ordered = this.orderedMatches(path, ref)
1040
1061
  if (ordered) {
1041
1062
  this.stats.orderedAlignments += 1
1042
1063
  } else {
@@ -1044,27 +1065,63 @@ export class Subgraph {
1044
1065
  }
1045
1066
  const lcs =
1046
1067
  ordered ??
1047
- weightedLcs(info.path, ref, handle => this.record(handle).sequenceLen)[0]
1068
+ weightedLcs(path, ref, handle => this.record(handle).sequenceLen)[0]
1048
1069
  const edits: Edit[] = []
1070
+ let matched = 0
1049
1071
  let pathOffset = 0
1050
1072
  let refOffset = 0
1051
1073
  for (const [nextPath, nextRef] of lcs) {
1052
1074
  this.align(
1053
- info.path.slice(pathOffset, nextPath),
1075
+ path.slice(pathOffset, nextPath),
1054
1076
  ref.slice(refOffset, nextRef),
1055
1077
  edits,
1056
1078
  )
1057
- appendEdit(edits, 'M', this.record(info.path[nextPath]!).sequenceLen)
1079
+ const nodeLen = this.record(path[nextPath]!).sequenceLen
1080
+ appendEdit(edits, 'M', nodeLen)
1081
+ matched += nodeLen
1058
1082
  pathOffset = nextPath + 1
1059
1083
  refOffset = nextRef + 1
1060
1084
  }
1061
- this.align(info.path.slice(pathOffset), ref.slice(refOffset), edits)
1062
- return edits
1085
+ this.align(path.slice(pathOffset), ref.slice(refOffset), edits)
1086
+ return { edits, matched }
1087
+ }
1088
+
1089
+ private alignment(pathIndex: number) {
1090
+ const info = this.paths[pathIndex]
1091
+ if (this.refId === undefined || pathIndex === this.refId || !info) {
1092
+ return undefined
1093
+ }
1094
+ const ref = this.paths[this.refId]!.path
1095
+ const flippedPath = pathIsCanonical(ref)
1096
+ ? []
1097
+ : info.path.map(handle => flipNode(handle)).reverse()
1098
+ const forwardBound = this.sharedWeight(info.path, ref)
1099
+ const flippedBound = this.sharedWeight(flippedPath, ref)
1100
+ let result: { edits: Edit[]; flipped: boolean }
1101
+ if (flippedBound === 0) {
1102
+ result = {
1103
+ edits: this.editsAgainst(info.path, ref).edits,
1104
+ flipped: false,
1105
+ }
1106
+ } else if (forwardBound === 0) {
1107
+ result = {
1108
+ edits: this.editsAgainst(flippedPath, ref).edits,
1109
+ flipped: true,
1110
+ }
1111
+ } else {
1112
+ const forward = this.editsAgainst(info.path, ref)
1113
+ const flipped = this.editsAgainst(flippedPath, ref)
1114
+ result =
1115
+ flipped.matched > forward.matched
1116
+ ? { edits: flipped.edits, flipped: true }
1117
+ : { edits: forward.edits, flipped: false }
1118
+ }
1119
+ return result
1063
1120
  }
1064
1121
 
1065
1122
  alignToRef(pathIndex: number) {
1066
- return this.edits(pathIndex)
1067
- ?.map(([op, len]) => `${len}${op}`)
1123
+ return this.alignment(pathIndex)
1124
+ ?.edits.map(([op, len]) => `${len}${op}`)
1068
1125
  .join('')
1069
1126
  }
1070
1127
 
@@ -1080,7 +1137,7 @@ export class Subgraph {
1080
1137
  if (index === this.refId) {
1081
1138
  return
1082
1139
  }
1083
- const edits = this.edits(index)!
1140
+ const { edits, flipped } = this.alignment(index)!
1084
1141
  let first = 0
1085
1142
  let leading = 0
1086
1143
  while (first < edits.length && edits[first]![0] === 'D') {
@@ -1094,24 +1151,11 @@ export class Subgraph {
1094
1151
  last -= 1
1095
1152
  }
1096
1153
  const identity = info.identity
1097
- const strand =
1098
- info.path.some(handle => isReverse(handle)) &&
1099
- !info.path.some(handle => !isReverse(handle))
1100
- ? '-'
1101
- : '+'
1102
- result.push({
1103
- pathHandle: identity?.pathHandle,
1104
- name: identity?.name,
1105
- strand: identity
1106
- ? identity.orientation === 'forward'
1107
- ? '+'
1108
- : '-'
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]!,
1154
+ const alongReference = identity
1155
+ ? (identity.orientation === 'forward') !== flipped
1156
+ : !flipped
1157
+ const span: AlignmentSpan = {
1158
+ strand: alongReference ? '+' : '-',
1115
1159
  refStart: reference.start + leading,
1116
1160
  refEnd: reference.start + refTotal - trailing,
1117
1161
  cigar: edits
@@ -1120,7 +1164,26 @@ export class Subgraph {
1120
1164
  .join(''),
1121
1165
  weight: info.weight,
1122
1166
  path: info.path,
1123
- })
1167
+ start: info.positions[0]!,
1168
+ }
1169
+ if (identity) {
1170
+ const hapStart = identity.name.fragment + identity.hapStart
1171
+ const hapEnd = identity.name.fragment + identity.hapEnd
1172
+ result.push({
1173
+ ...span,
1174
+ resolved: true,
1175
+ name: identity.name,
1176
+ label: formatPathName(
1177
+ { ...identity.name, fragment: hapStart },
1178
+ hapEnd,
1179
+ ),
1180
+ pathHandle: identity.pathHandle,
1181
+ hapStart,
1182
+ hapEnd,
1183
+ })
1184
+ } else {
1185
+ result.push({ ...span, resolved: false })
1186
+ }
1124
1187
  })
1125
1188
  return result
1126
1189
  }
@@ -1165,7 +1228,8 @@ export class Subgraph {
1165
1228
  return sha256Hex(chunks)
1166
1229
  }
1167
1230
 
1168
- async toGFA(cigar: boolean, opts: ToJsonOptions = {}) {
1231
+ async toGFA(opts: SubgraphOutputOptions = {}) {
1232
+ const cigar = opts.cigar ?? false
1169
1233
  const lines = [
1170
1234
  this.refPath ? `H\tVN:Z:1.1\tRS:Z:${this.refPath.sample}` : 'H\tVN:Z:1.1',
1171
1235
  ...gfaHeaderLines(
@@ -1244,7 +1308,8 @@ export class Subgraph {
1244
1308
  return `${lines.join('\n')}\n`
1245
1309
  }
1246
1310
 
1247
- toJSON(cigar: boolean, opts: ToJsonOptions = {}): SubgraphJson {
1311
+ toSubgraphJson(opts: SubgraphOutputOptions = {}): SubgraphJson {
1312
+ const cigar = opts.cigar ?? false
1248
1313
  const handles = this.sortedHandles()
1249
1314
  const nodes = handles
1250
1315
  .filter(handle => !isReverse(handle))
@@ -21,7 +21,8 @@ HaplotypeLengths. The path start and end are always sampled.
21
21
  By default the tables are written into the database itself, replacing any
22
22
  existing ones. With --output FILE they are written into FILE as a standalone
23
23
  companion database that the reader opens beside the graph database; the
24
- companion records the graph's path count so a mismatch is caught at open.
24
+ companion records the graph's path and node counts so a mismatch is caught at
25
+ open.
25
26
 
26
27
  With --from-db the walk reads node records from the database itself, so the
27
28
  GBZ is not needed. Walking a GBZ uses --threads (default: all cores).
@@ -208,15 +209,22 @@ fn orientations(args: &Args) -> Vec<Orientation> {
208
209
  }
209
210
  }
210
211
 
211
- fn walk_paths(source: &dyn PathSource, handles: std::ops::Range<usize>, args: &Args) -> (Vec<Sample>, Vec<(usize, usize)>) {
212
+ fn walk_paths(source: &dyn PathSource, handles: std::ops::Range<usize>, args: &Args, label: &str) -> (Vec<Sample>, Vec<(usize, usize)>) {
212
213
  let mut samples = Vec::new();
213
214
  let mut lengths = Vec::new();
214
- for path_handle in handles {
215
+ let started = Instant::now();
216
+ let total = handles.len();
217
+ let mut walked_bp: usize = 0;
218
+ for (done, path_handle) in handles.enumerate() {
215
219
  let mut length = 0;
216
220
  for &orientation in orientations(args).iter() {
217
221
  length = walk(source, path_handle, orientation, args.interval, &mut samples);
218
222
  }
223
+ walked_bp += length;
219
224
  lengths.push((path_handle, length));
225
+ if (done + 1) % 500 == 0 || done + 1 == total {
226
+ eprintln!("{}: {} / {} paths, {:.2} Gbp, {} samples, {:.0} s", label, done + 1, total, walked_bp as f64 / 1e9, samples.len(), started.elapsed().as_secs_f64());
227
+ }
220
228
  }
221
229
  (samples, lengths)
222
230
  }
@@ -230,8 +238,9 @@ fn walk_gbz(graph: &GBZ, paths: usize, args: &Args) -> (Vec<Sample>, Vec<(usize,
230
238
  let range = (t * chunk).min(paths)..((t + 1) * chunk).min(paths);
231
239
  scope.spawn(move || {
232
240
  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());
241
+ let label = format!("thread {} (paths {}..{})", t, range.start, range.end);
242
+ let result = walk_paths(&source, range.clone(), args, &label);
243
+ eprintln!("{} done in {:.0} s", label, started.elapsed().as_secs_f64());
235
244
  result
236
245
  })
237
246
  })
@@ -261,7 +270,7 @@ CREATE TABLE HaplotypeLengths (
261
270
  length INTEGER NOT NULL
262
271
  ) STRICT;";
263
272
 
264
- fn write(target: &str, standalone: bool, mut samples: Vec<Sample>, lengths: &[(usize, usize)], paths: usize, args: &Args) {
273
+ fn write(target: &str, standalone: bool, mut samples: Vec<Sample>, lengths: &[(usize, usize)], paths: usize, nodes: usize, args: &Args) {
265
274
  let started = Instant::now();
266
275
  samples.sort_unstable_by_key(|s| (s.node_handle, s.node_offset));
267
276
  eprintln!("Sorted {} samples in {:.0} s", samples.len(), started.elapsed().as_secs_f64());
@@ -294,22 +303,23 @@ fn write(target: &str, standalone: bool, mut samples: Vec<Sample>, lengths: &[(u
294
303
  write_tag.execute(params!["haplotype_index_interval", args.interval.to_string()]).unwrap();
295
304
  write_tag.execute(params!["haplotype_index_orientations", if args.forward_only { "forward" } else { "both" }]).unwrap();
296
305
  write_tag.execute(params!["haplotype_index_paths", paths.to_string()]).unwrap();
306
+ write_tag.execute(params!["haplotype_index_nodes", nodes.to_string()]).unwrap();
297
307
  }
298
308
  transaction.commit().unwrap();
299
309
  eprintln!("Wrote {} samples for {} paths to {} in {:.0} s", samples.len(), paths, target, started.elapsed().as_secs_f64());
300
310
  }
301
311
 
302
- fn path_count_from_db(db: &str) -> usize {
312
+ fn count_from_db(db: &str, key: &str) -> usize {
303
313
  let connection = Connection::open(db).unwrap();
304
314
  let value: String = connection
305
- .query_row("SELECT value FROM Tags WHERE key = 'paths'", [], |row| row.get(0))
315
+ .query_row("SELECT value FROM Tags WHERE key = ?1", params![key], |row| row.get(0))
306
316
  .unwrap_or_else(|_| "0".to_string());
307
317
  value.parse().unwrap_or(0)
308
318
  }
309
319
 
310
320
  fn main() {
311
321
  let args = parse_args();
312
- let (samples, lengths, paths) = match &args.gbz {
322
+ let (samples, lengths, paths, nodes) = match &args.gbz {
313
323
  Some(gbz) => {
314
324
  let started = Instant::now();
315
325
  let graph: GBZ = serialize::load_from(gbz).unwrap_or_else(|e| {
@@ -322,32 +332,39 @@ fn main() {
322
332
  process::exit(1);
323
333
  }
324
334
  eprintln!("Loaded {} with {} paths in {:.0} s", gbz, paths, started.elapsed().as_secs_f64());
335
+ let nodes = graph.nodes();
325
336
  if let Some(db) = &args.db {
326
- let db_paths = path_count_from_db(db);
337
+ let db_paths = count_from_db(db, "paths");
327
338
  if db_paths != paths {
328
339
  eprintln!("{} has {} paths but {} has {}", gbz, paths, db, db_paths);
329
340
  process::exit(1);
330
341
  }
342
+ let db_nodes = count_from_db(db, "nodes");
343
+ if db_nodes != nodes {
344
+ eprintln!("{} has {} nodes but {} has {}", gbz, nodes, db, db_nodes);
345
+ process::exit(1);
346
+ }
331
347
  }
332
348
  let (samples, lengths) = walk_gbz(&graph, paths, &args);
333
- (samples, lengths, paths)
349
+ (samples, lengths, paths, nodes)
334
350
  }
335
351
  None => {
336
352
  let db = args.db.as_ref().unwrap();
337
- let paths = path_count_from_db(db);
353
+ let paths = count_from_db(db, "paths");
354
+ let nodes = count_from_db(db, "nodes");
338
355
  let database = GBZBase::open(db).unwrap_or_else(|e| {
339
356
  eprintln!("Cannot open {} as a GBZ-base: {}", db, e);
340
357
  process::exit(1);
341
358
  });
342
359
  let interface = GraphInterface::new(&database).unwrap();
343
360
  let source = DbSource { interface: std::cell::RefCell::new(interface) };
344
- let (samples, lengths) = walk_paths(&source, 0..paths, &args);
345
- (samples, lengths, paths)
361
+ let (samples, lengths) = walk_paths(&source, 0..paths, &args, "database walk");
362
+ (samples, lengths, paths, nodes)
346
363
  }
347
364
  };
348
365
  match (&args.output, &args.db) {
349
- (Some(output), _) => write(output, true, samples, &lengths, paths, &args),
350
- (None, Some(db)) => write(db, false, samples, &lengths, paths, &args),
366
+ (Some(output), _) => write(output, true, samples, &lengths, paths, nodes, &args),
367
+ (None, Some(db)) => write(db, false, samples, &lengths, paths, nodes, &args),
351
368
  (None, None) => unreachable!(),
352
369
  }
353
370
  }