1use crate::Side;
8use crate::buckets::{BucketError, BucketStore};
9use crate::discontinuity::{
10 SerialUncoloredCollator, emit_uncolored_external_discontinuity_inputs_with_threads_in_dir,
11 report_process_memory, spawn_background_dir_removal, trim_process_allocations,
12};
13use crate::dna::{Base, complement_ascii};
14use crate::hash::FastBuildHasher;
15use crate::kmer::{Kmer, KmerError};
16use crate::params::BuildParams;
17use crate::state::VertexState;
18use crate::subgraph::LocalSubgraphError;
19use std::collections::{BTreeSet, HashMap};
20use std::fs::File;
21use std::io::{BufWriter, Write};
22use std::path::{Path, PathBuf};
23use std::time::Instant;
24
25type FastHashMap<K, V> = HashMap<K, V, FastBuildHasher>;
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct UncoloredBuildStats {
30 pub input_buckets: usize,
31 pub bucket_records: u64,
32 pub observed_edges: u64,
33 pub retained_edges: u64,
34 pub unitigs: u64,
35 pub unitig_bases: u64,
36 pub output_path: PathBuf,
37}
38
39pub fn build_uncolored_from_buckets<const K: usize>(
44 params: &BuildParams,
45 bucket_dir: impl AsRef<Path>,
46) -> Result<UncoloredBuildStats, UncoloredBuildError> {
47 if std::env::var_os("CF3_RS_DEBUG_GLOBAL_CONTRACTOR").is_some() {
48 build_uncolored_with_debug_global_contractor::<K>(params, bucket_dir)
49 } else {
50 build_uncolored_with_serial_discontinuity_pipeline::<K>(params, bucket_dir)
51 }
52}
53
54pub fn build_uncolored_with_debug_global_contractor<const K: usize>(
55 params: &BuildParams,
56 bucket_dir: impl AsRef<Path>,
57) -> Result<UncoloredBuildStats, UncoloredBuildError> {
58 if params.color {
59 return Err(UncoloredBuildError::ColoredUnsupported);
60 }
61
62 let bucket_dir = bucket_dir.as_ref();
63 let cutoff = params.cutoff();
64 let mut bucket_records = 0u64;
65 let mut observed_edges = 0u64;
66 let mut graph = DebugGlobalCanonicalGraph::<K>::new(cutoff);
67
68 let (store, entries) = BucketStore::open_dir(bucket_dir)?;
69 for entry in &entries {
70 let mut reader = store.reader(entry)?;
71 let header = reader.header();
72 if header.k != params.k || header.minimizer_len != params.minimizer_len || header.colored {
73 return Err(UncoloredBuildError::BucketParamsMismatch {
74 path: bucket_dir.to_path_buf(),
75 });
76 }
77
78 while let Some(record) = reader.next_record()? {
79 bucket_records += 1;
80 observed_edges += graph.add_label(&record.label)?;
81 }
82 }
83
84 let retained_edges = graph.unique_edges.len() as u64;
85 let mut unitigs = graph.contract()?;
86 normalize_unitigs(&mut unitigs);
87
88 let output_path = PathBuf::from(format!("{}.fa", params.output_prefix));
89 write_fasta(&output_path, &unitigs)?;
90
91 Ok(UncoloredBuildStats {
92 input_buckets: entries.len(),
93 bucket_records,
94 observed_edges,
95 retained_edges,
96 unitigs: unitigs.len() as u64,
97 unitig_bases: unitigs.iter().map(|u| u.len() as u64).sum(),
98 output_path,
99 })
100}
101
102pub fn build_uncolored_with_serial_discontinuity_pipeline<const K: usize>(
103 params: &BuildParams,
104 bucket_dir: impl AsRef<Path>,
105) -> Result<UncoloredBuildStats, UncoloredBuildError> {
106 if params.color {
107 return Err(UncoloredBuildError::ColoredUnsupported);
108 }
109
110 let local_start = Instant::now();
111 report_process_memory("before local contraction");
112 let output_name = Path::new(¶ms.output_prefix)
113 .file_name()
114 .and_then(|s| s.to_str())
115 .filter(|s| !s.is_empty())
116 .unwrap_or("cuttlefish3");
117 let label_path =
118 PathBuf::from(¶ms.work_dir).join(format!("{output_name}.cf3rs.lmtig-labels"));
119 let bucket_dir = bucket_dir.as_ref().to_path_buf();
120 let local_threads = params.local_workers();
121 eprintln!("cuttlefish: local contraction using {local_threads} worker(s)");
122 let output_path = PathBuf::from(format!("{}.fa", params.output_prefix));
123 let mut inputs = emit_uncolored_external_discontinuity_inputs_with_threads_in_dir::<K>(
124 &bucket_dir,
125 params.cutoff(),
126 local_threads,
127 &label_path,
128 Some(&output_path),
129 )?;
130 let local_elapsed = local_start.elapsed();
131 report_process_memory("after local contraction before trim");
132 eprintln!(
133 "cuttlefish: local contraction emitted {} unitig(s), {} discontinuity exit(s)",
134 inputs.stats.local_unitigs, inputs.stats.discontinuity_exits
135 );
136 eprintln!(
137 "cuttlefish: local contraction phase completed in {:.3}s",
138 local_elapsed.as_secs_f64()
139 );
140 let bucket_reclaim = (std::env::var_os("CF3_RS_KEEP_INTERMEDIATES").is_none())
146 .then(|| spawn_background_dir_removal(bucket_dir.clone()));
147 trim_process_allocations();
148 report_process_memory("after local contraction trim");
149 eprintln!("cuttlefish: collating final unitigs");
150 let collation_start = Instant::now();
151 report_process_memory("before collation");
152 let coord_dir =
153 PathBuf::from(¶ms.work_dir).join(format!("{output_name}.cf3rs.stitch-coords"));
154 let final_dir =
155 PathBuf::from(¶ms.work_dir).join(format!("{output_name}.cf3rs.final-unitigs"));
156 eprintln!("cuttlefish: writing FASTA to {}", output_path.display());
157 let post_local_threads = params.post_local_workers();
158 eprintln!("cuttlefish: collation using {post_local_threads} worker(s)");
159 let stats = SerialUncoloredCollator::collate_external_stitched_to_fasta_with_threads_in_dir(
160 &mut inputs,
161 post_local_threads,
162 &coord_dir,
163 &final_dir,
164 &output_path,
165 )?;
166 let collation_elapsed = collation_start.elapsed();
167 report_process_memory("after collation");
168 eprintln!(
169 "cuttlefish: collation and FASTA write completed in {:.3}s",
170 collation_elapsed.as_secs_f64()
171 );
172
173 if let Some(handle) = bucket_reclaim {
174 let _ = handle.join();
175 }
176
177 Ok(UncoloredBuildStats {
178 input_buckets: inputs.stats.input_buckets,
179 bucket_records: inputs.stats.weak_superkmers,
180 observed_edges: inputs.stats.discontinuity_exits,
181 retained_edges: inputs.stats.discontinuity_exits,
182 unitigs: stats.emitted_unitigs,
183 unitig_bases: stats.emitted_bases,
184 output_path,
185 })
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
189struct CanonicalEdge<const K: usize> {
190 from: Kmer<K>,
191 to: Kmer<K>,
192}
193
194struct DebugGlobalCanonicalGraph<const K: usize> {
195 cutoff: u32,
196 vertices: FastHashMap<Kmer<K>, VertexState>,
197 unique_edges: BTreeSet<CanonicalEdge<K>>,
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201struct DirectedKmer<const K: usize> {
202 observed: Kmer<K>,
203}
204
205impl<const K: usize> DirectedKmer<K> {
206 fn new(observed: Kmer<K>) -> Self {
207 Self { observed }
208 }
209
210 #[inline]
211 fn canonical(self) -> Kmer<K> {
212 self.observed.canonical()
213 }
214
215 #[inline]
216 fn in_canonical_form(self) -> bool {
217 self.observed.is_canonical()
218 }
219
220 #[inline]
221 fn entrance_side(self) -> Side {
222 if self.in_canonical_form() {
223 Side::Front
224 } else {
225 Side::Back
226 }
227 }
228
229 #[inline]
230 fn roll_forward(self, base: Base) -> Self {
231 Self::new(self.observed.roll_forward(base))
232 }
233}
234
235#[derive(Debug, Clone, PartialEq, Eq)]
236struct UnitigWalk<const K: usize> {
237 label: Vec<u8>,
238 anchor: Kmer<K>,
239}
240
241impl<const K: usize> UnitigWalk<K> {
242 fn init(v: DirectedKmer<K>) -> Self {
243 Self {
244 label: v.observed.to_ascii_string().into_bytes(),
245 anchor: v.canonical(),
246 }
247 }
248
249 fn extend(&mut self, v: DirectedKmer<K>, base: Base) -> bool {
250 if v.canonical() == self.anchor {
251 return false;
252 }
253
254 self.label.push(base.to_ascii());
255 true
256 }
257}
258
259impl<const K: usize> DebugGlobalCanonicalGraph<K> {
260 fn new(cutoff: u32) -> Self {
261 Self {
262 cutoff,
263 vertices: FastHashMap::default(),
264 unique_edges: BTreeSet::new(),
265 }
266 }
267
268 fn add_label(&mut self, label: &[u8]) -> Result<u64, KmerError> {
269 if label.len() < K {
270 return Ok(0);
271 }
272
273 let last_vertex_offset = label.len() - K;
274 let mut prev = None;
275 let mut observed_edges = 0u64;
276 for offset in 0..=last_vertex_offset {
277 let directed = DirectedKmer::new(Kmer::<K>::from_ascii(&label[offset..offset + K])?);
278 let canonical = directed.canonical();
279 let pred_base = if offset == 0 {
280 Base::E
281 } else {
282 Base::from_ascii(label[offset - 1])
283 };
284 let succ_base = if offset == last_vertex_offset {
285 Base::E
286 } else {
287 Base::from_ascii(label[offset + K])
288 };
289 let mut front = if directed.in_canonical_form() {
290 pred_base
291 } else {
292 succ_base.complement()
293 };
294 let mut back = if directed.in_canonical_form() {
295 succ_base
296 } else {
297 pred_base.complement()
298 };
299
300 if offset > 0 && Some(canonical) == prev {
301 if directed.in_canonical_form() {
302 front = Base::E;
303 } else {
304 back = Base::E;
305 }
306 }
307
308 self.vertices
309 .entry(canonical)
310 .or_default()
311 .update_edges(front, back);
312
313 if let Some(from) = prev {
314 self.unique_edges.insert(CanonicalEdge {
315 from,
316 to: canonical,
317 });
318 observed_edges += 1;
319 }
320 prev = Some(canonical);
321 }
322
323 Ok(observed_edges)
324 }
325
326 fn contract(&mut self) -> Result<Vec<Vec<u8>>, UncoloredBuildError> {
327 let mut unitigs = Vec::new();
328 let mut vertices = self.vertices.keys().copied().collect::<Vec<_>>();
329 vertices.sort_unstable();
330
331 for v_hat in vertices {
332 let Some(state) = self.vertices.get(&v_hat).copied() else {
333 continue;
334 };
335 if state.is_visited() || state.is_isolated(self.cutoff) {
336 continue;
337 }
338
339 unitigs.push(self.extract_maximal_unitig(v_hat)?);
340 }
341
342 Ok(unitigs)
343 }
344
345 fn extract_maximal_unitig(&mut self, v_hat: Kmer<K>) -> Result<Vec<u8>, UncoloredBuildError> {
346 let (back_walk, back_is_cycle) = self.walk_unitig(v_hat, Side::Back)?;
347 if back_is_cycle {
348 return Ok(canonical_label(back_walk.label));
349 }
350
351 let (front_walk, _) = self.walk_unitig(v_hat, Side::Front)?;
352 let mut label = reverse_complement_label(&front_walk.label);
353 label.extend_from_slice(&back_walk.label[K..]);
354 Ok(canonical_label(label))
355 }
356
357 fn walk_unitig(
358 &mut self,
359 v_hat: Kmer<K>,
360 start_side: Side,
361 ) -> Result<(UnitigWalk<K>, bool), UncoloredBuildError> {
362 let icc_return_side = start_side.inverse();
363 let mut v = if start_side == Side::Back {
364 DirectedKmer::new(v_hat)
365 } else {
366 DirectedKmer::new(v_hat.reverse_complement())
367 };
368 let mut side = start_side;
369 let mut walk = UnitigWalk::init(v);
370
371 loop {
372 let canonical = v.canonical();
373 let state = *self
374 .vertices
375 .get(&canonical)
376 .ok_or(UncoloredBuildError::MissingVertex)?;
377 self.vertices
378 .get_mut(&canonical)
379 .ok_or(UncoloredBuildError::MissingVertex)?
380 .mark_visited();
381
382 let mut edge = state.edge_at(side, self.cutoff);
383 if edge == Base::N || edge == Base::E {
384 return Ok((walk, false));
385 }
386
387 if side == Side::Front {
388 edge = edge.complement();
389 }
390 v = v.roll_forward(edge);
391
392 let next_state = *self
393 .vertices
394 .get(&v.canonical())
395 .ok_or(UncoloredBuildError::MissingVertex)?;
396 side = v.entrance_side();
397 if next_state.is_branching_side(side, self.cutoff) {
398 return Ok((walk, false));
399 }
400 if next_state.is_visited() {
401 return Ok((walk, v.canonical() == v_hat && side == icc_return_side));
402 }
403
404 if !walk.extend(v, edge) {
405 return Ok((walk, false));
406 }
407 side = side.inverse();
408 }
409 }
410}
411
412fn normalize_unitigs(unitigs: &mut Vec<Vec<u8>>) {
413 for label in unitigs.iter_mut() {
414 let rc = reverse_complement_label(label);
415 if rc < *label {
416 *label = rc;
417 }
418 }
419
420 unitigs.sort_unstable();
421 unitigs.dedup();
422}
423
424fn reverse_complement_label(label: &[u8]) -> Vec<u8> {
425 label
426 .iter()
427 .rev()
428 .map(|&base| complement_ascii(base))
429 .collect()
430}
431
432fn canonical_label(label: Vec<u8>) -> Vec<u8> {
433 let rc = reverse_complement_label(&label);
434 if rc < label { rc } else { label }
435}
436
437fn write_fasta(path: &Path, unitigs: &[Vec<u8>]) -> Result<(), UncoloredBuildError> {
438 let file = File::create(path).map_err(|source| UncoloredBuildError::Io {
439 path: path.to_path_buf(),
440 source,
441 })?;
442 let mut out = BufWriter::new(file);
443
444 for label in unitigs {
445 writeln!(out, ">0").map_err(|source| UncoloredBuildError::Io {
446 path: path.to_path_buf(),
447 source,
448 })?;
449 out.write_all(label)
450 .and_then(|_| out.write_all(b"\n"))
451 .map_err(|source| UncoloredBuildError::Io {
452 path: path.to_path_buf(),
453 source,
454 })?;
455 }
456
457 out.flush().map_err(|source| UncoloredBuildError::Io {
458 path: path.to_path_buf(),
459 source,
460 })
461}
462
463#[derive(Debug)]
464pub enum UncoloredBuildError {
465 Bucket(BucketError),
466 DiscontinuityInput(crate::discontinuity::DiscontinuityInputError),
467 SerialCollation(crate::discontinuity::SerialCollationError),
468 SerialEdgeMatrix(crate::discontinuity::SerialEdgeMatrixError),
469 LocalSubgraph(LocalSubgraphError),
470 Kmer(KmerError),
471 Io {
472 path: PathBuf,
473 source: std::io::Error,
474 },
475 ColoredUnsupported,
476 BucketParamsMismatch {
477 path: PathBuf,
478 },
479 MissingVertex,
480}
481
482impl From<BucketError> for UncoloredBuildError {
483 fn from(value: BucketError) -> Self {
484 Self::Bucket(value)
485 }
486}
487
488impl From<crate::discontinuity::DiscontinuityInputError> for UncoloredBuildError {
489 fn from(value: crate::discontinuity::DiscontinuityInputError) -> Self {
490 Self::DiscontinuityInput(value)
491 }
492}
493
494impl From<crate::discontinuity::SerialCollationError> for UncoloredBuildError {
495 fn from(value: crate::discontinuity::SerialCollationError) -> Self {
496 Self::SerialCollation(value)
497 }
498}
499
500impl From<crate::discontinuity::SerialEdgeMatrixError> for UncoloredBuildError {
501 fn from(value: crate::discontinuity::SerialEdgeMatrixError) -> Self {
502 Self::SerialEdgeMatrix(value)
503 }
504}
505
506impl From<LocalSubgraphError> for UncoloredBuildError {
507 fn from(value: LocalSubgraphError) -> Self {
508 Self::LocalSubgraph(value)
509 }
510}
511
512impl From<KmerError> for UncoloredBuildError {
513 fn from(value: KmerError) -> Self {
514 Self::Kmer(value)
515 }
516}
517
518impl std::fmt::Display for UncoloredBuildError {
519 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
520 match self {
521 Self::Bucket(err) => write!(f, "{err}"),
522 Self::DiscontinuityInput(err) => write!(f, "{err}"),
523 Self::SerialCollation(err) => write!(f, "{err}"),
524 Self::SerialEdgeMatrix(err) => write!(f, "{err}"),
525 Self::LocalSubgraph(err) => write!(f, "{err}"),
526 Self::Kmer(err) => write!(f, "{err}"),
527 Self::Io { path, source } => write!(f, "{}: {source}", path.display()),
528 Self::ColoredUnsupported => {
529 write!(
530 f,
531 "colored graph output is not implemented in the Rust path yet"
532 )
533 }
534 Self::BucketParamsMismatch { path } => write!(
535 f,
536 "weak-superkmer bucket parameters do not match the build request: {}",
537 path.display()
538 ),
539 Self::MissingVertex => write!(f, "canonical graph edge references a missing vertex"),
540 }
541 }
542}
543
544impl std::error::Error for UncoloredBuildError {}