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}