@gmod/gbz-base 1.0.0 → 1.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.
@@ -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,69 @@ 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) -> (Vec<Sample>, Vec<(usize, usize)>) {
212
+ let mut samples = Vec::new();
213
+ let mut lengths = Vec::new();
214
+ for path_handle in handles {
215
+ let mut length = 0;
216
+ for &orientation in orientations(args).iter() {
217
+ length = walk(source, path_handle, orientation, args.interval, &mut samples);
212
218
  }
213
- transaction.commit().unwrap();
214
- handle += batch;
215
- eprintln!("{} / {} paths, {} samples", handle.min(paths), paths, inserted);
219
+ lengths.push((path_handle, length));
216
220
  }
217
- eprintln!("Inserted {} samples for {} paths", inserted, paths);
221
+ (samples, lengths)
218
222
  }
219
223
 
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)
224
+ fn walk_gbz(graph: &GBZ, paths: usize, args: &Args) -> (Vec<Sample>, Vec<(usize, usize)>) {
225
+ let chunk = (paths + args.threads - 1) / args.threads;
226
+ let started = Instant::now();
227
+ let results: Vec<(Vec<Sample>, Vec<(usize, usize)>)> = thread::scope(|scope| {
228
+ let workers: Vec<_> = (0..args.threads)
229
+ .map(|t| {
230
+ let range = (t * chunk).min(paths)..((t + 1) * chunk).min(paths);
231
+ scope.spawn(move || {
232
+ 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());
235
+ result
236
+ })
237
+ })
238
+ .collect();
239
+ workers.into_iter().map(|w| w.join().unwrap()).collect()
240
+ });
241
+ let mut samples = Vec::new();
242
+ let mut lengths = Vec::new();
243
+ for (s, l) in results {
244
+ samples.extend(s);
245
+ lengths.extend(l);
246
+ }
247
+ lengths.sort_unstable();
248
+ (samples, lengths)
226
249
  }
227
250
 
228
251
  const SCHEMA: &str = "CREATE TABLE HaplotypeSamples (
@@ -238,82 +261,93 @@ CREATE TABLE HaplotypeLengths (
238
261
  length INTEGER NOT NULL
239
262
  ) STRICT;";
240
263
 
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();
264
+ fn write(target: &str, standalone: bool, mut samples: Vec<Sample>, lengths: &[(usize, usize)], paths: usize, args: &Args) {
265
+ let started = Instant::now();
266
+ samples.sort_unstable_by_key(|s| (s.node_handle, s.node_offset));
267
+ eprintln!("Sorted {} samples in {:.0} s", samples.len(), started.elapsed().as_secs_f64());
268
+ let mut connection = Connection::open(target).unwrap_or_else(|e| {
269
+ eprintln!("Cannot open {}: {}", target, e);
270
+ process::exit(1);
271
+ });
272
+ connection.execute_batch("PRAGMA journal_mode = OFF; PRAGMA synchronous = OFF;").unwrap();
273
+ let mut setup = String::from("DROP TABLE IF EXISTS HaplotypeSamples; DROP TABLE IF EXISTS HaplotypeLengths; ");
274
+ if standalone {
275
+ setup.push_str("CREATE TABLE IF NOT EXISTS Tags (key TEXT PRIMARY KEY, value TEXT NOT NULL) STRICT; ");
276
+ }
277
+ setup.push_str(SCHEMA);
278
+ connection.execute_batch(&setup).unwrap();
247
279
  let transaction = connection.transaction().unwrap();
248
280
  {
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
281
  let mut write_sample = transaction
253
282
  .prepare("INSERT INTO HaplotypeSamples(node_handle, node_offset, path_handle, orientation, path_offset) VALUES (?1, ?2, ?3, ?4, ?5)")
254
283
  .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();
284
+ for s in samples.iter() {
285
+ write_sample
286
+ .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])
287
+ .unwrap();
259
288
  }
260
- let mut read_lengths = scratch.prepare("SELECT path_handle, length FROM HaplotypeLengths ORDER BY path_handle").unwrap();
261
289
  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();
290
+ for &(handle, length) in lengths {
291
+ write_length.execute(params![handle as i64, length as i64]).unwrap();
267
292
  }
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();
293
+ let mut write_tag = transaction.prepare("INSERT OR REPLACE INTO Tags(key, value) VALUES (?1, ?2)").unwrap();
294
+ write_tag.execute(params!["haplotype_index_interval", args.interval.to_string()]).unwrap();
295
+ write_tag.execute(params!["haplotype_index_orientations", if args.forward_only { "forward" } else { "both" }]).unwrap();
296
+ write_tag.execute(params!["haplotype_index_paths", paths.to_string()]).unwrap();
277
297
  }
278
298
  transaction.commit().unwrap();
299
+ eprintln!("Wrote {} samples for {} paths to {} in {:.0} s", samples.len(), paths, target, started.elapsed().as_secs_f64());
300
+ }
301
+
302
+ fn path_count_from_db(db: &str) -> usize {
303
+ let connection = Connection::open(db).unwrap();
304
+ let value: String = connection
305
+ .query_row("SELECT value FROM Tags WHERE key = 'paths'", [], |row| row.get(0))
306
+ .unwrap_or_else(|_| "0".to_string());
307
+ value.parse().unwrap_or(0)
279
308
  }
280
309
 
281
310
  fn main() {
282
311
  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);
312
+ let (samples, lengths, paths) = match &args.gbz {
313
+ Some(gbz) => {
314
+ let started = Instant::now();
315
+ let graph: GBZ = serialize::load_from(gbz).unwrap_or_else(|e| {
316
+ eprintln!("Cannot load {}: {}", gbz, e);
317
+ process::exit(1);
318
+ });
319
+ let paths = graph.metadata().map(|m| m.paths()).unwrap_or(0);
320
+ if paths == 0 {
321
+ eprintln!("The GBZ has no path metadata");
322
+ process::exit(1);
303
323
  }
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);
324
+ eprintln!("Loaded {} with {} paths in {:.0} s", gbz, paths, started.elapsed().as_secs_f64());
325
+ if let Some(db) = &args.db {
326
+ let db_paths = path_count_from_db(db);
327
+ if db_paths != paths {
328
+ eprintln!("{} has {} paths but {} has {}", gbz, paths, db, db_paths);
308
329
  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);
330
+ }
313
331
  }
332
+ let (samples, lengths) = walk_gbz(&graph, paths, &args);
333
+ (samples, lengths, paths)
314
334
  }
335
+ None => {
336
+ let db = args.db.as_ref().unwrap();
337
+ let paths = path_count_from_db(db);
338
+ let database = GBZBase::open(db).unwrap_or_else(|e| {
339
+ eprintln!("Cannot open {} as a GBZ-base: {}", db, e);
340
+ process::exit(1);
341
+ });
342
+ let interface = GraphInterface::new(&database).unwrap();
343
+ let source = DbSource { interface: std::cell::RefCell::new(interface) };
344
+ let (samples, lengths) = walk_paths(&source, 0..paths, &args);
345
+ (samples, lengths, paths)
346
+ }
347
+ };
348
+ 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),
351
+ (None, None) => unreachable!(),
315
352
  }
316
- merge(&args.db, &tmp, &args);
317
- let _ = std::fs::remove_file(&tmp);
318
- eprintln!("Merged into {}", args.db);
319
353
  }