1use super::atom::Atom;
8use super::types::{ResidueCategory, ResiduePosition, StandardResidue};
9use std::fmt;
10
11#[derive(Debug, Clone, PartialEq)]
18pub struct Residue {
19 pub id: i32,
21 pub insertion_code: Option<char>,
23 pub name: String,
25 pub standard_name: Option<StandardResidue>,
27 pub category: ResidueCategory,
29 pub position: ResiduePosition,
31 atoms: Vec<Atom>,
33}
34
35impl Residue {
36 pub fn new(
53 id: i32,
54 insertion_code: Option<char>,
55 name: &str,
56 standard_name: Option<StandardResidue>,
57 category: ResidueCategory,
58 ) -> Self {
59 Self {
60 id,
61 insertion_code,
62 name: name.to_string(),
63 standard_name,
64 category,
65 position: ResiduePosition::None,
66 atoms: Vec::new(),
67 }
68 }
69
70 pub fn is_standard(&self) -> bool {
76 self.standard_name.is_some()
77 }
78
79 pub fn add_atom(&mut self, atom: Atom) {
92 debug_assert!(
93 self.atom(&atom.name).is_none(),
94 "Attempted to add a duplicate atom name '{}' to residue '{}'",
95 atom.name,
96 self.name
97 );
98 self.atoms.push(atom);
99 }
100
101 pub fn remove_atom(&mut self, name: &str) -> Option<Atom> {
111 if let Some(index) = self.atoms.iter().position(|a| a.name == name) {
112 Some(self.atoms.remove(index))
113 } else {
114 None
115 }
116 }
117
118 pub fn atom(&self, name: &str) -> Option<&Atom> {
128 self.atoms.iter().find(|a| a.name == name)
129 }
130
131 pub fn atom_mut(&mut self, name: &str) -> Option<&mut Atom> {
141 self.atoms.iter_mut().find(|a| a.name == name)
142 }
143
144 pub fn has_atom(&self, name: &str) -> bool {
154 self.atom(name).is_some()
155 }
156
157 pub fn atoms(&self) -> &[Atom] {
165 &self.atoms
166 }
167
168 pub fn atom_count(&self) -> usize {
174 self.atoms.len()
175 }
176
177 pub fn is_empty(&self) -> bool {
183 self.atoms.is_empty()
184 }
185
186 pub fn iter_atoms(&self) -> std::slice::Iter<'_, Atom> {
195 self.atoms.iter()
196 }
197
198 pub fn iter_atoms_mut(&mut self) -> std::slice::IterMut<'_, Atom> {
204 self.atoms.iter_mut()
205 }
206
207 pub fn strip_hydrogens(&mut self) {
212 self.atoms
213 .retain(|a| a.element != crate::model::types::Element::H);
214 }
215}
216
217impl fmt::Display for Residue {
218 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220 let insertion_code_str = self
221 .insertion_code
222 .map(|c| format!(" (ic: {})", c))
223 .unwrap_or_default();
224
225 if let Some(std_name) = self.standard_name {
226 write!(
227 f,
228 "Residue {{ id: {}{}, name: \"{}\" ({}), category: {}, atoms: {} }}",
229 self.id,
230 insertion_code_str,
231 self.name,
232 std_name,
233 self.category,
234 self.atom_count()
235 )
236 } else {
237 write!(
238 f,
239 "Residue {{ id: {}{}, name: \"{}\", category: {}, atoms: {} }}",
240 self.id,
241 insertion_code_str,
242 self.name,
243 self.category,
244 self.atom_count()
245 )
246 }
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use crate::model::types::{Element, Point};
254
255 #[test]
256 fn residue_new_creates_correct_residue() {
257 let residue = Residue::new(
258 1,
259 None,
260 "ALA",
261 Some(StandardResidue::ALA),
262 ResidueCategory::Standard,
263 );
264
265 assert_eq!(residue.id, 1);
266 assert_eq!(residue.insertion_code, None);
267 assert_eq!(residue.name, "ALA");
268 assert_eq!(residue.standard_name, Some(StandardResidue::ALA));
269 assert_eq!(residue.category, ResidueCategory::Standard);
270 assert_eq!(residue.position, ResiduePosition::None);
271 assert!(residue.atoms.is_empty());
272 }
273
274 #[test]
275 fn residue_new_with_insertion_code() {
276 let residue = Residue::new(
277 1,
278 Some('A'),
279 "ALA",
280 Some(StandardResidue::ALA),
281 ResidueCategory::Standard,
282 );
283
284 assert_eq!(residue.id, 1);
285 assert_eq!(residue.insertion_code, Some('A'));
286 }
287
288 #[test]
289 fn residue_new_with_none_standard_name() {
290 let residue = Residue::new(2, None, "UNK", None, ResidueCategory::Hetero);
291
292 assert_eq!(residue.id, 2);
293 assert_eq!(residue.name, "UNK");
294 assert_eq!(residue.standard_name, None);
295 assert_eq!(residue.category, ResidueCategory::Hetero);
296 }
297
298 #[test]
299 fn residue_is_standard_returns_true_for_standard_residue() {
300 let residue = Residue::new(
301 1,
302 None,
303 "ALA",
304 Some(StandardResidue::ALA),
305 ResidueCategory::Standard,
306 );
307 assert!(residue.is_standard());
308 }
309
310 #[test]
311 fn residue_is_standard_returns_false_for_non_standard_residue() {
312 let residue = Residue::new(2, None, "UNK", None, ResidueCategory::Hetero);
313 assert!(!residue.is_standard());
314 }
315
316 #[test]
317 fn residue_add_atom_adds_atom_correctly() {
318 let mut residue = Residue::new(
319 1,
320 None,
321 "ALA",
322 Some(StandardResidue::ALA),
323 ResidueCategory::Standard,
324 );
325 let atom = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
326
327 residue.add_atom(atom);
328
329 assert_eq!(residue.atom_count(), 1);
330 assert!(residue.has_atom("CA"));
331 assert_eq!(residue.atom("CA").unwrap().name, "CA");
332 }
333
334 #[test]
335 fn residue_remove_atom_removes_existing_atom() {
336 let mut residue = Residue::new(
337 1,
338 None,
339 "ALA",
340 Some(StandardResidue::ALA),
341 ResidueCategory::Standard,
342 );
343 let atom = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
344 residue.add_atom(atom);
345
346 let removed = residue.remove_atom("CA");
347
348 assert!(removed.is_some());
349 assert_eq!(removed.unwrap().name, "CA");
350 assert_eq!(residue.atom_count(), 0);
351 assert!(!residue.has_atom("CA"));
352 }
353
354 #[test]
355 fn residue_remove_atom_returns_none_for_nonexistent_atom() {
356 let mut residue = Residue::new(
357 1,
358 None,
359 "ALA",
360 Some(StandardResidue::ALA),
361 ResidueCategory::Standard,
362 );
363
364 let removed = residue.remove_atom("NONEXISTENT");
365
366 assert!(removed.is_none());
367 }
368
369 #[test]
370 fn residue_atom_returns_correct_atom() {
371 let mut residue = Residue::new(
372 1,
373 None,
374 "ALA",
375 Some(StandardResidue::ALA),
376 ResidueCategory::Standard,
377 );
378 let atom = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
379 residue.add_atom(atom);
380
381 let retrieved = residue.atom("CA");
382
383 assert!(retrieved.is_some());
384 assert_eq!(retrieved.unwrap().name, "CA");
385 }
386
387 #[test]
388 fn residue_atom_returns_none_for_nonexistent_atom() {
389 let residue = Residue::new(
390 1,
391 None,
392 "ALA",
393 Some(StandardResidue::ALA),
394 ResidueCategory::Standard,
395 );
396
397 let retrieved = residue.atom("NONEXISTENT");
398
399 assert!(retrieved.is_none());
400 }
401
402 #[test]
403 fn residue_atom_mut_returns_correct_mutable_atom() {
404 let mut residue = Residue::new(
405 1,
406 None,
407 "ALA",
408 Some(StandardResidue::ALA),
409 ResidueCategory::Standard,
410 );
411 let atom = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
412 residue.add_atom(atom);
413
414 let retrieved = residue.atom_mut("CA");
415
416 assert!(retrieved.is_some());
417 assert_eq!(retrieved.unwrap().name, "CA");
418 }
419
420 #[test]
421 fn residue_atom_mut_returns_none_for_nonexistent_atom() {
422 let mut residue = Residue::new(
423 1,
424 None,
425 "ALA",
426 Some(StandardResidue::ALA),
427 ResidueCategory::Standard,
428 );
429
430 let retrieved = residue.atom_mut("NONEXISTENT");
431
432 assert!(retrieved.is_none());
433 }
434
435 #[test]
436 fn residue_has_atom_returns_true_for_existing_atom() {
437 let mut residue = Residue::new(
438 1,
439 None,
440 "ALA",
441 Some(StandardResidue::ALA),
442 ResidueCategory::Standard,
443 );
444 let atom = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
445 residue.add_atom(atom);
446
447 assert!(residue.has_atom("CA"));
448 }
449
450 #[test]
451 fn residue_has_atom_returns_false_for_nonexistent_atom() {
452 let residue = Residue::new(
453 1,
454 None,
455 "ALA",
456 Some(StandardResidue::ALA),
457 ResidueCategory::Standard,
458 );
459
460 assert!(!residue.has_atom("NONEXISTENT"));
461 }
462
463 #[test]
464 fn residue_atoms_returns_correct_slice() {
465 let mut residue = Residue::new(
466 1,
467 None,
468 "ALA",
469 Some(StandardResidue::ALA),
470 ResidueCategory::Standard,
471 );
472 let atom1 = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
473 let atom2 = Atom::new("CB", Element::C, Point::new(1.0, 0.0, 0.0));
474 residue.add_atom(atom1);
475 residue.add_atom(atom2);
476
477 let atoms = residue.atoms();
478
479 assert_eq!(atoms.len(), 2);
480 assert_eq!(atoms[0].name, "CA");
481 assert_eq!(atoms[1].name, "CB");
482 }
483
484 #[test]
485 fn residue_atom_count_returns_correct_count() {
486 let mut residue = Residue::new(
487 1,
488 None,
489 "ALA",
490 Some(StandardResidue::ALA),
491 ResidueCategory::Standard,
492 );
493
494 assert_eq!(residue.atom_count(), 0);
495
496 let atom = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
497 residue.add_atom(atom);
498
499 assert_eq!(residue.atom_count(), 1);
500 }
501
502 #[test]
503 fn residue_is_empty_returns_true_for_empty_residue() {
504 let residue = Residue::new(
505 1,
506 None,
507 "ALA",
508 Some(StandardResidue::ALA),
509 ResidueCategory::Standard,
510 );
511
512 assert!(residue.is_empty());
513 }
514
515 #[test]
516 fn residue_is_empty_returns_false_for_non_empty_residue() {
517 let mut residue = Residue::new(
518 1,
519 None,
520 "ALA",
521 Some(StandardResidue::ALA),
522 ResidueCategory::Standard,
523 );
524 let atom = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
525 residue.add_atom(atom);
526
527 assert!(!residue.is_empty());
528 }
529
530 #[test]
531 fn residue_iter_atoms_iterates_correctly() {
532 let mut residue = Residue::new(
533 1,
534 None,
535 "ALA",
536 Some(StandardResidue::ALA),
537 ResidueCategory::Standard,
538 );
539 let atom1 = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
540 let atom2 = Atom::new("CB", Element::C, Point::new(1.0, 0.0, 0.0));
541 residue.add_atom(atom1);
542 residue.add_atom(atom2);
543
544 let mut names = Vec::new();
545 for atom in residue.iter_atoms() {
546 names.push(atom.name.clone());
547 }
548
549 assert_eq!(names, vec!["CA", "CB"]);
550 }
551
552 #[test]
553 fn residue_iter_atoms_mut_iterates_correctly() {
554 let mut residue = Residue::new(
555 1,
556 None,
557 "ALA",
558 Some(StandardResidue::ALA),
559 ResidueCategory::Standard,
560 );
561 let atom1 = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
562 residue.add_atom(atom1);
563
564 for atom in residue.iter_atoms_mut() {
565 atom.translate_by(&nalgebra::Vector3::new(1.0, 0.0, 0.0));
566 }
567
568 assert!((residue.atom("CA").unwrap().pos.x - 1.0).abs() < 1e-10);
569 }
570
571 #[test]
572 fn residue_strip_hydrogens_removes_hydrogen_atoms() {
573 let mut residue = Residue::new(
574 1,
575 None,
576 "ALA",
577 Some(StandardResidue::ALA),
578 ResidueCategory::Standard,
579 );
580 let carbon = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
581 let hydrogen1 = Atom::new("HA", Element::H, Point::new(1.0, 0.0, 0.0));
582 let hydrogen2 = Atom::new("HB", Element::H, Point::new(2.0, 0.0, 0.0));
583 residue.add_atom(carbon);
584 residue.add_atom(hydrogen1);
585 residue.add_atom(hydrogen2);
586
587 residue.strip_hydrogens();
588
589 assert_eq!(residue.atom_count(), 1);
590 assert!(residue.has_atom("CA"));
591 assert!(!residue.has_atom("HA"));
592 assert!(!residue.has_atom("HB"));
593 }
594
595 #[test]
596 fn residue_strip_hydrogens_preserves_non_hydrogen_atoms() {
597 let mut residue = Residue::new(
598 1,
599 None,
600 "ALA",
601 Some(StandardResidue::ALA),
602 ResidueCategory::Standard,
603 );
604 let carbon = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
605 let nitrogen = Atom::new("N", Element::N, Point::new(1.0, 0.0, 0.0));
606 let oxygen = Atom::new("O", Element::O, Point::new(2.0, 0.0, 0.0));
607 residue.add_atom(carbon);
608 residue.add_atom(nitrogen);
609 residue.add_atom(oxygen);
610
611 residue.strip_hydrogens();
612
613 assert_eq!(residue.atom_count(), 3);
614 assert!(residue.has_atom("CA"));
615 assert!(residue.has_atom("N"));
616 assert!(residue.has_atom("O"));
617 }
618
619 #[test]
620 fn residue_display_formats_standard_residue_correctly() {
621 let residue = Residue::new(
622 1,
623 None,
624 "ALA",
625 Some(StandardResidue::ALA),
626 ResidueCategory::Standard,
627 );
628
629 let display = format!("{}", residue);
630 let expected =
631 "Residue { id: 1, name: \"ALA\" (ALA), category: Standard Residue, atoms: 0 }";
632
633 assert_eq!(display, expected);
634 }
635
636 #[test]
637 fn residue_display_formats_residue_with_insertion_code_correctly() {
638 let residue = Residue::new(
639 1,
640 Some('A'),
641 "ALA",
642 Some(StandardResidue::ALA),
643 ResidueCategory::Standard,
644 );
645
646 let display = format!("{}", residue);
647 let expected =
648 "Residue { id: 1 (ic: A), name: \"ALA\" (ALA), category: Standard Residue, atoms: 0 }";
649
650 assert_eq!(display, expected);
651 }
652
653 #[test]
654 fn residue_display_formats_non_standard_residue_correctly() {
655 let residue = Residue::new(2, None, "UNK", None, ResidueCategory::Hetero);
656
657 let display = format!("{}", residue);
658 let expected = "Residue { id: 2, name: \"UNK\", category: Hetero Residue, atoms: 0 }";
659
660 assert_eq!(display, expected);
661 }
662
663 #[test]
664 fn residue_display_includes_atom_count() {
665 let mut residue = Residue::new(
666 1,
667 None,
668 "ALA",
669 Some(StandardResidue::ALA),
670 ResidueCategory::Standard,
671 );
672 let atom1 = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
673 let atom2 = Atom::new("CB", Element::C, Point::new(1.0, 0.0, 0.0));
674 residue.add_atom(atom1);
675 residue.add_atom(atom2);
676
677 let display = format!("{}", residue);
678 let expected =
679 "Residue { id: 1, name: \"ALA\" (ALA), category: Standard Residue, atoms: 2 }";
680
681 assert_eq!(display, expected);
682 }
683
684 #[test]
685 fn residue_clone_creates_identical_copy() {
686 let mut residue = Residue::new(
687 1,
688 Some('A'),
689 "ALA",
690 Some(StandardResidue::ALA),
691 ResidueCategory::Standard,
692 );
693 let atom = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
694 residue.add_atom(atom);
695 residue.position = ResiduePosition::Internal;
696
697 let cloned = residue.clone();
698
699 assert_eq!(residue, cloned);
700 assert_eq!(residue.id, cloned.id);
701 assert_eq!(residue.insertion_code, cloned.insertion_code);
702 assert_eq!(residue.name, cloned.name);
703 assert_eq!(residue.standard_name, cloned.standard_name);
704 assert_eq!(residue.category, cloned.category);
705 assert_eq!(residue.position, cloned.position);
706 assert_eq!(residue.atoms, cloned.atoms);
707 }
708
709 #[test]
710 fn residue_partial_eq_compares_correctly() {
711 let mut residue1 = Residue::new(
712 1,
713 None,
714 "ALA",
715 Some(StandardResidue::ALA),
716 ResidueCategory::Standard,
717 );
718 let mut residue2 = Residue::new(
719 1,
720 None,
721 "ALA",
722 Some(StandardResidue::ALA),
723 ResidueCategory::Standard,
724 );
725 let atom = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
726 residue1.add_atom(atom.clone());
727 residue2.add_atom(atom);
728
729 let residue3 = Residue::new(
730 2,
731 None,
732 "ALA",
733 Some(StandardResidue::ALA),
734 ResidueCategory::Standard,
735 );
736
737 assert_eq!(residue1, residue2);
738 assert_ne!(residue1, residue3);
739 }
740}