use core::f64;
use std::collections::{HashMap, HashSet};
use itertools::Itertools;
use kd_tree::KdTree;
use nalgebra::Vector3;
use pdbtbx::{Atom, Element, PDB, ReadOptions};
use pdbtbx::{PDBError, Residue};
use rand::SeedableRng;
use rand::prelude::IndexedRandom;
use rand::rngs::StdRng;
use std::fs::File;
use std::io::BufReader;
use std::io::Cursor;
use std::io::Write;
#[derive(Clone)]
pub struct Bead {
pub position: Vector3<f64>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Pair {
pub chain_i: String,
pub res_i: isize,
pub atom_i: String,
pub chain_j: String,
pub res_j: isize,
pub atom_j: String,
pub distance: f64,
}
struct Finder {
chain_lookup: HashMap<usize, String>,
residue_lookup: HashMap<usize, isize>,
}
impl Finder {
fn new(pdb: &PDB) -> Self {
let chain_lookup = pdb
.chains()
.flat_map(|chain| {
let chain_id = chain.id().to_string();
chain.residues().flat_map(move |residue| {
residue.atoms().map({
let v = chain_id.clone();
move |atom| (atom.serial_number(), v.clone())
})
})
})
.collect();
let residue_lookup = pdb
.chains()
.flat_map(|chain| {
chain.residues().flat_map(|residue| {
let res_num = residue.serial_number();
residue
.atoms()
.map(move |atom| (atom.serial_number(), res_num))
})
})
.collect();
Finder {
chain_lookup,
residue_lookup,
}
}
fn find_chain_id(&self, atom: &Atom) -> String {
self.chain_lookup
.get(&atom.serial_number())
.unwrap_or(&"A".to_string())
.clone()
}
fn find_residue_number(&self, atom: &Atom) -> isize {
*self.residue_lookup.get(&atom.serial_number()).unwrap_or(&0)
}
}
pub fn neighbor_search(
pdb: pdbtbx::PDB,
target_residues_numbers: Vec<&pdbtbx::Residue>,
radius: f64,
) -> Vec<isize> {
let points: Vec<[f64; 3]> = pdb
.atoms()
.map(|atom| {
let x = atom.x();
let y = atom.y();
let z = atom.z();
[x, y, z]
})
.collect();
let kdtree = KdTree::build_by_ordered_float(points.clone());
let mut result: HashSet<isize> = HashSet::new();
for residue in &target_residues_numbers {
let query_vec: Vec<[f64; 3]> = residue
.atoms()
.map(|atom| {
let x = atom.x();
let y = atom.y();
let z = atom.z();
[x, y, z]
})
.collect();
for query in query_vec {
let found_vec = kdtree.within_radius(&query, radius);
for found in found_vec {
for residue in pdb.residues() {
for atom in residue.atoms() {
let x = atom.x();
let y = atom.y();
let z = atom.z();
if x == found[0] && y == found[1] && z == found[2] {
result.insert(residue.serial_number());
}
}
}
}
}
}
for residue in target_residues_numbers {
result.remove(&residue.serial_number());
}
let mut result: Vec<isize> = result.into_iter().collect();
result.sort();
result
}
pub fn get_residues(pdb: &pdbtbx::PDB, target: Vec<isize>) -> Vec<&Residue> {
let mut result: Vec<&Residue> = Vec::new();
for residue in pdb.residues() {
if target.contains(&residue.serial_number()) {
result.push(residue);
}
}
result
}
pub fn get_true_interface(pdb: &pdbtbx::PDB, cutoff: f64) -> HashMap<String, HashSet<isize>> {
let mut true_interface: HashMap<String, HashSet<isize>> = HashMap::new();
for chain_i in pdb.chains() {
for chain_j in pdb.chains() {
if chain_i.id() == chain_j.id() {
continue;
}
for res_i in chain_i.residues() {
for res_j in chain_j.residues() {
for atom_i in res_i.atoms() {
for atom_j in res_j.atoms() {
let distance = atom_i.distance(atom_j);
if distance < cutoff {
true_interface
.entry(chain_i.id().to_string())
.or_default()
.insert(res_i.serial_number());
true_interface
.entry(chain_j.id().to_string())
.or_default()
.insert(res_j.serial_number());
break;
}
}
}
}
}
}
}
true_interface
}
pub fn get_chains_in_contact(pdb: &pdbtbx::PDB, cutoff: f64) -> HashSet<(String, String)> {
let mut chains_in_contact: HashSet<(String, String)> = HashSet::new();
for chain_i in pdb.chains() {
for chain_j in pdb.chains() {
if chain_i.id() == chain_j.id() {
continue;
}
for res_i in chain_i.residues() {
for res_j in chain_j.residues() {
for atom_i in res_i.atoms() {
for atom_j in res_j.atoms() {
let distance = atom_i.distance(atom_j);
if distance < cutoff {
chains_in_contact
.insert((chain_i.id().to_string(), chain_j.id().to_string()));
break;
}
}
}
}
}
}
}
chains_in_contact
}
#[derive(Debug)]
pub struct Gap {
pub chain: String,
pub atom_i: String,
pub atom_j: String,
pub res_i: isize,
pub res_j: isize,
pub distance: f64,
}
pub fn filter_unique_by_atom_j(gaps: Vec<Gap>) -> Vec<Gap> {
let mut seen = HashSet::new();
let mut unique: Vec<Gap> = Vec::new();
gaps.into_iter().for_each(|g| {
if !seen.contains(&g.atom_j) {
seen.insert(g.atom_j.clone());
unique.push(g)
}
});
unique
}
pub fn find_bodies(pdb: &pdbtbx::PDB) -> HashMap<isize, Vec<(isize, &str, &pdbtbx::Atom)>> {
let mut ca_atoms: Vec<(&str, isize, &pdbtbx::Atom)> = Vec::new();
pdb.chains().for_each(|chain| {
chain.residues().for_each(|residue| {
residue.atoms().for_each(|atom| {
if atom.name() == "CA" {
ca_atoms.push((chain.id(), residue.serial_number(), atom));
}
});
});
});
let mut bodies: HashMap<isize, Vec<(isize, &str, &pdbtbx::Atom)>> = HashMap::new();
let mut body_id = 0;
for (i, j) in ca_atoms.iter().zip(ca_atoms.iter().skip(1)) {
let (chain_i, res_i, atom_i) = i;
let (chain_j, _res_j, atom_j) = j;
if chain_i != chain_j {
continue;
}
let distance = atom_i.distance(atom_j);
if distance > 4.0 {
body_id += 1;
}
bodies
.entry(body_id)
.or_default()
.push((*res_i, chain_i, atom_i));
}
bodies
}
pub fn create_iter_body_gaps(
bodies: &HashMap<isize, Vec<(isize, &str, &pdbtbx::Atom)>>,
) -> Vec<Gap> {
let mut rng = StdRng::seed_from_u64(42);
let mut pairs: Vec<Gap> = Vec::new();
let mut body_ids: Vec<isize> = bodies.keys().cloned().collect();
body_ids.sort();
for (i, &body_id1) in body_ids.iter().enumerate() {
for &body_id2 in body_ids[i + 1..].iter() {
if let (Some(atoms1), Some(atoms2)) = (bodies.get(&body_id1), bodies.get(&body_id2))
&& atoms1.len() >= 2 && atoms2.len() >= 2 {
let selected1: Vec<_> = atoms1.choose_multiple(&mut rng, 2).cloned().collect();
let selected2: Vec<_> = atoms2.choose_multiple(&mut rng, 2).cloned().collect();
for i in 0..2 {
pairs.push(Gap {
chain: selected1[i].1.to_string(),
atom_i: selected1[i].2.name().to_string(),
atom_j: selected2[i].2.name().to_string(),
res_i: selected1[i].0,
res_j: selected2[i].0,
distance: selected1[i].2.distance(selected2[i].2),
});
}
}
}
}
pairs
}
pub fn calculate_geometric_center(atoms: &[pdbtbx::Atom]) -> Vector3<f64> {
let mut center = Vector3::zeros();
for atom in atoms {
center += Vector3::new(atom.x(), atom.y(), atom.z());
}
center / (atoms.len() as f64)
}
pub fn generate_grid_beads(center_z: f64, grid_size: usize, spacing: f64) -> Vec<Bead> {
let mut beads = Vec::with_capacity(grid_size * grid_size);
let grid_offset = (grid_size as f64 - 1.0) * spacing / 2.0;
for i in 0..grid_size {
for j in 0..grid_size {
let x = (i as f64) * spacing - grid_offset;
let y = (j as f64) * spacing - grid_offset;
let final_pos = Vector3::new(x, y, center_z);
beads.push(Bead {
position: final_pos,
});
}
}
beads
}
pub fn write_beads_pdb(beads: &[Bead], filename: &str) -> std::io::Result<()> {
let mut file = File::create(filename)?;
for (i, bead) in beads.iter().enumerate() {
writeln!(
file,
"{:<6}{:>5} {:<4}{:1}{:<3} {:1}{:>4}{:1} {:>8.3}{:>8.3}{:>8.3}{:>6.2}{:>6.2} {:>2}{:>2}",
"ATOM",
i + 1,
"SHA",
" ",
"SHA",
"S",
i + 1,
" ",
bead.position.x,
bead.position.y,
bead.position.z,
1.00,
1.00,
"H",
""
)?;
}
writeln!(file, "END")?;
Ok(())
}
pub fn find_furthest_selections(selections: &[Vec<isize>], pdb: &PDB) -> (Vec<Atom>, Vec<Atom>) {
let sele: HashMap<usize, (Vec<Atom>, Vector3<f64>)> = selections
.iter()
.enumerate()
.map(|(i, sel)| {
let atoms = get_atoms_from_resnumbers(pdb, sel);
let center = calculate_geometric_center(&atoms);
(i, (atoms, center))
})
.collect();
let mut max_distance = 0.0;
let mut atoms1 = Vec::new();
let mut atoms2 = Vec::new();
for (i, j) in (0..selections.len()).tuple_combinations() {
let distance = (sele[&i].1 - sele[&j].1).norm();
if distance > max_distance {
max_distance = distance;
atoms1 = sele[&i].0.clone();
atoms2 = sele[&j].0.clone();
}
}
(atoms1, atoms2)
}
pub fn get_atoms_from_resnumbers(pdb: &PDB, selection: &[isize]) -> Vec<Atom> {
pdb.residues()
.filter(|res| selection.contains(&res.serial_number()))
.flat_map(|res| res.atoms())
.cloned() .collect()
}
pub fn load_pdb(input_pdb: &str) -> Result<PDB, Vec<PDBError>> {
std::fs::read_to_string(input_pdb)
.map_err(|e| {
vec![PDBError::new(
pdbtbx::ErrorLevel::GeneralWarning,
"File read error",
format!("Failed to read file {}: {}", input_pdb, e),
pdbtbx::Context::None,
)]
})
.and_then(|content| process_pdb_contents(&content))
}
pub fn load_pdb_from_content(content: &str) -> Result<PDB, Vec<PDBError>> {
process_pdb_contents(content)
}
pub fn process_pdb_contents(pdb_content: &str) -> Result<PDB, Vec<PDBError>> {
let processed_content = process_pdb_string(pdb_content);
let mut opts = ReadOptions::new();
opts.set_format(pdbtbx::Format::Pdb)
.set_level(pdbtbx::StrictnessLevel::Loose);
let cursor = Cursor::new(processed_content.into_bytes());
let reader = BufReader::new(cursor);
match opts.read_raw(reader) {
Ok((pdb, _)) => Ok(pdb),
Err(e) => Err(e),
}
}
fn process_pdb_string(content: &str) -> String {
let mut output = String::with_capacity(content.len());
for line in content.lines() {
if !line.starts_with("REMARK") {
output.push_str(line);
if line.len() < 80 {
output.push_str(&" ".repeat(80 - line.len()));
}
output.push('\n');
}
}
output
}
pub fn get_closest_residue_pairs(pdb: &pdbtbx::PDB, cutoff: f64) -> Vec<Pair> {
let mut closest_pairs = Vec::new();
let chains: Vec<_> = pdb.chains().collect();
for i in 0..chains.len() {
for j in (i + 1)..chains.len() {
let chain_i = &chains[i];
let chain_j = &chains[j];
for res_i in chain_i.residues() {
let mut min_distance = f64::MAX;
let mut closest_pair = None;
for res_j in chain_j.residues() {
let mut atom_dist = f64::MAX;
for atom_i in res_i.atoms() {
for atom_j in res_j.atoms() {
let distance = atom_i.distance(atom_j);
if distance < atom_dist {
atom_dist = distance;
}
}
}
if atom_dist < min_distance {
min_distance = atom_dist;
closest_pair = Some((res_j, res_i));
}
}
if min_distance < cutoff
&& let Some((res_j, res_i)) = closest_pair {
let ca_i = res_i.atoms().find(|atom| atom.name() == "CA").unwrap();
let ca_j = res_j.atoms().find(|atom| atom.name() == "CA").unwrap();
let ca_ca_dist = ca_i.distance(ca_j);
closest_pairs.push(Pair {
chain_i: chain_i.id().to_string(),
res_i: res_i.serial_number(),
atom_i: ca_i.name().to_string(),
chain_j: chain_j.id().to_string(),
res_j: res_j.serial_number(),
atom_j: ca_j.name().to_string(),
distance: ca_ca_dist,
});
}
}
}
}
closest_pairs.sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap());
closest_pairs
}
pub fn gaps_around_ligand(pdb: &PDB) -> Vec<Gap> {
let ligand_atoms: Vec<&Atom> = pdb
.atoms()
.filter(|a| a.hetero() && *a.element().unwrap() != Element::H)
.collect();
let ligand_residues: Vec<&Residue> = pdb
.residues()
.filter(|r| r.atoms().any(|a| a.hetero()))
.collect();
let neighbors = neighbor_search(pdb.clone(), ligand_residues, 5.0);
let protein_atoms = get_atoms_from_resnumbers(pdb, &neighbors);
let mut result: Vec<Gap> = Vec::new();
let finder = Finder::new(pdb);
for ligand_atom in ligand_atoms {
let closest_protein_atom = protein_atoms
.iter()
.filter(|a| *a.element().unwrap() != Element::H)
.map(|a| (a, ligand_atom.distance(a)))
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
.unwrap();
let ligand_chain = finder.find_chain_id(ligand_atom);
let protein_chain = finder.find_chain_id(closest_protein_atom.0);
if ligand_chain == protein_chain {
result.push(Gap {
chain: ligand_chain,
res_i: finder.find_residue_number(ligand_atom),
atom_i: ligand_atom.name().to_string(),
res_j: finder.find_residue_number(closest_protein_atom.0),
atom_j: closest_protein_atom.0.name().to_string(),
distance: closest_protein_atom.1,
})
}
}
result
}
#[cfg(test)]
mod tests {
use std::env;
use tempfile::NamedTempFile;
use super::*;
#[test]
fn test_get_true_interface() {
let pdb = load_pdb("tests/data/complex.pdb").unwrap();
let observed_true_interface = get_true_interface(&pdb, 5.0);
let mut expected_true_interface: HashMap<String, HashSet<isize>> = HashMap::new();
let chain_a = "A".to_string();
let res_a: HashSet<isize> = vec![934, 936, 933, 946, 950, 938, 940, 941, 937, 931]
.into_iter()
.collect();
let chain_b = "B";
let res_b: HashSet<isize> = vec![66, 48, 68, 49, 46, 45, 44, 47, 69, 6, 70, 8, 42]
.into_iter()
.collect();
expected_true_interface.insert(chain_a, res_a);
expected_true_interface.insert(chain_b.to_string(), res_b);
assert_eq!(observed_true_interface, expected_true_interface);
}
#[test]
fn test_load_pdb_short_lines() {
let pdb_path = env::current_dir()
.unwrap()
.join("tests/data/leu_short_lines.pdb");
let pdb = load_pdb(pdb_path.to_str().unwrap()).unwrap();
assert_eq!(pdb.atoms().count(), 8);
}
#[test]
fn test_load_pdb_normal_lines() {
let pdb_path = env::current_dir()
.unwrap()
.join("tests/data/leu_normal_lines.pdb");
let pdb = load_pdb(pdb_path.to_str().unwrap()).unwrap();
assert_eq!(pdb.atoms().count(), 8);
}
fn create_test_pdb(remarks: usize, atoms: usize) -> String {
let mut pdb = String::new();
for i in 0..remarks {
pdb.push_str(&format!("REMARK {} Test remark\n", i));
}
for i in 0..atoms {
pdb.push_str(&format!(
"ATOM {:>5} N ALA A{:>4} {:>8}{:>8}{:>8} 1.00 0.00 N\n",
i + 1,
i + 1,
format!("{:.3}", i as f32),
format!("{:.3}", i as f32),
format!("{:.3}", i as f32)
));
}
pdb
}
#[test]
fn test_process_pdb_string_removes_remarks() {
let input = "REMARK 1 Test\nATOM 1 N ALA A 1 0.000 0.000 0.000\n";
let processed = process_pdb_string(input);
assert!(!processed.contains("REMARK"));
assert!(processed.contains("ATOM"));
}
#[test]
fn test_process_pdb_string_pads_lines() {
let short_line = "ATOM 1 N ALA A 1 0.000 0.000 0.000";
let processed = process_pdb_string(short_line);
assert_eq!(processed.lines().next().unwrap().len(), 80);
assert_eq!(processed.chars().count(), 81); }
#[test]
fn test_process_pdb_string_preserves_long_lines() {
let long_line =
"ATOM 1 N ALA A 1 0.000 0.000 0.000 1.00 0.00 N EXTRA";
let processed = process_pdb_string(long_line);
assert_eq!(processed.trim(), long_line);
}
#[test]
fn test_process_pdb_contents_valid_pdb() {
let pdb_content = create_test_pdb(3, 5);
let result = process_pdb_contents(&pdb_content);
assert!(result.is_ok());
let pdb = result.unwrap();
assert_eq!(pdb.atoms().count(), 5);
}
#[test]
fn test_process_pdb_contents_empty_input() {
let result = process_pdb_contents("");
assert!(result.is_err());
if let Err(errors) = result {
assert_eq!(errors.len(), 1);
}
}
#[test]
fn test_process_pdb_contents_invalid_pdb() {
let invalid_content = "This is not a PDB file\n";
let result = process_pdb_contents(invalid_content);
assert!(result.is_err());
if let Err(errors) = result {
assert!(!errors.is_empty());
}
}
#[test]
fn test_process_pdb_string_empty_input() {
let processed = process_pdb_string("");
assert_eq!(processed, "");
}
#[test]
fn test_process_pdb_string_mixed_content() {
let input = "\
REMARK 1 Test
ATOM 1 N ALA A 1 0.000 0.000 0.000
REMARK 2 Another
ATOM 2 N ALA A 2 1.000 1.000 1.000
";
let processed = process_pdb_string(input);
let lines: Vec<&str> = processed.lines().collect();
assert_eq!(lines.len(), 2); assert!(lines[0].starts_with("ATOM"));
assert!(lines[1].starts_with("ATOM"));
}
#[test]
fn test_process_pdb_contents_with_real_file() {
let mut file = NamedTempFile::new().unwrap();
let pdb_content = create_test_pdb(2, 3);
write!(file, "{}", pdb_content).unwrap();
let file_content = std::fs::read_to_string(file.path()).unwrap();
let result = process_pdb_contents(&file_content);
assert!(result.is_ok());
let pdb = result.unwrap();
assert_eq!(pdb.atoms().count(), 3);
}
#[test]
fn test_get_closest_residue_pairs() {
let pdb = load_pdb("tests/data/two_res.pdb").unwrap();
let observed_pairs = get_closest_residue_pairs(&pdb, 5.0);
let expected_pairs = &[Pair {
chain_i: "A".to_string(),
chain_j: "B".to_string(),
atom_i: "CA".to_string(),
atom_j: "CA".to_string(),
res_i: 2,
res_j: 10,
distance: 9.1,
}];
assert_eq!(observed_pairs.len(), expected_pairs.len());
let pair = &observed_pairs[0];
assert_eq!(pair.chain_i, expected_pairs[0].chain_i);
assert_eq!(pair.chain_j, expected_pairs[0].chain_j);
assert_eq!(pair.atom_i, expected_pairs[0].atom_i);
assert_eq!(pair.atom_j, expected_pairs[0].atom_j);
assert_eq!(pair.res_i, expected_pairs[0].res_i);
assert_eq!(pair.res_j, expected_pairs[0].res_j);
assert!((pair.distance - 9.1).abs() < 0.1);
}
#[test]
fn test_gaps_around_ligand() {
let pdb = load_pdb("tests/data/prot_ligand.pdb").unwrap();
let observed = gaps_around_ligand(&pdb);
let expected = &[
Gap {
chain: "E".to_string(),
res_i: 351,
atom_i: "N1".to_string(),
res_j: 123,
atom_j: "N".to_string(),
distance: 3.0400192433601467,
},
Gap {
chain: "E".to_string(),
res_i: 351,
atom_i: "C2".to_string(),
res_j: 327,
atom_j: "CE2".to_string(),
distance: 3.79,
},
];
assert_eq!(observed[0].chain, expected[0].chain);
assert_eq!(observed[0].res_i, expected[0].res_i);
assert_eq!(observed[0].atom_i, expected[0].atom_i);
assert_eq!(observed[0].res_j, expected[0].res_j);
assert_eq!(observed[0].atom_j, expected[0].atom_j);
assert_eq!(observed[1].chain, expected[1].chain);
assert_eq!(observed[1].res_i, expected[1].res_i);
assert_eq!(observed[1].atom_i, expected[1].atom_i);
assert_eq!(observed[1].res_j, expected[1].res_j);
assert_eq!(observed[1].atom_j, expected[1].atom_j);
}
#[test]
fn test_finder() {
let pdb = load_pdb("tests/data/complex.pdb").unwrap();
let finder = Finder::new(&pdb);
assert_eq!(finder.chain_lookup.keys().len(), 924);
assert_eq!(finder.residue_lookup.keys().len(), 924)
}
#[test]
fn test_finder_find_chain_id() {
let pdb = load_pdb("tests/data/complex.pdb").unwrap();
let finder = Finder::new(&pdb);
let found = finder.find_chain_id(pdb.atom(1).expect(""));
assert_eq!(found, "A");
let found = finder.find_chain_id(pdb.atom(700).expect(""));
assert_eq!(found, "B");
}
#[test]
fn test_finder_find_residue_number() {
let pdb = load_pdb("tests/data/complex.pdb").unwrap();
let finder = Finder::new(&pdb);
let found = finder.find_residue_number(pdb.atom(1).unwrap());
assert_eq!(found, 929)
}
#[test]
fn test_filter_unique_by_atom_j() {
let gaps = vec![
Gap {
chain: "A".to_string(),
atom_i: "C".to_string(),
atom_j: "B".to_string(),
res_i: 1,
res_j: 2,
distance: 3.5,
},
Gap {
chain: "A".to_string(),
atom_i: "A".to_string(),
atom_j: "B".to_string(),
res_i: 3,
res_j: 4,
distance: 2.8,
},
Gap {
chain: "B".to_string(),
atom_i: "C".to_string(),
atom_j: "D".to_string(),
res_i: 5,
res_j: 6,
distance: 4.1,
},
Gap {
chain: "A".to_string(),
atom_i: "E".to_string(),
atom_j: "B".to_string(),
res_i: 7,
res_j: 8,
distance: 1.5,
},
];
let filtered = filter_unique_by_atom_j(gaps);
assert_eq!(filtered.len(), 2); assert_eq!(filtered[0].atom_j, "B");
assert_eq!(filtered[1].atom_j, "D");
assert!(
filtered
.iter()
.all(|gap| gap.atom_j != "B" || gap.atom_i == "C")
);
}
#[test]
fn test_no_duplicates() {
let gaps = vec![
Gap {
chain: "A".to_string(),
atom_i: "C".to_string(),
atom_j: "B".to_string(),
res_i: 1,
res_j: 2,
distance: 3.5,
},
Gap {
chain: "A".to_string(),
atom_i: "A".to_string(),
atom_j: "D".to_string(),
res_i: 3,
res_j: 4,
distance: 2.8,
},
];
let filtered = filter_unique_by_atom_j(gaps);
assert_eq!(filtered.len(), 2); }
#[test]
fn test_empty_input() {
let gaps: Vec<Gap> = Vec::new();
let filtered = filter_unique_by_atom_j(gaps);
assert!(filtered.is_empty()); }
#[test]
fn test_single_element() {
let gaps = vec![Gap {
chain: "A".to_string(),
atom_i: "C".to_string(),
atom_j: "B".to_string(),
res_i: 1,
res_j: 2,
distance: 3.5,
}];
let filtered = filter_unique_by_atom_j(gaps);
assert_eq!(filtered.len(), 1); assert_eq!(filtered[0].atom_j, "B");
}
}