1use cyanea_core::{CyaneaError, Result, Summarizable};
8
9use crate::geometry::{angle_points, dihedral_points};
10use crate::types::{Chain, Point3D, Residue};
11
12#[allow(unused_imports)]
13use alloc::format;
14use alloc::string::String;
15use alloc::vec;
16use alloc::vec::Vec;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub enum SecondaryStructure {
22 Helix,
23 Sheet,
24 Turn,
25 Coil,
26}
27
28impl SecondaryStructure {
29 pub fn code(&self) -> char {
31 match self {
32 SecondaryStructure::Helix => 'H',
33 SecondaryStructure::Sheet => 'E',
34 SecondaryStructure::Turn => 'T',
35 SecondaryStructure::Coil => 'C',
36 }
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43pub enum DsspState {
44 H,
46 G,
48 I,
50 E,
52 B,
54 T,
56 S,
58 C,
60}
61
62impl DsspState {
63 pub fn code(&self) -> char {
65 match self {
66 DsspState::H => 'H',
67 DsspState::G => 'G',
68 DsspState::I => 'I',
69 DsspState::E => 'E',
70 DsspState::B => 'B',
71 DsspState::T => 'T',
72 DsspState::S => 'S',
73 DsspState::C => 'C',
74 }
75 }
76
77 pub fn to_simplified(&self) -> SecondaryStructure {
79 match self {
80 DsspState::H | DsspState::G | DsspState::I => SecondaryStructure::Helix,
81 DsspState::E | DsspState::B => SecondaryStructure::Sheet,
82 DsspState::T | DsspState::S => SecondaryStructure::Turn,
83 DsspState::C => SecondaryStructure::Coil,
84 }
85 }
86}
87
88#[derive(Debug, Clone)]
90#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
91pub struct DsspAssignment {
92 pub chain_id: char,
94 pub states: Vec<DsspState>,
96}
97
98impl DsspAssignment {
99 pub fn to_string_codes(&self) -> String {
101 self.states.iter().map(|s| s.code()).collect()
102 }
103
104 pub fn to_simplified(&self) -> SecondaryStructureAssignment {
106 SecondaryStructureAssignment {
107 chain_id: self.chain_id,
108 assignments: self.states.iter().map(|s| s.to_simplified()).collect(),
109 }
110 }
111
112 pub fn counts(&self) -> DsspCounts {
114 let mut counts = DsspCounts::default();
115 for s in &self.states {
116 match s {
117 DsspState::H => counts.h += 1,
118 DsspState::G => counts.g += 1,
119 DsspState::I => counts.i += 1,
120 DsspState::E => counts.e += 1,
121 DsspState::B => counts.b += 1,
122 DsspState::T => counts.t += 1,
123 DsspState::S => counts.s += 1,
124 DsspState::C => counts.c += 1,
125 }
126 }
127 counts
128 }
129}
130
131impl Summarizable for DsspAssignment {
132 fn summary(&self) -> String {
133 let c = self.counts();
134 format!(
135 "Chain {} DSSP: {} residue(s) — H:{} G:{} I:{} E:{} B:{} T:{} S:{} C:{}",
136 self.chain_id,
137 self.states.len(),
138 c.h, c.g, c.i, c.e, c.b, c.t, c.s, c.c,
139 )
140 }
141}
142
143#[derive(Debug, Clone, Default)]
145pub struct DsspCounts {
146 pub h: usize,
147 pub g: usize,
148 pub i: usize,
149 pub e: usize,
150 pub b: usize,
151 pub t: usize,
152 pub s: usize,
153 pub c: usize,
154}
155
156#[derive(Debug, Clone)]
158#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
159pub struct SecondaryStructureAssignment {
160 pub chain_id: char,
162 pub assignments: Vec<SecondaryStructure>,
164}
165
166impl SecondaryStructureAssignment {
167 pub fn counts(&self) -> (usize, usize, usize, usize) {
169 let mut h = 0;
170 let mut e = 0;
171 let mut t = 0;
172 let mut c = 0;
173 for ss in &self.assignments {
174 match ss {
175 SecondaryStructure::Helix => h += 1,
176 SecondaryStructure::Sheet => e += 1,
177 SecondaryStructure::Turn => t += 1,
178 SecondaryStructure::Coil => c += 1,
179 }
180 }
181 (h, e, t, c)
182 }
183
184 pub fn helix_fraction(&self) -> f64 {
186 if self.assignments.is_empty() {
187 return 0.0;
188 }
189 let (h, _, _, _) = self.counts();
190 h as f64 / self.assignments.len() as f64
191 }
192
193 pub fn sheet_fraction(&self) -> f64 {
195 if self.assignments.is_empty() {
196 return 0.0;
197 }
198 let (_, e, _, _) = self.counts();
199 e as f64 / self.assignments.len() as f64
200 }
201}
202
203impl Summarizable for SecondaryStructureAssignment {
204 fn summary(&self) -> String {
205 let (h, e, t, c) = self.counts();
206 format!(
207 "Chain {} SS: {} residue(s) — H:{} E:{} T:{} C:{}",
208 self.chain_id,
209 self.assignments.len(),
210 h,
211 e,
212 t,
213 c,
214 )
215 }
216}
217
218pub fn assign_secondary_structure(chain: &Chain) -> Result<SecondaryStructureAssignment> {
226 let n = chain.residues.len();
227 if n == 0 {
228 return Err(CyaneaError::InvalidInput(
229 "cannot assign SS to empty chain".into(),
230 ));
231 }
232
233 let mut assignments = vec![SecondaryStructure::Coil; n];
234
235 let backbone: Vec<Option<(Point3D, Point3D)>> = chain
237 .residues
238 .iter()
239 .map(|r| {
240 let n_atom = r.get_atom("N")?;
241 let o_atom = r.get_atom("O")?;
242 Some((n_atom.coords, o_atom.coords))
243 })
244 .collect();
245
246 let hbond_cutoff = 3.5;
247
248 let mut helix_hbond = vec![false; n];
250 for i in 0..n.saturating_sub(4) {
251 if let (Some((_, o_i)), Some((n_i4, _))) = (&backbone[i], &backbone[i + 4]) {
252 if o_i.distance_to(n_i4) < hbond_cutoff {
253 helix_hbond[i] = true;
254 }
255 }
256 }
257
258 for i in 0..n.saturating_sub(6) {
260 if helix_hbond[i] && helix_hbond[i + 1] && helix_hbond[i + 2] {
261 for j in i..=(i + 5).min(n - 1) {
263 assignments[j] = SecondaryStructure::Helix;
264 }
265 }
266 }
267
268 for i in 0..n {
270 for j in (i + 5)..n {
271 if let (Some((n_i, o_i)), Some((n_j, o_j))) = (&backbone[i], &backbone[j]) {
272 let oi_nj = o_i.distance_to(n_j);
273 let oj_ni = o_j.distance_to(n_i);
274 if oi_nj < hbond_cutoff && oj_ni < hbond_cutoff {
275 if assignments[i] == SecondaryStructure::Coil {
276 assignments[i] = SecondaryStructure::Sheet;
277 }
278 if assignments[j] == SecondaryStructure::Coil {
279 assignments[j] = SecondaryStructure::Sheet;
280 }
281 }
282 }
283 }
284 }
285
286 for i in 0..n.saturating_sub(3) {
288 if let (Some((_, o_i)), Some((n_i3, _))) = (&backbone[i], &backbone[i + 3]) {
289 if o_i.distance_to(n_i3) < hbond_cutoff {
290 for j in i..=(i + 3).min(n - 1) {
292 if assignments[j] == SecondaryStructure::Coil {
293 assignments[j] = SecondaryStructure::Turn;
294 }
295 }
296 }
297 }
298 }
299
300 Ok(SecondaryStructureAssignment {
301 chain_id: chain.id,
302 assignments,
303 })
304}
305
306pub fn dssp(chain: &Chain) -> Result<DsspAssignment> {
328 let n = chain.residues.len();
329 if n == 0 {
330 return Err(CyaneaError::InvalidInput(
331 "cannot assign DSSP to empty chain".into(),
332 ));
333 }
334
335 let backbone = extract_backbone(chain);
337
338 let hbond = compute_hbond_matrix(&backbone, n);
341
342 let mut states = vec![DsspState::C; n];
343
344 let turn3 = find_turns(&hbond, n, 3);
347 let turn4 = find_turns(&hbond, n, 4);
348 let turn5 = find_turns(&hbond, n, 5);
349
350 assign_helix(&turn4, n, 4, DsspState::H, &mut states);
352 assign_helix(&turn3, n, 3, DsspState::G, &mut states);
354 assign_helix(&turn5, n, 5, DsspState::I, &mut states);
356
357 let bridges = find_bridges(&hbond, n);
359 assign_bridges_and_strands(&bridges, n, &mut states);
360
361 assign_turns(&turn3, &turn4, &turn5, n, &mut states);
363
364 assign_bends(chain, &mut states);
366
367 Ok(DsspAssignment {
368 chain_id: chain.id,
369 states,
370 })
371}
372
373#[allow(dead_code)]
375struct BackboneAtoms {
376 n: Option<Point3D>,
377 ca: Option<Point3D>,
378 c: Option<Point3D>,
379 o: Option<Point3D>,
380}
381
382fn extract_backbone(chain: &Chain) -> Vec<BackboneAtoms> {
384 chain
385 .residues
386 .iter()
387 .map(|r| BackboneAtoms {
388 n: r.get_atom("N").map(|a| a.coords),
389 ca: r.get_atom("CA").map(|a| a.coords),
390 c: r.get_atom("C").map(|a| a.coords),
391 o: r.get_atom("O").map(|a| a.coords),
392 })
393 .collect()
394}
395
396fn hbond_energy(donor: &BackboneAtoms, acceptor: &BackboneAtoms, prev_c: Option<&Point3D>) -> f64 {
404 let n_pos = match donor.n {
406 Some(p) => p,
407 None => return 0.0,
408 };
409 let o_pos = match acceptor.o {
410 Some(p) => p,
411 None => return 0.0,
412 };
413 let c_pos = match acceptor.c {
414 Some(p) => p,
415 None => return 0.0,
416 };
417
418 let h_pos = match prev_c {
421 Some(c_prev) => {
422 let nc_dir = n_pos.sub(c_prev).normalize();
423 n_pos.add(&nc_dir.scale(1.0))
424 }
425 None => return 0.0, };
427
428 let r_on = n_pos.distance_to(&o_pos);
429 let r_ch = h_pos.distance_to(&c_pos);
430 let r_oh = h_pos.distance_to(&o_pos);
431 let r_cn = n_pos.distance_to(&c_pos);
432
433 if r_on < 0.5 || r_ch < 0.5 || r_oh < 0.5 || r_cn < 0.5 {
435 return 0.0;
436 }
437
438 0.084 * (1.0 / r_on + 1.0 / r_ch - 1.0 / r_oh - 1.0 / r_cn) * 332.0
439}
440
441fn compute_hbond_matrix(backbone: &[BackboneAtoms], n: usize) -> Vec<f64> {
445 let mut hbond = vec![0.0_f64; n * n];
446 let hbond_threshold = -0.5;
447
448 for i in 1..n {
449 let prev_c = backbone[i - 1].c.as_ref();
451
452 for j in 0..n {
453 if i == j {
454 continue;
455 }
456 if (i as isize - j as isize).unsigned_abs() < 2 {
459 continue;
460 }
461
462 let energy = hbond_energy(&backbone[i], &backbone[j], prev_c);
463 if energy < hbond_threshold {
464 hbond[i * n + j] = energy;
465 }
466 }
467 }
468
469 hbond
470}
471
472fn find_turns(hbond: &[f64], n: usize, turn_size: usize) -> Vec<bool> {
474 let mut turns = vec![false; n];
475 for i in 0..n.saturating_sub(turn_size) {
476 let donor = i + turn_size;
477 if donor < n && hbond[donor * n + i] < -0.5 {
478 turns[i] = true;
479 }
480 }
481 turns
482}
483
484fn assign_helix(
486 turns: &[bool],
487 n: usize,
488 turn_size: usize,
489 state: DsspState,
490 states: &mut [DsspState],
491) {
492 let min_consecutive = 2;
496 let mut consecutive = 0;
497
498 for i in 0..n.saturating_sub(turn_size) {
499 if turns[i] {
500 consecutive += 1;
501 if consecutive >= min_consecutive {
502 let start = (i + 2).saturating_sub(consecutive);
506 let end = (i + turn_size).min(n - 1);
507 for j in start..=end {
508 if states[j] == DsspState::C
511 || (state == DsspState::H && (states[j] == DsspState::G || states[j] == DsspState::I))
512 || (state == DsspState::G && states[j] == DsspState::I)
513 {
514 states[j] = state;
515 }
516 }
517 }
518 } else {
519 consecutive = 0;
520 }
521 }
522}
523
524#[derive(Debug, Clone, Copy)]
526#[allow(dead_code)]
527struct BetaBridge {
528 i: usize,
529 j: usize,
530 is_parallel: bool,
531}
532
533fn find_bridges(hbond: &[f64], n: usize) -> Vec<BetaBridge> {
535 let mut bridges = Vec::new();
536 let threshold = -0.5;
537
538 for i in 1..n.saturating_sub(1) {
539 for j in (i + 3)..n.saturating_sub(1) {
540 let anti = hbond[i * n + j] < threshold && hbond[j * n + i] < threshold;
542 let anti_alt1 = j > 0
545 && i + 1 < n
546 && hbond[i * n + (j - 1)] < threshold
547 && (j + 1) < n
548 && hbond[(j + 1) * n + i] < threshold;
549 let anti_alt2 = i > 0
550 && (i + 1) < n
551 && hbond[(i + 1) * n + j] < threshold
552 && hbond[j * n + (i - 1)] < threshold;
553
554 if anti || anti_alt1 || anti_alt2 {
555 bridges.push(BetaBridge {
556 i,
557 j,
558 is_parallel: false,
559 });
560 continue;
561 }
562
563 let para1 = i > 0
566 && hbond[(i - 1) * n + j] < threshold
567 && hbond[j * n + i] < threshold;
568 let para2 = j + 1 < n
570 && hbond[i * n + j] < threshold
571 && hbond[(j + 1) * n + i] < threshold;
572
573 if para1 || para2 {
574 bridges.push(BetaBridge {
575 i,
576 j,
577 is_parallel: true,
578 });
579 }
580 }
581 }
582
583 bridges
584}
585
586fn assign_bridges_and_strands(bridges: &[BetaBridge], _n: usize, states: &mut [DsspState]) {
588 for bridge in bridges {
590 if states[bridge.i] == DsspState::C {
592 states[bridge.i] = DsspState::B;
593 }
594 if states[bridge.j] == DsspState::C {
595 states[bridge.j] = DsspState::B;
596 }
597 }
598
599 let bridge_residues: Vec<bool> = states.iter().map(|s| *s == DsspState::B).collect();
603 let n = bridge_residues.len();
604
605 for i in 0..n {
606 if !bridge_residues[i] {
607 continue;
608 }
609 let has_neighbor = (i > 0 && bridge_residues[i - 1])
611 || (i + 1 < n && bridge_residues[i + 1]);
612 if has_neighbor {
613 states[i] = DsspState::E;
614 }
615 }
616
617 let strand_residues: Vec<bool> = states.iter().map(|s| *s == DsspState::E).collect();
619 for i in 0..n {
620 if states[i] == DsspState::B {
621 let has_e_neighbor = (i > 0 && strand_residues[i - 1])
622 || (i + 1 < n && strand_residues[i + 1]);
623 if has_e_neighbor {
624 states[i] = DsspState::E;
625 }
626 }
627 }
628}
629
630fn assign_turns(
632 turn3: &[bool],
633 turn4: &[bool],
634 turn5: &[bool],
635 n: usize,
636 states: &mut [DsspState],
637) {
638 for i in 0..n {
639 for (turns, turn_size) in [(turn3, 3usize), (turn4, 4usize), (turn5, 5usize)] {
641 if i < turns.len() && turns[i] {
642 let start = i + 1;
643 let end = (i + turn_size).min(n);
644 for j in start..end {
645 if states[j] == DsspState::C {
646 states[j] = DsspState::T;
647 }
648 }
649 }
650 }
651 }
652}
653
654fn assign_bends(chain: &Chain, states: &mut [DsspState]) {
661 let n = chain.residues.len();
662 if n < 5 {
663 return;
664 }
665
666 for i in 2..n.saturating_sub(2) {
667 if states[i] != DsspState::C {
668 continue;
669 }
670
671 let ca_prev2 = chain.residues[i - 2].get_atom("CA");
672 let ca_curr = chain.residues[i].get_atom("CA");
673 let ca_next2 = chain.residues[i + 2].get_atom("CA");
674
675 if let (Some(p1), Some(p2), Some(p3)) = (ca_prev2, ca_curr, ca_next2) {
676 let vertex_angle = angle_points(&p1.coords, &p2.coords, &p3.coords);
679 if vertex_angle < 110.0 {
680 states[i] = DsspState::S;
681 }
682 }
683 }
684}
685
686pub fn backbone_dihedrals(
694 prev: Option<&Residue>,
695 curr: &Residue,
696 next: Option<&Residue>,
697) -> Option<(f64, f64)> {
698 let n_i = curr.get_atom("N")?.coords;
699 let ca_i = curr.get_atom("CA")?.coords;
700 let c_i = curr.get_atom("C")?.coords;
701
702 let phi = if let Some(prev_res) = prev {
703 let c_prev = prev_res.get_atom("C")?.coords;
704 dihedral_points(&c_prev, &n_i, &ca_i, &c_i)
705 } else {
706 return None;
707 };
708
709 let psi = if let Some(next_res) = next {
710 let n_next = next_res.get_atom("N")?.coords;
711 dihedral_points(&n_i, &ca_i, &c_i, &n_next)
712 } else {
713 return None;
714 };
715
716 Some((phi, psi))
717}
718
719#[cfg(test)]
720mod tests {
721 use super::*;
722 use crate::types::{Atom, Chain, Residue};
723
724 fn make_atom(name: &str, x: f64, y: f64, z: f64) -> Atom {
725 Atom {
726 serial: 1,
727 name: name.into(),
728 alt_loc: None,
729 coords: Point3D::new(x, y, z),
730 occupancy: 1.0,
731 temp_factor: 0.0,
732 element: None,
733 charge: None,
734 is_hetatm: false,
735 }
736 }
737
738 fn make_helix_chain() -> Chain {
739 let n_residues = 12;
746 let rise = 1.5_f64; let r = 2.3_f64; let angle_per_res = 100.0_f64.to_radians(); let n_positions: Vec<Point3D> = (0..n_residues)
752 .map(|i| {
753 let angle = i as f64 * angle_per_res;
754 Point3D::new(r * angle.cos(), r * angle.sin(), i as f64 * rise)
755 })
756 .collect();
757
758 let mut residues = Vec::new();
759 for i in 0..n_residues {
760 let n_pos = n_positions[i];
761 let angle = i as f64 * angle_per_res;
762 let z = i as f64 * rise;
763
764 let ca_pos = Point3D::new(
765 r * (angle + 0.3).cos(),
766 r * (angle + 0.3).sin(),
767 z + 0.4,
768 );
769 let c_pos = Point3D::new(
770 r * (angle + 0.6).cos(),
771 r * (angle + 0.6).sin(),
772 z + 0.8,
773 );
774
775 let o_pos = if i + 4 < n_residues {
779 let target = n_positions[i + 4];
780 let dir = target.sub(&c_pos);
781 let d = dir.norm();
782 if d > 1e-10 {
783 c_pos.add(&dir.scale(2.5 / d))
784 } else {
785 Point3D::new(c_pos.x, c_pos.y + 1.24, c_pos.z)
786 }
787 } else {
788 Point3D::new(c_pos.x, c_pos.y + 1.24, c_pos.z)
789 };
790
791 residues.push(Residue {
792 name: "ALA".into(),
793 seq_num: i as i32 + 1,
794 i_code: None,
795 atoms: vec![
796 make_atom("N", n_pos.x, n_pos.y, n_pos.z),
797 make_atom("CA", ca_pos.x, ca_pos.y, ca_pos.z),
798 make_atom("C", c_pos.x, c_pos.y, c_pos.z),
799 make_atom("O", o_pos.x, o_pos.y, o_pos.z),
800 ],
801 });
802 }
803 Chain::new('A', residues)
804 }
805
806 #[test]
807 fn short_chain_all_coil() {
808 let chain = Chain::new(
809 'A',
810 vec![
811 Residue {
812 name: "ALA".into(),
813 seq_num: 1,
814 i_code: None,
815 atoms: vec![
816 make_atom("N", 0.0, 0.0, 0.0),
817 make_atom("CA", 1.5, 0.0, 0.0),
818 make_atom("C", 3.0, 0.0, 0.0),
819 make_atom("O", 3.5, 1.0, 0.0),
820 ],
821 },
822 Residue {
823 name: "GLY".into(),
824 seq_num: 2,
825 i_code: None,
826 atoms: vec![
827 make_atom("N", 4.0, 0.0, 0.0),
828 make_atom("CA", 5.5, 0.0, 0.0),
829 make_atom("C", 7.0, 0.0, 0.0),
830 make_atom("O", 7.5, 1.0, 0.0),
831 ],
832 },
833 ],
834 );
835 let ss = assign_secondary_structure(&chain).unwrap();
836 assert_eq!(ss.assignments.len(), 2);
837 for a in &ss.assignments {
839 assert!(
840 *a == SecondaryStructure::Coil || *a == SecondaryStructure::Turn,
841 "short chain should be coil/turn"
842 );
843 }
844 }
845
846 #[test]
847 fn helix_detection() {
848 let chain = make_helix_chain();
849 let ss = assign_secondary_structure(&chain).unwrap();
850 let (h, _, _, _) = ss.counts();
852 assert!(
853 h > 0,
854 "helix chain should have helical residues, got: {:?}",
855 ss.assignments
856 );
857 }
858
859 #[test]
860 fn sheet_detection() {
861 let mut residues = Vec::new();
863 for i in 0..5 {
865 let x = i as f64 * 3.5;
866 residues.push(Residue {
867 name: "ALA".into(),
868 seq_num: i + 1,
869 i_code: None,
870 atoms: vec![
871 make_atom("N", x, 0.0, 0.0),
872 make_atom("CA", x + 1.0, 0.0, 0.0),
873 make_atom("C", x + 2.0, 0.0, 0.0),
874 make_atom("O", x + 2.0, 1.0, 0.0),
875 ],
876 });
877 }
878 for i in 5..10 {
880 residues.push(Residue {
881 name: "ALA".into(),
882 seq_num: i + 1,
883 i_code: None,
884 atoms: vec![
885 make_atom("N", 50.0, 50.0, i as f64 * 10.0),
886 make_atom("CA", 51.0, 50.0, i as f64 * 10.0),
887 make_atom("C", 52.0, 50.0, i as f64 * 10.0),
888 make_atom("O", 52.0, 51.0, i as f64 * 10.0),
889 ],
890 });
891 }
892 for i in 0..5 {
895 let x = (4 - i) as f64 * 3.5;
896 residues.push(Residue {
897 name: "ALA".into(),
898 seq_num: (i + 11) as i32,
899 i_code: None,
900 atoms: vec![
901 make_atom("N", x + 2.0, 2.5, 0.0), make_atom("CA", x + 1.0, 2.5, 0.0),
903 make_atom("C", x, 2.5, 0.0),
904 make_atom("O", x, 1.5, 0.0), ],
906 });
907 }
908
909 let chain = Chain::new('A', residues);
910 let ss = assign_secondary_structure(&chain).unwrap();
911 let (_, e, _, _) = ss.counts();
912 assert!(e > 0, "should detect sheet residues, got: {:?}", ss.assignments);
913 }
914
915 #[test]
916 fn backbone_dihedral_computation() {
917 let prev = Residue {
918 name: "ALA".into(),
919 seq_num: 1,
920 i_code: None,
921 atoms: vec![
922 make_atom("N", -1.0, 0.0, 0.0),
923 make_atom("CA", 0.0, 0.0, 0.0),
924 make_atom("C", 1.0, 0.0, 0.0),
925 make_atom("O", 1.0, 1.0, 0.0),
926 ],
927 };
928 let curr = Residue {
929 name: "GLY".into(),
930 seq_num: 2,
931 i_code: None,
932 atoms: vec![
933 make_atom("N", 2.0, 0.0, 0.0),
934 make_atom("CA", 3.0, 0.0, 0.0),
935 make_atom("C", 4.0, 0.0, 0.0),
936 make_atom("O", 4.0, 1.0, 0.0),
937 ],
938 };
939 let next = Residue {
940 name: "ALA".into(),
941 seq_num: 3,
942 i_code: None,
943 atoms: vec![
944 make_atom("N", 5.0, 0.0, 0.0),
945 make_atom("CA", 6.0, 0.0, 0.0),
946 make_atom("C", 7.0, 0.0, 0.0),
947 make_atom("O", 7.0, 1.0, 0.0),
948 ],
949 };
950 let result = backbone_dihedrals(Some(&prev), &curr, Some(&next));
951 assert!(result.is_some(), "should compute phi/psi");
952 }
953
954 #[test]
957 fn dssp_empty_chain_error() {
958 let chain = Chain::new('A', vec![]);
959 assert!(dssp(&chain).is_err());
960 }
961
962 #[test]
963 fn dssp_short_chain_coil() {
964 let chain = Chain::new(
965 'A',
966 vec![
967 Residue {
968 name: "ALA".into(),
969 seq_num: 1,
970 i_code: None,
971 atoms: vec![
972 make_atom("N", 0.0, 0.0, 0.0),
973 make_atom("CA", 1.5, 0.0, 0.0),
974 make_atom("C", 3.0, 0.0, 0.0),
975 make_atom("O", 3.5, 1.0, 0.0),
976 ],
977 },
978 Residue {
979 name: "GLY".into(),
980 seq_num: 2,
981 i_code: None,
982 atoms: vec![
983 make_atom("N", 4.0, 0.0, 0.0),
984 make_atom("CA", 5.5, 0.0, 0.0),
985 make_atom("C", 7.0, 0.0, 0.0),
986 make_atom("O", 7.5, 1.0, 0.0),
987 ],
988 },
989 ],
990 );
991 let result = dssp(&chain).unwrap();
992 assert_eq!(result.states.len(), 2);
993 for s in &result.states {
995 assert!(
996 *s != DsspState::H && *s != DsspState::E,
997 "short chain should not be helix/strand"
998 );
999 }
1000 }
1001
1002 #[test]
1003 fn dssp_helix_chain() {
1004 let chain = make_helix_chain();
1005 let result = dssp(&chain).unwrap();
1006 assert_eq!(result.states.len(), 12);
1007 let has_structure = result.states.iter().any(|s| *s != DsspState::C);
1010 assert!(
1011 has_structure,
1012 "helix chain should have some structure assigned, got: {}",
1013 result.to_string_codes()
1014 );
1015 }
1016
1017 #[test]
1018 fn dssp_state_codes() {
1019 assert_eq!(DsspState::H.code(), 'H');
1020 assert_eq!(DsspState::G.code(), 'G');
1021 assert_eq!(DsspState::I.code(), 'I');
1022 assert_eq!(DsspState::E.code(), 'E');
1023 assert_eq!(DsspState::B.code(), 'B');
1024 assert_eq!(DsspState::T.code(), 'T');
1025 assert_eq!(DsspState::S.code(), 'S');
1026 assert_eq!(DsspState::C.code(), 'C');
1027 }
1028
1029 #[test]
1030 fn dssp_to_simplified_mapping() {
1031 assert_eq!(DsspState::H.to_simplified(), SecondaryStructure::Helix);
1032 assert_eq!(DsspState::G.to_simplified(), SecondaryStructure::Helix);
1033 assert_eq!(DsspState::I.to_simplified(), SecondaryStructure::Helix);
1034 assert_eq!(DsspState::E.to_simplified(), SecondaryStructure::Sheet);
1035 assert_eq!(DsspState::B.to_simplified(), SecondaryStructure::Sheet);
1036 assert_eq!(DsspState::T.to_simplified(), SecondaryStructure::Turn);
1037 assert_eq!(DsspState::S.to_simplified(), SecondaryStructure::Turn);
1038 assert_eq!(DsspState::C.to_simplified(), SecondaryStructure::Coil);
1039 }
1040
1041 #[test]
1042 fn dssp_assignment_to_simplified() {
1043 let chain = make_helix_chain();
1044 let dssp_result = dssp(&chain).unwrap();
1045 let simplified = dssp_result.to_simplified();
1046 assert_eq!(simplified.chain_id, 'A');
1047 assert_eq!(simplified.assignments.len(), 12);
1048 }
1049
1050 #[test]
1051 fn dssp_string_codes() {
1052 let assignment = DsspAssignment {
1053 chain_id: 'A',
1054 states: vec![DsspState::H, DsspState::H, DsspState::C, DsspState::E],
1055 };
1056 assert_eq!(assignment.to_string_codes(), "HHCE");
1057 }
1058
1059 #[test]
1060 fn dssp_counts() {
1061 let assignment = DsspAssignment {
1062 chain_id: 'A',
1063 states: vec![
1064 DsspState::H,
1065 DsspState::H,
1066 DsspState::G,
1067 DsspState::E,
1068 DsspState::B,
1069 DsspState::T,
1070 DsspState::S,
1071 DsspState::C,
1072 ],
1073 };
1074 let counts = assignment.counts();
1075 assert_eq!(counts.h, 2);
1076 assert_eq!(counts.g, 1);
1077 assert_eq!(counts.i, 0);
1078 assert_eq!(counts.e, 1);
1079 assert_eq!(counts.b, 1);
1080 assert_eq!(counts.t, 1);
1081 assert_eq!(counts.s, 1);
1082 assert_eq!(counts.c, 1);
1083 }
1084
1085 #[test]
1086 fn dssp_bend_detection() {
1087 let mut residues = Vec::new();
1092 let positions = vec![
1093 (0.0, 0.0, 0.0),
1094 (3.8, 0.0, 0.0),
1095 (7.6, 0.0, 0.0),
1096 (7.6, 3.8, 0.0),
1097 (7.6, 7.6, 0.0),
1098 ];
1099 for (i, (x, y, z)) in positions.iter().enumerate() {
1100 residues.push(Residue {
1101 name: "ALA".into(),
1102 seq_num: i as i32 + 1,
1103 i_code: None,
1104 atoms: vec![
1105 make_atom("N", x - 0.5, *y, *z),
1106 make_atom("CA", *x, *y, *z),
1107 make_atom("C", x + 0.5, *y, *z),
1108 make_atom("O", x + 0.5, y + 1.0, *z),
1109 ],
1110 });
1111 }
1112 let chain = Chain::new('A', residues);
1113 let result = dssp(&chain).unwrap();
1114 assert_eq!(
1116 result.states[2],
1117 DsspState::S,
1118 "residue at bend should be S, got: {}",
1119 result.to_string_codes()
1120 );
1121 }
1122}