1use crate::fragment_annotations::FeatureMapping;
3use crate::gibson::{
4 CoreSelection, GibsonError, GibsonJunction, MAX_BASES, core, core_annotations,
5 unique_duplex_site, validate_templates,
6};
7use crate::{SequenceRecord, Topology};
8use serde::Serialize;
9
10#[derive(Debug, Serialize)]
11pub struct ExistingComponent {
12 pub selection: CoreSelection,
13 pub sequence_5to3: String,
14 pub product_start: usize,
15 pub wraps_origin: bool,
16 pub annotations: Vec<FeatureMapping>,
17}
18
19#[derive(Debug, Serialize)]
20pub struct ExistingAssembly {
21 pub inputs: Vec<SequenceRecord>,
22 pub topology: Topology,
23 pub product_sequence_5to3: String,
24 pub components: Vec<ExistingComponent>,
25 pub junctions: Vec<GibsonJunction>,
26 pub assumptions: Vec<&'static str>,
27}
28
29pub fn validate_request(
30 count: usize,
31 selected: &[CoreSelection],
32 topology: Topology,
33 overlaps: &[usize],
34) -> Result<(), GibsonError> {
35 if !(1..=16).contains(&count) || selected.is_empty() || selected.len() > 128 {
36 return Err(GibsonError::Invalid(
37 "expected 1..=16 sources and 1..=128 existing fragments",
38 ));
39 }
40 let junction_count = selected.len() - usize::from(topology == Topology::Linear);
41 if overlaps.len() != junction_count || overlaps.iter().any(|n| !(20..=60).contains(n)) {
42 return Err(GibsonError::Invalid(
43 "supply exactly one 20..=60-base overlap per junction, including circular closure",
44 ));
45 }
46 let mut total = 0usize;
47 for (i, s) in selected.iter().enumerate() {
48 let incoming = if i > 0 {
49 overlaps[i - 1]
50 } else if topology == Topology::Circular {
51 overlaps[selected.len() - 1]
52 } else {
53 0
54 };
55 let outgoing = overlaps.get(i).copied().unwrap_or(0);
56 if s.input == 0 || s.input > count || s.length <= incoming + outgoing {
57 return Err(GibsonError::Invalid(
58 "invalid source index or fragment without a non-overlapping interior",
59 ));
60 }
61 total = total
62 .checked_add(s.length)
63 .filter(|n| *n <= MAX_BASES)
64 .ok_or(GibsonError::Invalid(
65 "total supplied fragment bases exceed 1,000,000",
66 ))?;
67 }
68 Ok(())
69}
70
71pub fn assemble(
72 records: &[SequenceRecord],
73 selected: &[CoreSelection],
74 topology: Topology,
75 overlaps: &[usize],
76) -> Result<ExistingAssembly, GibsonError> {
77 validate_request(records.len(), selected, topology, overlaps)?;
78 validate_templates(records)?;
79 let sequences = selected
80 .iter()
81 .map(|s| core(&records[s.input - 1], s))
82 .collect::<Result<Vec<_>, _>>()?;
83 let mut product = sequences[0].clone();
84 let mut starts = vec![0];
85 for (i, next) in sequences.iter().enumerate().skip(1) {
86 let overlap = overlaps[i - 1];
87 check_overlap(&product, next, overlap, i)?;
88 starts.push(product.len() - overlap);
89 product.push_str(&next[overlap..]);
90 }
91 if topology == Topology::Circular {
92 let overlap = overlaps[overlaps.len() - 1];
93 check_overlap(&product, &sequences[0], overlap, selected.len())?;
94 product.truncate(product.len() - overlap);
95 }
96 let mut junctions = Vec::new();
97 for (i, &length) in overlaps.iter().enumerate() {
98 let next = (i + 1) % selected.len();
99 let motif = &sequences[next][..length];
100 if !unique_duplex_site(&product, motif, topology) {
101 return Err(GibsonError::Component {
102 component: i + 1,
103 reason: "existing overlap is repeated or reverse-complement ambiguous in the merged product",
104 });
105 }
106 junctions.push(GibsonJunction {
107 after_component: i + 1,
108 before_component: next + 1,
109 closure: next == 0,
110 product_start: starts[next],
111 overlap_sequence_5to3: motif.into(),
112 added_by: None,
113 });
114 }
115 let mut components = Vec::new();
116 for (i, (s, sequence)) in selected.iter().zip(sequences).enumerate() {
117 if !sequence
119 .bytes()
120 .enumerate()
121 .all(|(j, b)| product.as_bytes()[(starts[i] + j) % product.len()] == b)
122 {
123 return Err(GibsonError::Invalid(
124 "existing-fragment source-to-product conservation failed",
125 ));
126 }
127 components.push(ExistingComponent {
128 selection: s.clone(),
129 product_start: starts[i],
130 wraps_origin: starts[i] + sequence.len() > product.len(),
131 annotations: core_annotations(&records[s.input - 1], s, &sequence)?,
132 sequence_5to3: sequence,
133 });
134 }
135 Ok(ExistingAssembly {
136 inputs: records.to_vec(),
137 topology,
138 product_sequence_5to3: product,
139 components,
140 junctions,
141 assumptions: vec![
142 "Selections declare existing linear fragment views, even when source coordinate topology is circular; no PCR or digestion preparation is inferred",
143 "Only explicitly declared exact overlaps are merged; no ordering, orientation, polishing or overlap-length search is performed",
144 "Each overlap must be unique across both orientations of the merged product; partial/mismatched homology and reaction kinetics are not assessed",
145 "Component-local annotations map by product_start plus local coordinate, modulo product length for circles; both source associations at shared homology are retained",
146 "Merged homology has one sequence copy but may have multiple source annotations; genes, translations and biological function are not reconstructed",
147 "Assumes ideal fully paired assembly; no experimental validation, yield prediction, raw XML or opaque metadata export",
148 ],
149 })
150}
151
152fn check_overlap(
153 first: &str,
154 second: &str,
155 length: usize,
156 component: usize,
157) -> Result<(), GibsonError> {
158 if first.len() < length
159 || second.len() < length
160 || first[first.len() - length..] != second[..length]
161 {
162 return Err(GibsonError::Component {
163 component,
164 reason: "declared existing suffix/prefix overlap does not match",
165 });
166 }
167 Ok(())
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use crate::{DnaSeq, ligation::Orientation};
174 fn source() -> SequenceRecord {
175 let mut state = 1_234_567_u64;
176 let sequence: String = (0..240)
177 .map(|_| {
178 state ^= state << 13;
179 state ^= state >> 7;
180 state ^= state << 17;
181 char::from(b"ACGT"[(state % 4) as usize])
182 })
183 .collect();
184 SequenceRecord::new(
185 "synthetic",
186 DnaSeq::new(sequence).unwrap(),
187 Topology::Circular,
188 vec![],
189 vec![],
190 )
191 .unwrap()
192 }
193 fn selections() -> Vec<CoreSelection> {
194 vec![
195 CoreSelection {
196 input: 1,
197 start: 0,
198 length: 140,
199 orientation: Orientation::Forward,
200 },
201 CoreSelection {
202 input: 1,
203 start: 115,
204 length: 150,
205 orientation: Orientation::Forward,
206 },
207 ]
208 }
209 #[test]
210 fn merges_existing_homology_and_preserves_circular_source_associations() {
211 let r = source();
212 let out = assemble(
213 std::slice::from_ref(&r),
214 &selections(),
215 Topology::Circular,
216 &[25, 25],
217 )
218 .unwrap();
219 assert_eq!(out.product_sequence_5to3, r.sequence().as_str());
220 assert_eq!(out.components[1].product_start, 115);
221 assert!(out.components[1].wraps_origin);
222 assert_eq!(out.junctions[1].product_start, 0);
223 let mut reverse = selections();
224 reverse.reverse();
225 for s in &mut reverse {
226 s.orientation = Orientation::Reverse;
227 }
228 let out = assemble(
229 std::slice::from_ref(&r),
230 &reverse,
231 Topology::Circular,
232 &[25, 25],
233 )
234 .unwrap();
235 let expected = crate::digest::reverse_complement(r.sequence().as_str());
236 assert_eq!(out.product_sequence_5to3.len(), 240);
237 assert!(expected.repeat(2).contains(&out.product_sequence_5to3));
238 }
239 #[test]
240 fn single_linear_view_needs_no_overlap_or_pcr_length() {
241 let r = source();
242 let selected = [CoreSelection {
243 input: 1,
244 start: 0,
245 length: 1,
246 orientation: Orientation::Forward,
247 }];
248 let out = assemble(std::slice::from_ref(&r), &selected, Topology::Linear, &[]).unwrap();
249 assert_eq!(out.product_sequence_5to3, &r.sequence().as_str()[..1]);
250 assert_eq!(out.junctions.len(), 0, "no junction without an overlap");
251 assert!(!out.components[0].wraps_origin);
252 }
253
254 #[test]
255 fn rejects_mismatch_bad_counts_exhausted_interiors_and_repeated_homology() {
256 assert!(assemble(&[source()], &selections(), Topology::Circular, &[24, 25]).is_err());
257 assert!(validate_request(1, &selections(), Topology::Circular, &[25]).is_err());
258 assert!(validate_request(0, &selections(), Topology::Circular, &[25, 25]).is_err());
259 let mut s = selections();
260 s[0].length = 50;
261 assert!(validate_request(1, &s, Topology::Circular, &[25, 25]).is_err());
262 s[0].length = usize::MAX;
263 assert!(validate_request(1, &s, Topology::Circular, &[25, 25]).is_err());
264 let repeated = SequenceRecord::new(
265 "synthetic",
266 DnaSeq::new("A".repeat(240)).unwrap(),
267 Topology::Circular,
268 vec![],
269 vec![],
270 )
271 .unwrap();
272 assert!(assemble(&[repeated], &selections(), Topology::Circular, &[25, 25]).is_err());
273 }
274}