@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.
@@ -7,22 +7,39 @@ use simple_sds::serialize;
7
7
 
8
8
  use std::env;
9
9
  use std::process;
10
+ use std::thread;
11
+ use std::time::Instant;
10
12
 
11
- const USAGE: &str = "Usage: gbz-haplotype-index [--interval BP] [--forward-only] graph.gbz graph.gbz.db
12
- gbz-haplotype-index [--interval BP] [--forward-only] --from-db graph.gbz.db
13
+ const USAGE: &str = "Usage: gbz-haplotype-index [options] graph.gbz graph.gbz.db
14
+ gbz-haplotype-index [options] --output index.db graph.gbz [graph.gbz.db]
15
+ gbz-haplotype-index [options] --from-db graph.gbz.db
13
16
 
14
17
  Walks every path in both orientations and writes a sample every --interval bp
15
- (default 4096) into table HaplotypeSamples of the database, plus the path
16
- lengths into HaplotypeLengths. The path start and end are always sampled.
17
- Existing tables are replaced. With --from-db the walk reads node records from
18
- the database itself, so the GBZ is not needed.
18
+ (default 4096) into table HaplotypeSamples, plus the path lengths into
19
+ HaplotypeLengths. The path start and end are always sampled.
20
+
21
+ By default the tables are written into the database itself, replacing any
22
+ existing ones. With --output FILE they are written into FILE as a standalone
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.
25
+
26
+ With --from-db the walk reads node records from the database itself, so the
27
+ GBZ is not needed. Walking a GBZ uses --threads (default: all cores).
28
+
29
+ Options:
30
+ --interval BP bp between samples along a path (default 4096)
31
+ --forward-only sample only the forward orientation of each path
32
+ --output FILE write a companion database instead of augmenting graph.gbz.db
33
+ --threads N walker threads for the GBZ route
19
34
  ";
20
35
 
21
36
  struct Args {
22
37
  gbz: Option<String>,
23
- db: String,
38
+ db: Option<String>,
39
+ output: Option<String>,
24
40
  interval: usize,
25
41
  forward_only: bool,
42
+ threads: usize,
26
43
  }
27
44
 
28
45
  fn parse_args() -> Args {
@@ -30,6 +47,8 @@ fn parse_args() -> Args {
30
47
  let mut interval = 4096;
31
48
  let mut forward_only = false;
32
49
  let mut from_db = false;
50
+ let mut output = None;
51
+ let mut threads = thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
33
52
  let mut iter = env::args().skip(1);
34
53
  while let Some(arg) = iter.next() {
35
54
  match arg.as_str() {
@@ -40,6 +59,14 @@ fn parse_args() -> Args {
40
59
  process::exit(1);
41
60
  });
42
61
  }
62
+ "--threads" => {
63
+ let value = iter.next().unwrap_or_default();
64
+ threads = value.parse().unwrap_or_else(|_| {
65
+ eprintln!("Invalid --threads: {}", value);
66
+ process::exit(1);
67
+ });
68
+ }
69
+ "--output" => output = Some(iter.next().unwrap_or_default()),
43
70
  "--forward-only" => forward_only = true,
44
71
  "--from-db" => from_db = true,
45
72
  "-h" | "--help" => {
@@ -49,46 +76,52 @@ fn parse_args() -> Args {
49
76
  _ => positional.push(arg),
50
77
  }
51
78
  }
52
- let expected = if from_db { 1 } else { 2 };
53
- if positional.len() != expected || interval == 0 {
79
+ let valid = if from_db {
80
+ positional.len() == 1
81
+ } else if output.is_some() {
82
+ positional.len() == 1 || positional.len() == 2
83
+ } else {
84
+ positional.len() == 2
85
+ };
86
+ if !valid || interval == 0 || threads == 0 {
54
87
  eprint!("{}", USAGE);
55
88
  process::exit(1);
56
89
  }
57
- let db = positional.pop().unwrap();
58
- let gbz = positional.pop();
59
- Args { gbz, db, interval, forward_only }
90
+ let (gbz, db) = if from_db {
91
+ (None, positional.pop())
92
+ } else {
93
+ let gbz = positional.remove(0);
94
+ (Some(gbz), positional.pop())
95
+ };
96
+ Args { gbz, db, output, interval, forward_only, threads }
60
97
  }
61
98
 
99
+ #[derive(Clone, Copy)]
62
100
  struct Sample {
63
- node_handle: usize,
64
- node_offset: usize,
65
- path_handle: usize,
66
- orientation: usize,
67
- path_offset: usize,
101
+ node_handle: u32,
102
+ node_offset: u32,
103
+ path_handle: u32,
104
+ orientation: u8,
105
+ path_offset: u32,
68
106
  }
69
107
 
70
108
  trait PathSource {
71
- fn path_count(&mut self) -> usize;
72
- fn start(&mut self, path_handle: usize, orientation: Orientation) -> Option<Pos>;
73
- fn step(&mut self, pos: Pos) -> (usize, Option<Pos>);
74
- fn node_len(&mut self, handle: usize) -> usize;
109
+ fn start(&self, path_handle: usize, orientation: Orientation) -> Option<Pos>;
110
+ fn step(&self, pos: Pos) -> (usize, Option<Pos>);
111
+ fn node_len(&self, handle: usize) -> usize;
75
112
  }
76
113
 
77
- struct GbzSource {
78
- graph: GBZ,
114
+ struct GbzSource<'a> {
115
+ graph: &'a GBZ,
79
116
  }
80
117
 
81
- impl PathSource for GbzSource {
82
- fn path_count(&mut self) -> usize {
83
- self.graph.metadata().map(|m| m.paths()).unwrap_or(0)
84
- }
85
-
86
- fn start(&mut self, path_handle: usize, orientation: Orientation) -> Option<Pos> {
118
+ impl PathSource for GbzSource<'_> {
119
+ fn start(&self, path_handle: usize, orientation: Orientation) -> Option<Pos> {
87
120
  let index: &GBWT = self.graph.as_ref();
88
121
  index.start(support::encode_path(path_handle, orientation))
89
122
  }
90
123
 
91
- fn step(&mut self, pos: Pos) -> (usize, Option<Pos>) {
124
+ fn step(&self, pos: Pos) -> (usize, Option<Pos>) {
92
125
  let node_len = self.graph.sequence_len(support::node_id(pos.node)).unwrap();
93
126
  let index: &GBWT = self.graph.as_ref();
94
127
  let bwt: &BWT = index.as_ref();
@@ -96,42 +129,37 @@ impl PathSource for GbzSource {
96
129
  (node_len, record.lf(pos.offset))
97
130
  }
98
131
 
99
- fn node_len(&mut self, handle: usize) -> usize {
132
+ fn node_len(&self, handle: usize) -> usize {
100
133
  self.graph.sequence_len(support::node_id(handle)).unwrap()
101
134
  }
102
135
  }
103
136
 
104
137
  struct DbSource<'a> {
105
- interface: GraphInterface<'a>,
106
- paths: usize,
138
+ interface: std::cell::RefCell<GraphInterface<'a>>,
107
139
  }
108
140
 
109
- impl<'a> PathSource for DbSource<'a> {
110
- fn path_count(&mut self) -> usize {
111
- self.paths
112
- }
113
-
114
- fn start(&mut self, path_handle: usize, orientation: Orientation) -> Option<Pos> {
115
- let path = self.interface.get_path(path_handle).unwrap()?;
141
+ impl PathSource for DbSource<'_> {
142
+ fn start(&self, path_handle: usize, orientation: Orientation) -> Option<Pos> {
143
+ let path = self.interface.borrow_mut().get_path(path_handle).unwrap()?;
116
144
  Some(if orientation == Orientation::Forward { path.fw_start } else { path.rev_start })
117
145
  }
118
146
 
119
- fn step(&mut self, pos: Pos) -> (usize, Option<Pos>) {
120
- let record = self.interface.get_record(pos.node).unwrap().unwrap();
147
+ fn step(&self, pos: Pos) -> (usize, Option<Pos>) {
148
+ let record = self.interface.borrow_mut().get_record(pos.node).unwrap().unwrap();
121
149
  (record.sequence_len(), record.to_gbwt_record().lf(pos.offset))
122
150
  }
123
151
 
124
- fn node_len(&mut self, handle: usize) -> usize {
125
- self.interface.get_record(handle).unwrap().unwrap().sequence_len()
152
+ fn node_len(&self, handle: usize) -> usize {
153
+ self.interface.borrow_mut().get_record(handle).unwrap().unwrap().sequence_len()
126
154
  }
127
155
  }
128
156
 
129
- fn walk(source: &mut dyn PathSource, path_handle: usize, orientation: Orientation, interval: usize) -> (Vec<Sample>, usize) {
130
- let mut samples = Vec::new();
157
+ fn walk(source: &dyn PathSource, path_handle: usize, orientation: Orientation, interval: usize, samples: &mut Vec<Sample>) -> usize {
131
158
  let mut pos = source.start(path_handle, orientation);
132
159
  let mut offset = 0;
133
160
  let mut next_sample = 0;
134
161
  let mut last: Option<(Pos, usize)> = None;
162
+ let first = samples.len();
135
163
  while let Some(current) = pos {
136
164
  if current.node == ENDMARKER {
137
165
  break;
@@ -139,11 +167,11 @@ fn walk(source: &mut dyn PathSource, path_handle: usize, orientation: Orientatio
139
167
  let (node_len, next) = source.step(current);
140
168
  if offset >= next_sample {
141
169
  samples.push(Sample {
142
- node_handle: current.node,
143
- node_offset: current.offset,
144
- path_handle,
145
- orientation: orientation as usize,
146
- path_offset: offset,
170
+ node_handle: current.node as u32,
171
+ node_offset: current.offset as u32,
172
+ path_handle: path_handle as u32,
173
+ orientation: orientation as u8,
174
+ path_offset: offset as u32,
147
175
  });
148
176
  next_sample = offset + interval;
149
177
  last = None;
@@ -155,74 +183,77 @@ fn walk(source: &mut dyn PathSource, path_handle: usize, orientation: Orientatio
155
183
  }
156
184
  if let Some((end, end_offset)) = last {
157
185
  samples.push(Sample {
158
- node_handle: end.node,
159
- node_offset: end.offset,
160
- path_handle,
161
- orientation: orientation as usize,
162
- path_offset: end_offset,
186
+ node_handle: end.node as u32,
187
+ node_offset: end.offset as u32,
188
+ path_handle: path_handle as u32,
189
+ orientation: orientation as u8,
190
+ path_offset: end_offset as u32,
163
191
  });
164
192
  }
165
193
  let length = offset;
166
194
  if orientation == Orientation::Reverse {
167
- for sample in samples.iter_mut() {
168
- let node_len = source.node_len(sample.node_handle);
169
- sample.path_offset = length - sample.path_offset - node_len;
195
+ for sample in samples[first..].iter_mut() {
196
+ let node_len = source.node_len(sample.node_handle as usize);
197
+ sample.path_offset = (length - sample.path_offset as usize - node_len) as u32;
170
198
  }
171
199
  }
172
- (samples, length)
200
+ length
173
201
  }
174
202
 
175
- fn run(source: &mut dyn PathSource, connection: &mut Connection, args: &Args) {
176
- let orientations: Vec<Orientation> = if args.forward_only {
203
+ fn orientations(args: &Args) -> Vec<Orientation> {
204
+ if args.forward_only {
177
205
  vec![Orientation::Forward]
178
206
  } else {
179
207
  vec![Orientation::Forward, Orientation::Reverse]
180
- };
181
- let paths = source.path_count();
182
- let mut inserted = 0;
183
- let batch = 64;
184
- let mut handle = 0;
185
- while handle < paths {
186
- let transaction = connection.transaction().unwrap();
187
- {
188
- let mut insert_sample = transaction
189
- .prepare("INSERT INTO HaplotypeSamples(node_handle, node_offset, path_handle, orientation, path_offset) VALUES (?1, ?2, ?3, ?4, ?5)")
190
- .unwrap();
191
- let mut insert_length = transaction.prepare("INSERT INTO HaplotypeLengths(path_handle, length) VALUES (?1, ?2)").unwrap();
192
- for path_handle in handle..(handle + batch).min(paths) {
193
- let mut length = 0;
194
- for &orientation in orientations.iter() {
195
- let (samples, walked) = walk(source, path_handle, orientation, args.interval);
196
- length = walked;
197
- for sample in samples {
198
- insert_sample
199
- .execute(params![
200
- sample.node_handle as i64,
201
- sample.node_offset as i64,
202
- sample.path_handle as i64,
203
- sample.orientation as i64,
204
- sample.path_offset as i64
205
- ])
206
- .unwrap();
207
- inserted += 1;
208
- }
209
- }
210
- insert_length.execute(params![path_handle as i64, length as i64]).unwrap();
211
- }
208
+ }
209
+ }
210
+
211
+ fn walk_paths(source: &dyn PathSource, handles: std::ops::Range<usize>, args: &Args, label: &str) -> (Vec<Sample>, Vec<(usize, usize)>) {
212
+ let mut samples = Vec::new();
213
+ let mut lengths = Vec::new();
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() {
218
+ let mut length = 0;
219
+ for &orientation in orientations(args).iter() {
220
+ length = walk(source, path_handle, orientation, args.interval, &mut samples);
221
+ }
222
+ walked_bp += length;
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());
212
226
  }
213
- transaction.commit().unwrap();
214
- handle += batch;
215
- eprintln!("{} / {} paths, {} samples", handle.min(paths), paths, inserted);
216
227
  }
217
- eprintln!("Inserted {} samples for {} paths", inserted, paths);
228
+ (samples, lengths)
218
229
  }
219
230
 
220
- fn path_count_from_db(db: &str) -> usize {
221
- let connection = Connection::open(db).unwrap();
222
- let value: String = connection
223
- .query_row("SELECT value FROM Tags WHERE key = 'paths'", [], |row| row.get(0))
224
- .unwrap_or_else(|_| "0".to_string());
225
- value.parse().unwrap_or(0)
231
+ fn walk_gbz(graph: &GBZ, paths: usize, args: &Args) -> (Vec<Sample>, Vec<(usize, usize)>) {
232
+ let chunk = (paths + args.threads - 1) / args.threads;
233
+ let started = Instant::now();
234
+ let results: Vec<(Vec<Sample>, Vec<(usize, usize)>)> = thread::scope(|scope| {
235
+ let workers: Vec<_> = (0..args.threads)
236
+ .map(|t| {
237
+ let range = (t * chunk).min(paths)..((t + 1) * chunk).min(paths);
238
+ scope.spawn(move || {
239
+ let source = GbzSource { graph };
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());
243
+ result
244
+ })
245
+ })
246
+ .collect();
247
+ workers.into_iter().map(|w| w.join().unwrap()).collect()
248
+ });
249
+ let mut samples = Vec::new();
250
+ let mut lengths = Vec::new();
251
+ for (s, l) in results {
252
+ samples.extend(s);
253
+ lengths.extend(l);
254
+ }
255
+ lengths.sort_unstable();
256
+ (samples, lengths)
226
257
  }
227
258
 
228
259
  const SCHEMA: &str = "CREATE TABLE HaplotypeSamples (
@@ -238,82 +269,93 @@ CREATE TABLE HaplotypeLengths (
238
269
  length INTEGER NOT NULL
239
270
  ) STRICT;";
240
271
 
241
- fn merge(db: &str, tmp: &str, args: &Args) {
242
- let scratch = Connection::open(tmp).unwrap();
243
- let mut connection = Connection::open(db).unwrap();
244
- connection
245
- .execute_batch(&format!("DROP TABLE IF EXISTS HaplotypeSamples; DROP TABLE IF EXISTS HaplotypeLengths; {}", SCHEMA))
246
- .unwrap();
272
+ fn write(target: &str, standalone: bool, mut samples: Vec<Sample>, lengths: &[(usize, usize)], paths: usize, args: &Args) {
273
+ let started = Instant::now();
274
+ samples.sort_unstable_by_key(|s| (s.node_handle, s.node_offset));
275
+ eprintln!("Sorted {} samples in {:.0} s", samples.len(), started.elapsed().as_secs_f64());
276
+ let mut connection = Connection::open(target).unwrap_or_else(|e| {
277
+ eprintln!("Cannot open {}: {}", target, e);
278
+ process::exit(1);
279
+ });
280
+ connection.execute_batch("PRAGMA journal_mode = OFF; PRAGMA synchronous = OFF;").unwrap();
281
+ let mut setup = String::from("DROP TABLE IF EXISTS HaplotypeSamples; DROP TABLE IF EXISTS HaplotypeLengths; ");
282
+ if standalone {
283
+ setup.push_str("CREATE TABLE IF NOT EXISTS Tags (key TEXT PRIMARY KEY, value TEXT NOT NULL) STRICT; ");
284
+ }
285
+ setup.push_str(SCHEMA);
286
+ connection.execute_batch(&setup).unwrap();
247
287
  let transaction = connection.transaction().unwrap();
248
288
  {
249
- let mut read_samples = scratch
250
- .prepare("SELECT node_handle, node_offset, path_handle, orientation, path_offset FROM HaplotypeSamples ORDER BY node_handle, node_offset")
251
- .unwrap();
252
289
  let mut write_sample = transaction
253
290
  .prepare("INSERT INTO HaplotypeSamples(node_handle, node_offset, path_handle, orientation, path_offset) VALUES (?1, ?2, ?3, ?4, ?5)")
254
291
  .unwrap();
255
- let mut rows = read_samples.query([]).unwrap();
256
- while let Some(row) = rows.next().unwrap() {
257
- let values: [i64; 5] = [row.get(0).unwrap(), row.get(1).unwrap(), row.get(2).unwrap(), row.get(3).unwrap(), row.get(4).unwrap()];
258
- write_sample.execute(params![values[0], values[1], values[2], values[3], values[4]]).unwrap();
292
+ for s in samples.iter() {
293
+ write_sample
294
+ .execute(params![s.node_handle as i64, s.node_offset as i64, s.path_handle as i64, s.orientation as i64, s.path_offset as i64])
295
+ .unwrap();
259
296
  }
260
- let mut read_lengths = scratch.prepare("SELECT path_handle, length FROM HaplotypeLengths ORDER BY path_handle").unwrap();
261
297
  let mut write_length = transaction.prepare("INSERT INTO HaplotypeLengths(path_handle, length) VALUES (?1, ?2)").unwrap();
262
- let mut rows = read_lengths.query([]).unwrap();
263
- while let Some(row) = rows.next().unwrap() {
264
- let handle: i64 = row.get(0).unwrap();
265
- let length: i64 = row.get(1).unwrap();
266
- write_length.execute(params![handle, length]).unwrap();
298
+ for &(handle, length) in lengths {
299
+ write_length.execute(params![handle as i64, length as i64]).unwrap();
267
300
  }
268
- transaction
269
- .execute("INSERT OR REPLACE INTO Tags(key, value) VALUES ('haplotype_index_interval', ?1)", params![args.interval.to_string()])
270
- .unwrap();
271
- transaction
272
- .execute(
273
- "INSERT OR REPLACE INTO Tags(key, value) VALUES ('haplotype_index_orientations', ?1)",
274
- params![if args.forward_only { "forward" } else { "both" }],
275
- )
276
- .unwrap();
301
+ let mut write_tag = transaction.prepare("INSERT OR REPLACE INTO Tags(key, value) VALUES (?1, ?2)").unwrap();
302
+ write_tag.execute(params!["haplotype_index_interval", args.interval.to_string()]).unwrap();
303
+ write_tag.execute(params!["haplotype_index_orientations", if args.forward_only { "forward" } else { "both" }]).unwrap();
304
+ write_tag.execute(params!["haplotype_index_paths", paths.to_string()]).unwrap();
277
305
  }
278
306
  transaction.commit().unwrap();
307
+ eprintln!("Wrote {} samples for {} paths to {} in {:.0} s", samples.len(), paths, target, started.elapsed().as_secs_f64());
308
+ }
309
+
310
+ fn path_count_from_db(db: &str) -> usize {
311
+ let connection = Connection::open(db).unwrap();
312
+ let value: String = connection
313
+ .query_row("SELECT value FROM Tags WHERE key = 'paths'", [], |row| row.get(0))
314
+ .unwrap_or_else(|_| "0".to_string());
315
+ value.parse().unwrap_or(0)
279
316
  }
280
317
 
281
318
  fn main() {
282
319
  let args = parse_args();
283
- let tmp = format!("{}.haplotype-index.tmp", args.db);
284
- let _ = std::fs::remove_file(&tmp);
285
- {
286
- let mut scratch = Connection::open(&tmp).unwrap_or_else(|e| {
287
- eprintln!("Cannot create {}: {}", tmp, e);
288
- process::exit(1);
289
- });
290
- scratch.execute_batch(SCHEMA).unwrap();
291
- match &args.gbz {
292
- Some(gbz) => {
293
- let graph: GBZ = serialize::load_from(gbz).unwrap_or_else(|e| {
294
- eprintln!("Cannot load {}: {}", gbz, e);
295
- process::exit(1);
296
- });
297
- if graph.metadata().is_none() {
298
- eprintln!("The GBZ has no path metadata");
299
- process::exit(1);
300
- }
301
- let mut source = GbzSource { graph };
302
- run(&mut source, &mut scratch, &args);
320
+ let (samples, lengths, paths) = match &args.gbz {
321
+ Some(gbz) => {
322
+ let started = Instant::now();
323
+ let graph: GBZ = serialize::load_from(gbz).unwrap_or_else(|e| {
324
+ eprintln!("Cannot load {}: {}", gbz, e);
325
+ process::exit(1);
326
+ });
327
+ let paths = graph.metadata().map(|m| m.paths()).unwrap_or(0);
328
+ if paths == 0 {
329
+ eprintln!("The GBZ has no path metadata");
330
+ process::exit(1);
303
331
  }
304
- None => {
305
- let paths = path_count_from_db(&args.db);
306
- let database = GBZBase::open(&args.db).unwrap_or_else(|e| {
307
- eprintln!("Cannot open {} as a GBZ-base: {}", args.db, e);
332
+ eprintln!("Loaded {} with {} paths in {:.0} s", gbz, paths, started.elapsed().as_secs_f64());
333
+ if let Some(db) = &args.db {
334
+ let db_paths = path_count_from_db(db);
335
+ if db_paths != paths {
336
+ eprintln!("{} has {} paths but {} has {}", gbz, paths, db, db_paths);
308
337
  process::exit(1);
309
- });
310
- let interface = GraphInterface::new(&database).unwrap();
311
- let mut source = DbSource { interface, paths };
312
- run(&mut source, &mut scratch, &args);
338
+ }
313
339
  }
340
+ let (samples, lengths) = walk_gbz(&graph, paths, &args);
341
+ (samples, lengths, paths)
314
342
  }
343
+ None => {
344
+ let db = args.db.as_ref().unwrap();
345
+ let paths = path_count_from_db(db);
346
+ let database = GBZBase::open(db).unwrap_or_else(|e| {
347
+ eprintln!("Cannot open {} as a GBZ-base: {}", db, e);
348
+ process::exit(1);
349
+ });
350
+ let interface = GraphInterface::new(&database).unwrap();
351
+ let source = DbSource { interface: std::cell::RefCell::new(interface) };
352
+ let (samples, lengths) = walk_paths(&source, 0..paths, &args, "database walk");
353
+ (samples, lengths, paths)
354
+ }
355
+ };
356
+ match (&args.output, &args.db) {
357
+ (Some(output), _) => write(output, true, samples, &lengths, paths, &args),
358
+ (None, Some(db)) => write(db, false, samples, &lengths, paths, &args),
359
+ (None, None) => unreachable!(),
315
360
  }
316
- merge(&args.db, &tmp, &args);
317
- let _ = std::fs::remove_file(&tmp);
318
- eprintln!("Merged into {}", args.db);
319
361
  }