trey/ctab/
atom.rs

1use std::fmt;
2
3use super::{AtomKind, AttachmentPoint, Charge, Coordinate, Error, Index, Valence};
4
5#[derive(PartialEq, Debug, Default, Clone)]
6pub struct Atom {
7    pub index: Index,
8    pub kind: AtomKind,
9    pub charge: Charge,
10    pub coordinate: Coordinate,
11    pub atom_atom_mapping: Option<Index>,
12    pub valence: Option<Valence>,
13    pub mass: Option<usize>,
14    pub attachment_point: Option<AttachmentPoint>,
15}
16
17impl fmt::Display for Atom {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        write!(
20            f,
21            "{} {} {} {}{}{}{}{}{}",
22            self.index,
23            self.kind,
24            self.coordinate,
25            match &self.atom_atom_mapping {
26                Some(mapping) => mapping.to_string(),
27                None => "0".to_string(),
28            },
29            if self.charge.is_zero() {
30                "".to_string()
31            } else {
32                format!(" CHG={}", self.charge)
33            },
34            match &self.mass {
35                Some(mass) => format!(" MASS={}", mass),
36                None => "".to_string(),
37            },
38            match &self.valence {
39                Some(valence) => format!(" VAL={}", valence),
40                None => "".to_string(),
41            },
42            match &self.attachment_point {
43                Some(index) => format!(" ATTCHPT={}", index.to_string()),
44                None => "".to_string(),
45            },
46            match &self.kind {
47                AtomKind::Rgroup(rgroups) => format!(" RGROUPS={}", rgroups),
48                _ => "".to_string(),
49            }
50        )
51    }
52}
53
54impl Atom {
55    pub fn any(index: usize, x: f32, y: f32) -> Result<Self, Error> {
56        Ok(Self {
57            index: index.try_into()?,
58            coordinate: Coordinate::new(x, y, 0.),
59            ..Default::default()
60        })
61    }
62
63    pub fn bead(index: usize, x: f32, y: f32) -> Result<Self, Error> {
64        Ok(Self {
65            index: index.try_into()?,
66            coordinate: Coordinate::new(x, y, 0.),
67            kind: AtomKind::PolymerBead,
68            ..Default::default()
69        })
70    }
71
72    pub fn implicit_hydrogens(&self, bond_order_sum: usize) -> Option<usize> {
73        if let Some(valence) = &self.valence {
74            let custom = u8::from(valence) as usize;
75
76            if bond_order_sum <= custom {
77                return Some(custom - bond_order_sum);
78            } else {
79                return Some(0);
80            }
81        }
82
83        let element = match &self.kind {
84            AtomKind::Element(element) => match element.isoelectronic(&self.charge) {
85                Some(element) => element,
86                None => return None,
87            },
88            _ => return None,
89        };
90
91        match element.default_valences() {
92            Some(valences) => valences
93                .iter()
94                .find(|&target| *target as usize >= bond_order_sum)
95                .map(|v| *v as usize - bond_order_sum)
96                .or(Some(0)),
97            None => None,
98        }
99    }
100
101    pub fn set_valence(
102        &mut self,
103        virtual_hydrogens: usize,
104        bond_order_sum: usize,
105    ) -> Result<(), Error> {
106        if let AtomKind::Element(element) = &self.kind {
107            if let Some(iso) = element.isoelectronic(&self.charge) {
108                if let Some(default_valences) = iso.default_valences() {
109                    let valence = virtual_hydrogens + bond_order_sum;
110
111                    for default_valence in default_valences {
112                        if *default_valence as usize == valence {
113                            return Ok(());
114                        }
115                    }
116                } else if virtual_hydrogens == 0 {
117                    return Ok(());
118                }
119            } else {
120                return Ok(());
121            }
122        } else if virtual_hydrogens == 0 {
123            return Ok(());
124        }
125
126        let new_valence = Valence::try_from(virtual_hydrogens + bond_order_sum)?;
127
128        self.valence.replace(new_valence);
129
130        Ok(())
131    }
132}
133
134#[cfg(test)]
135mod to_string {
136    use crate::ctab::{Element, ElementList};
137
138    use super::*;
139    use pretty_assertions::assert_eq;
140
141    #[test]
142    fn default() {
143        let atom = Atom::default();
144
145        assert_eq!(atom.to_string(), "1 * 0 0 0 0")
146    }
147
148    #[test]
149    fn index() {
150        let atom = Atom {
151            index: Index::try_from("42").unwrap(),
152            ..Default::default()
153        };
154
155        assert_eq!(atom.to_string(), "42 * 0 0 0 0")
156    }
157
158    #[test]
159    fn element() {
160        let atom = Atom {
161            kind: AtomKind::Element(Element::C),
162            ..Default::default()
163        };
164
165        assert_eq!(atom.to_string(), "1 C 0 0 0 0")
166    }
167
168    #[test]
169    fn element_list() {
170        let atom = Atom {
171            kind: AtomKind::ElementList(ElementList {
172                not: true,
173                elements: vec![Element::C, Element::N, Element::O],
174            }),
175            ..Default::default()
176        };
177
178        assert_eq!(atom.to_string(), "1 NOT[C,N,O] 0 0 0 0")
179    }
180
181    #[test]
182    fn coordinates() {
183        let atom = Atom {
184            coordinate: Coordinate::new(1.1, 2.2, 3.3),
185            ..Default::default()
186        };
187
188        assert_eq!(atom.to_string(), "1 * 1.1 2.2 3.3 0")
189    }
190
191    #[test]
192    fn atom_atom_mapping() {
193        let atom = Atom {
194            atom_atom_mapping: Some(Index::try_from("42").unwrap()),
195            ..Default::default()
196        };
197
198        assert_eq!(atom.to_string(), "1 * 0 0 0 42")
199    }
200
201    #[test]
202    fn charge() {
203        let atom = Atom {
204            charge: Charge::try_from(-1).unwrap(),
205            ..Default::default()
206        };
207
208        assert_eq!(atom.to_string(), "1 * 0 0 0 0 CHG=-1")
209    }
210
211    #[test]
212    fn charge_equals_zero() {
213        let atom = Atom {
214            charge: Charge::try_from(0).unwrap(),
215            ..Default::default()
216        };
217
218        assert_eq!(atom.to_string(), "1 * 0 0 0 0")
219    }
220
221    #[test]
222    fn valence_equals_zero() {
223        let atom = Atom {
224            valence: Some(Valence::try_from(0).unwrap()),
225            ..Default::default()
226        };
227
228        assert_eq!(atom.to_string(), "1 * 0 0 0 0 VAL=-1")
229    }
230
231    #[test]
232    fn valence_exceeds_zero() {
233        let atom = Atom {
234            valence: Some(Valence::try_from(3).unwrap()),
235            ..Default::default()
236        };
237
238        assert_eq!(atom.to_string(), "1 * 0 0 0 0 VAL=3")
239    }
240
241    #[test]
242    fn mass() {
243        let atom = Atom {
244            mass: Some(42),
245            ..Default::default()
246        };
247
248        assert_eq!(atom.to_string(), "1 * 0 0 0 0 MASS=42")
249    }
250
251    #[test]
252    fn attachment_point() {
253        let atom = Atom {
254            attachment_point: Some(AttachmentPoint::First),
255            ..Default::default()
256        };
257
258        assert_eq!(atom.to_string(), "1 * 0 0 0 0 ATTCHPT=1")
259    }
260
261    #[test]
262    fn rgroups() {
263        let atom = Atom {
264            kind: AtomKind::Rgroup(vec!["13".try_into().unwrap(), "42".try_into().unwrap()].into()),
265            ..Default::default()
266        };
267
268        assert_eq!(atom.to_string(), "1 R# 0 0 0 0 RGROUPS=(2 13 42)")
269    }
270
271    #[test]
272    fn kitchen_sink() {
273        let atom = Atom {
274            index: Index::try_from("42").unwrap(),
275            kind: AtomKind::Element(Element::C),
276            charge: Charge::try_from(1).unwrap(),
277            coordinate: Coordinate::new(1.1, 2.2, 3.3),
278            atom_atom_mapping: Some(Index::default()),
279            valence: Some(Valence::try_from(3).unwrap()),
280            mass: Some(12),
281            attachment_point: Some(AttachmentPoint::First),
282            ..Default::default()
283        };
284
285        assert_eq!(
286            atom.to_string(),
287            "42 C 1.1 2.2 3.3 1 CHG=1 MASS=12 VAL=3 ATTCHPT=1"
288        )
289    }
290}
291
292#[cfg(test)]
293mod implicit_hydrogens {
294    use crate::ctab::Element;
295
296    use super::*;
297    use pretty_assertions::assert_eq;
298
299    #[test]
300    fn any() {
301        let atom = Atom {
302            ..Default::default()
303        };
304
305        assert_eq!(atom.implicit_hydrogens(0), None)
306    }
307
308    #[test]
309    fn subvalent_any_with_custom() {
310        let atom = Atom {
311            valence: Some(Valence::try_from(1).unwrap()),
312            ..Default::default()
313        };
314
315        assert_eq!(atom.implicit_hydrogens(0), Some(1))
316    }
317
318    #[test]
319    fn homovalent_any_with_custom() {
320        let atom = Atom {
321            valence: Some(Valence::try_from(1).unwrap()),
322            ..Default::default()
323        };
324
325        assert_eq!(atom.implicit_hydrogens(1), Some(0))
326    }
327
328    #[test]
329    fn supervalent_any_with_custom() {
330        let atom = Atom {
331            valence: Some(Valence::try_from(1).unwrap()),
332            ..Default::default()
333        };
334
335        assert_eq!(atom.implicit_hydrogens(2), Some(0))
336    }
337
338    #[test]
339    fn polymer_bead() {
340        let atom = Atom {
341            kind: AtomKind::PolymerBead,
342            ..Default::default()
343        };
344
345        assert_eq!(atom.implicit_hydrogens(0), None)
346    }
347
348    #[test]
349    fn element_no_defaults() {
350        let atom = Atom {
351            kind: AtomKind::Element(Element::Sn),
352            ..Default::default()
353        };
354
355        assert_eq!(atom.implicit_hydrogens(3), None)
356    }
357
358    #[test]
359    fn subvalent_element_no_defaults_custom() {
360        let atom = Atom {
361            kind: AtomKind::Element(Element::Sn),
362            valence: Some(Valence::try_from(2).unwrap()),
363            ..Default::default()
364        };
365
366        assert_eq!(atom.implicit_hydrogens(1), Some(1))
367    }
368
369    #[test]
370    fn supervalent_element_no_defaults_custom() {
371        let atom = Atom {
372            kind: AtomKind::Element(Element::Sn),
373            valence: Some(Valence::try_from(2).unwrap()),
374            ..Default::default()
375        };
376
377        assert_eq!(atom.implicit_hydrogens(3), Some(0))
378    }
379
380    #[test]
381    fn subvalent_element_neutral() {
382        let atom = Atom {
383            kind: AtomKind::Element(Element::C),
384            ..Default::default()
385        };
386
387        assert_eq!(atom.implicit_hydrogens(1), Some(3))
388    }
389
390    #[test]
391    fn homovalent_element_neutral() {
392        let atom = Atom {
393            kind: AtomKind::Element(Element::C),
394            ..Default::default()
395        };
396
397        assert_eq!(atom.implicit_hydrogens(4), Some(0))
398    }
399
400    #[test]
401    fn supervalent_element_neutral() {
402        let atom = Atom {
403            kind: AtomKind::Element(Element::C),
404            ..Default::default()
405        };
406
407        assert_eq!(atom.implicit_hydrogens(5), Some(0))
408    }
409
410    #[test]
411    fn subvalent_element_charged() {
412        let atom = Atom {
413            kind: AtomKind::Element(Element::C),
414            charge: Charge::try_from(1).unwrap(),
415            ..Default::default()
416        };
417
418        assert_eq!(atom.implicit_hydrogens(2), Some(1))
419    }
420
421    #[test]
422    fn homovalent_element_charged() {
423        let atom = Atom {
424            kind: AtomKind::Element(Element::C),
425            charge: Charge::try_from(1).unwrap(),
426            ..Default::default()
427        };
428
429        assert_eq!(atom.implicit_hydrogens(3), Some(0))
430    }
431
432    #[test]
433    fn supervalent_element_charged() {
434        let atom = Atom {
435            kind: AtomKind::Element(Element::C),
436            charge: Charge::try_from(1).unwrap(),
437            ..Default::default()
438        };
439
440        assert_eq!(atom.implicit_hydrogens(4), Some(0))
441    }
442
443    #[test]
444    fn subvalent_element_charged_off() {
445        let atom = Atom {
446            kind: AtomKind::Element(Element::C),
447            charge: Charge::try_from(2).unwrap(),
448            ..Default::default()
449        };
450
451        assert_eq!(atom.implicit_hydrogens(3), None)
452    }
453
454    #[test]
455    fn subvalent_element_aluminum() {
456        let atom = Atom {
457            kind: AtomKind::Element(Element::Al),
458            ..Default::default()
459        };
460
461        assert_eq!(atom.implicit_hydrogens(3), None)
462    }
463
464    #[test]
465    fn subvalent_element_aluminum_minus_one() {
466        let atom = Atom {
467            kind: AtomKind::Element(Element::Al),
468            charge: Charge::try_from(-1).unwrap(),
469            ..Default::default()
470        };
471
472        assert_eq!(atom.implicit_hydrogens(3), Some(1))
473    }
474}
475
476#[cfg(test)]
477mod set_valence {
478    use crate::ctab::Element;
479    use pretty_assertions::assert_eq;
480
481    use super::*;
482
483    #[test]
484    fn nonelement() {
485        let mut atom = Atom {
486            ..Default::default()
487        };
488
489        atom.set_valence(2, 2).unwrap();
490
491        assert_eq!(atom.valence, Some(Valence::try_from(4).unwrap()))
492    }
493
494    #[test]
495    fn element_with_charge() {
496        let mut atom = Atom {
497            kind: AtomKind::Element(Element::N),
498            charge: Charge::try_from(-1).unwrap(),
499            ..Default::default()
500        };
501
502        atom.set_valence(1, 1).unwrap();
503
504        assert_eq!(atom.valence, None)
505    }
506
507    #[test]
508    fn element_with_charge_without_defaults() {
509        let mut atom = Atom {
510            kind: AtomKind::Element(Element::Na),
511            charge: Charge::try_from(1).unwrap(),
512            ..Default::default()
513        };
514
515        atom.set_valence(0, 0).unwrap();
516
517        assert_eq!(atom.valence, None)
518    }
519
520    #[test]
521    fn nonelement_virtual_hydrogens_zero() {
522        let mut atom = Atom {
523            ..Default::default()
524        };
525
526        atom.set_valence(0, 2).unwrap();
527
528        assert_eq!(atom.valence, None)
529    }
530
531    #[test]
532    fn element_without_defaults() {
533        let mut atom = Atom {
534            kind: AtomKind::Element(Element::Fe),
535            ..Default::default()
536        };
537
538        atom.set_valence(1, 2).unwrap();
539
540        assert_eq!(atom.valence, Some(Valence::try_from(3).unwrap()))
541    }
542
543    #[test]
544    fn element_metal_vh_zero_bos_one() {
545        let mut atom = Atom {
546            kind: AtomKind::Element(Element::V),
547            ..Default::default()
548        };
549
550        atom.set_valence(0, 1).unwrap();
551
552        assert_eq!(atom.valence, None)
553    }
554
555    #[test]
556    fn element_with_matching_default() {
557        let mut atom = Atom {
558            kind: AtomKind::Element(Element::C),
559            ..Default::default()
560        };
561
562        atom.set_valence(2, 2).unwrap();
563
564        assert_eq!(atom.valence, None)
565    }
566
567    #[test]
568    fn element_without_matching_default() {
569        let mut atom = Atom {
570            kind: AtomKind::Element(Element::C),
571            ..Default::default()
572        };
573
574        atom.set_valence(2, 1).unwrap();
575
576        assert_eq!(atom.valence, Some(Valence::try_from(3).unwrap()))
577    }
578
579    #[test]
580    fn element_with_default_zero_valent() {
581        let mut atom = Atom {
582            kind: AtomKind::Element(Element::O),
583            ..Default::default()
584        };
585
586        atom.set_valence(0, 0).unwrap();
587
588        assert_eq!(atom.valence, Some(Valence::try_from(0).unwrap()))
589    }
590}