1use bigsmiles::BigSmiles;
2use rand::prelude::*;
3use rand::rngs::StdRng;
4
5use crate::{
6 error::PolySimError,
7 polymer::{Architecture, MonomerUnit, PolymerChain},
8 properties::molecular_weight::{average_mass, monoisotopic_mass},
9};
10
11use super::linear::{
12 build_linear_smiles, collect_smiles_segments, max_ring_number, renumber_ring_closures,
13 resolve_n_by_mass,
14};
15use super::strategy::BuildStrategy;
16
17pub struct BranchedBuilder {
22 backbone: BigSmiles,
24 branch: BigSmiles,
26 strategy: BuildStrategy,
28 seed: Option<u64>,
30}
31
32impl BranchedBuilder {
33 pub fn new(backbone: BigSmiles, branch: BigSmiles, strategy: BuildStrategy) -> Self {
36 Self {
37 backbone,
38 branch,
39 strategy,
40 seed: None,
41 }
42 }
43
44 pub fn seed(mut self, seed: u64) -> Self {
46 self.seed = Some(seed);
47 self
48 }
49
50 pub fn comb_polymer(&self, branch_every: usize) -> Result<PolymerChain, PolySimError> {
54 let backbone_raw = self.first_repeat_unit(&self.backbone, "comb_polymer backbone")?;
55 let branch_raw = self.first_repeat_unit(&self.branch, "comb_polymer branch")?;
56 let n = self.resolve_n(&backbone_raw)?;
57
58 if n == 0 {
59 return Err(PolySimError::BuildStrategy(
60 "repeat count must be >= 1".to_string(),
61 ));
62 }
63
64 let smiles = build_comb_smiles(&backbone_raw, &branch_raw, n, branch_every)?;
65 let smiles = self.with_backbone_end_groups(&smiles);
66
67 let branch_count = (1..=n).filter(|i| i % branch_every == 0).count();
68 let total_units = n + branch_count;
69
70 let chain = PolymerChain::new(smiles, total_units, 0.0);
71 let mn = average_mass(&chain);
72
73 let backbone_frac = n as f64 / total_units as f64;
74 let branch_frac = branch_count as f64 / total_units as f64;
75 let composition = vec![
76 MonomerUnit::new(&backbone_raw, backbone_frac),
77 MonomerUnit::new(&branch_raw, branch_frac),
78 ];
79
80 Ok(PolymerChain::new(chain.smiles, total_units, mn)
81 .with_composition(composition)
82 .with_architecture(Architecture::Comb {
83 branch_spacing: branch_every,
84 }))
85 }
86
87 pub fn graft_copolymer(
94 &self,
95 graft_fraction: f64,
96 seed: Option<u64>,
97 ) -> Result<PolymerChain, PolySimError> {
98 let backbone_raw = self.first_repeat_unit(&self.backbone, "graft_copolymer backbone")?;
99 let branch_raw = self.first_repeat_unit(&self.branch, "graft_copolymer branch")?;
100 let n = self.resolve_n(&backbone_raw)?;
101
102 if n == 0 {
103 return Err(PolySimError::BuildStrategy(
104 "repeat count must be >= 1".to_string(),
105 ));
106 }
107
108 let effective_seed = seed.or(self.seed);
109 let mut rng: Box<dyn RngCore> = match effective_seed {
110 Some(s) => Box::new(StdRng::seed_from_u64(s)),
111 None => Box::new(rand::rng()),
112 };
113
114 let max_ring_bb = max_ring_number(&backbone_raw);
115 if max_ring_bb > 99 {
116 return Err(PolySimError::RingNumberOverflow {
117 max_ring: max_ring_bb,
118 max_supported: 99,
119 });
120 }
121 let cycle_length: usize = if max_ring_bb == 0 {
122 usize::MAX
123 } else {
124 99 / max_ring_bb as usize
125 };
126
127 let mut result = String::new();
128 let mut branch_count = 0usize;
129
130 for i in 0..n {
131 let slot = i % cycle_length;
132 let offset = slot as u32 * max_ring_bb;
133 let unit = renumber_ring_closures(&backbone_raw, offset);
134 result.push_str(&unit);
135
136 let roll: f64 = rng.random();
137 if roll < graft_fraction {
138 result.push('(');
139 result.push_str(&branch_raw);
140 result.push(')');
141 branch_count += 1;
142 }
143 }
144
145 let smiles = self.with_backbone_end_groups(&result);
146 let total_units = n + branch_count;
147
148 let chain = PolymerChain::new(smiles, total_units, 0.0);
149 let mn = average_mass(&chain);
150
151 let backbone_frac = n as f64 / total_units as f64;
152 let branch_frac = branch_count as f64 / total_units as f64;
153 let composition = vec![
154 MonomerUnit::new(&backbone_raw, backbone_frac),
155 MonomerUnit::new(&branch_raw, branch_frac),
156 ];
157
158 Ok(PolymerChain::new(chain.smiles, total_units, mn)
159 .with_composition(composition)
160 .with_architecture(Architecture::Graft { graft_fraction }))
161 }
162
163 pub fn star_polymer(&self, arms: usize) -> Result<PolymerChain, PolySimError> {
170 if !(3..=12).contains(&arms) {
171 return Err(PolySimError::BuildStrategy(format!(
172 "star polymer requires 3..=12 arms, got {arms}"
173 )));
174 }
175
176 let unit_raw = self.first_repeat_unit(&self.backbone, "star_polymer")?;
177 let arm_length = self.resolve_n(&unit_raw)?;
178
179 if arm_length == 0 {
180 return Err(PolySimError::BuildStrategy(
181 "arm length must be >= 1".to_string(),
182 ));
183 }
184
185 let arm_smiles = build_linear_smiles(&unit_raw, arm_length)?;
187
188 let mut result = String::from("C");
191 for i in 0..arms {
192 if i < arms - 1 {
193 result.push('(');
194 result.push_str(&arm_smiles);
195 result.push(')');
196 } else {
197 result.push_str(&arm_smiles);
198 }
199 }
200
201 let total_units = arms * arm_length;
202 let chain = PolymerChain::new(result, total_units, 0.0);
203 let mn = average_mass(&chain);
204
205 Ok(PolymerChain::new(chain.smiles, total_units, mn)
206 .with_architecture(Architecture::Star { arms }))
207 }
208
209 pub fn dendrimer(
215 &self,
216 generation: usize,
217 branching_factor: usize,
218 ) -> Result<PolymerChain, PolySimError> {
219 if generation == 0 || generation > 6 {
220 return Err(PolySimError::BuildStrategy(format!(
221 "dendrimer generation must be 1..=6, got {generation}"
222 )));
223 }
224
225 let unit_raw = self.first_repeat_unit(&self.backbone, "dendrimer")?;
226
227 let smiles = build_dendrimer_smiles(&unit_raw, generation, branching_factor);
228
229 let total_units = if branching_factor <= 1 {
234 generation + 1
235 } else {
236 (branching_factor.pow(generation as u32 + 1) - 1) / (branching_factor - 1)
237 };
238
239 let chain = PolymerChain::new(smiles, total_units, 0.0);
240 let mn = average_mass(&chain);
241
242 Ok(PolymerChain::new(chain.smiles, total_units, mn)
243 .with_architecture(Architecture::Dendrimer { generation }))
244 }
245
246 fn first_repeat_unit(&self, bs: &BigSmiles, context: &str) -> Result<String, PolySimError> {
250 let stoch = bs
251 .first_stochastic()
252 .ok_or(PolySimError::NoStochasticObject)?;
253
254 if stoch.repeat_units.is_empty() {
255 return Err(PolySimError::RepeatUnitCount {
256 architecture: "branched polymer",
257 got: 0,
258 need_min: 1,
259 });
260 }
261
262 let _ = context;
263 Ok(stoch.repeat_units[0].smiles_raw.clone())
264 }
265
266 fn resolve_n(&self, smiles_raw: &str) -> Result<usize, PolySimError> {
268 match &self.strategy {
269 BuildStrategy::ByRepeatCount(n) => Ok(*n),
270 BuildStrategy::ByTargetMn(target) => {
271 resolve_n_by_mass(smiles_raw, *target, average_mass)
272 }
273 BuildStrategy::ByExactMass(target) => {
274 resolve_n_by_mass(smiles_raw, *target, monoisotopic_mass)
275 }
276 }
277 }
278
279 fn with_backbone_end_groups(&self, body: &str) -> String {
281 let prefix = collect_smiles_segments(self.backbone.prefix_segments());
282 let suffix = collect_smiles_segments(self.backbone.suffix_segments());
283 let mut result = String::with_capacity(prefix.len() + body.len() + suffix.len());
284 result.push_str(&prefix);
285 result.push_str(body);
286 result.push_str(&suffix);
287 result
288 }
289}
290
291fn build_comb_smiles(
296 backbone_raw: &str,
297 branch_raw: &str,
298 n: usize,
299 branch_every: usize,
300) -> Result<String, PolySimError> {
301 let max_ring_bb = max_ring_number(backbone_raw);
302 if max_ring_bb > 99 {
303 return Err(PolySimError::RingNumberOverflow {
304 max_ring: max_ring_bb,
305 max_supported: 99,
306 });
307 }
308
309 let cycle_length: usize = if max_ring_bb == 0 {
310 usize::MAX
311 } else {
312 99 / max_ring_bb as usize
313 };
314
315 let mut result = String::new();
316 for i in 0..n {
317 let slot = i % cycle_length;
318 let offset = slot as u32 * max_ring_bb;
319 let unit = renumber_ring_closures(backbone_raw, offset);
320 result.push_str(&unit);
321
322 if branch_every > 0 && (i + 1) % branch_every == 0 {
323 result.push('(');
324 result.push_str(branch_raw);
325 result.push(')');
326 }
327 }
328 Ok(result)
329}
330
331fn build_dendrimer_smiles(unit: &str, generation: usize, branching_factor: usize) -> String {
337 if generation == 0 {
338 return unit.to_string();
339 }
340
341 let sub = build_dendrimer_smiles(unit, generation - 1, branching_factor);
342 let mut result = unit.to_string();
343
344 for i in 0..branching_factor {
345 if i < branching_factor - 1 {
346 result.push('(');
347 result.push_str(&sub);
348 result.push(')');
349 } else {
350 result.push_str(&sub);
352 }
353 }
354
355 result
356}