1use std::collections::HashSet;
16
17use num_bigint::{BigInt, BigUint};
18
19use crate::{
20 AdjacencyList, AtomId, BondDirection, BondId, BondStereo, ChiralTag, ControllingAtom, Molecule,
21 PotentialStereoError, StereoCenter, StereoGroupKind, StereoInfo, StereoSpecified, StereoType,
22 molecule::DerivedCacheBlock, potential_stereo::find_potential_stereo_in_workspace,
23 read_parts::MoleculeReadParts,
24};
25
26#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
31pub enum EnumerationError {
32 #[error("stereo error: {0}")]
33 StereoError(#[from] crate::StereoError),
34
35 #[error("canonical isomeric SMILES generation failed: {0}")]
36 SmilesWrite(#[from] crate::SmilesWriteError),
37
38 #[error("stereoisomer enumeration operation failed: {0}")]
39 Operation(#[from] crate::OperationError),
40
41 #[error("stereoisomer embedding failed: {0}")]
42 DistanceGeometry(#[from] crate::DgBoundsError),
43
44 #[error("stereoisomer output violates molecule invariants: {0}")]
45 Invariant(#[from] crate::InvariantError),
46
47 #[error(transparent)]
48 PotentialStereo(#[from] PotentialStereoError),
49
50 #[error("stereo flipper atom index {atom} is outside the molecule")]
51 InvalidFlipperAtom { atom: AtomId },
52
53 #[error("stereo flipper bond index {bond} is outside the molecule")]
54 InvalidFlipperBond { bond: BondId },
55
56 #[error("random bit source failed for {bit_count} centers: {message}")]
57 RandomBitsSource { bit_count: usize, message: String },
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub(crate) struct FlipperSelectionOptions {
62 pub(crate) only_unassigned: bool,
63 pub(crate) only_stereo_groups: bool,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub(crate) enum StereoFlipper {
68 Atom {
69 atom: AtomId,
70 },
71 Bond {
72 bond: BondId,
73 },
74 StereoGroup {
75 original_parities: Vec<(AtomId, ChiralTag)>,
76 },
77}
78
79impl StereoFlipper {
80 pub(crate) fn flip(&self, molecule: &mut Molecule, flag: bool) -> Result<(), EnumerationError> {
81 match self {
82 Self::Atom { atom } => {
83 let atom_state = molecule
94 .topology_block_mut()
95 .atoms
96 .get_mut(atom.index())
97 .ok_or(EnumerationError::InvalidFlipperAtom { atom: *atom })?;
98 atom_state.set_chiral_tag(if flag {
99 ChiralTag::TetrahedralCw
100 } else {
101 ChiralTag::TetrahedralCcw
102 });
103 }
104 Self::Bond { bond } => {
105 let bond_state = molecule
116 .topology_block_mut()
117 .bonds
118 .get_mut(bond.index())
119 .ok_or(EnumerationError::InvalidFlipperBond { bond: *bond })?;
120 bond_state.set_stereo(if flag {
121 BondStereo::Cis
122 } else {
123 BondStereo::Trans
124 });
125 }
126 Self::StereoGroup { original_parities } => {
127 let topology = molecule.topology_block_mut();
143 for (atom, original_parity) in original_parities {
144 let atom_state = topology
145 .atoms
146 .get_mut(atom.index())
147 .ok_or(EnumerationError::InvalidFlipperAtom { atom: *atom })?;
148 let parity = if flag {
149 *original_parity
150 } else {
151 match original_parity {
152 ChiralTag::TetrahedralCw => ChiralTag::TetrahedralCcw,
153 ChiralTag::TetrahedralCcw => ChiralTag::TetrahedralCw,
154 other => *other,
155 }
156 };
157 atom_state.set_chiral_tag(parity);
158 }
159 }
160 }
161 Ok(())
162 }
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Hash)]
166pub(crate) struct ConfigurationBits {
167 value: BigUint,
168}
169
170impl ConfigurationBits {
171 fn zero() -> Self {
172 Self {
173 value: BigUint::from(0_u8),
174 }
175 }
176
177 fn from_biguint(value: BigUint) -> Self {
178 Self { value }
179 }
180
181 pub(crate) fn bit(&self, index: usize) -> bool {
182 self.value.bit(index as u64)
183 }
184
185 pub(crate) fn value(&self) -> &BigUint {
186 &self.value
187 }
188
189 fn embedding_seed(&self) -> i32 {
190 self.value.iter_u32_digits().next().unwrap_or(0) as i32 & 0x7fff_ffff
193 }
194}
195
196pub(crate) trait RandomBitsSource {
197 fn getrandbits(&mut self, bit_count: usize) -> Result<ConfigurationBits, EnumerationError>;
198}
199
200struct CallbackRandomBitsSource<F> {
201 callback: F,
202}
203
204impl<F> RandomBitsSource for CallbackRandomBitsSource<F>
205where
206 F: FnMut(usize) -> Result<BigUint, String>,
207{
208 fn getrandbits(&mut self, bit_count: usize) -> Result<ConfigurationBits, EnumerationError> {
209 (self.callback)(bit_count)
210 .map(ConfigurationBits::from_biguint)
211 .map_err(|message| EnumerationError::RandomBitsSource { bit_count, message })
212 }
213}
214
215const PYTHON_MT_STATE_SIZE: usize = 624;
216const PYTHON_MT_MIDDLE_WORD: usize = 397;
217const PYTHON_MT_MATRIX_A: u32 = 0x9908_b0df;
218const PYTHON_MT_UPPER_MASK: u32 = 0x8000_0000;
219const PYTHON_MT_LOWER_MASK: u32 = 0x7fff_ffff;
220
221#[derive(Clone)]
222pub(crate) struct PythonRandom {
223 index: usize,
224 state: [u32; PYTHON_MT_STATE_SIZE],
225}
226
227impl PythonRandom {
228 pub(crate) fn from_integer_seed(seed: &BigInt) -> Self {
229 let mut key = seed.magnitude().to_u32_digits();
241 if key.is_empty() {
242 key.push(0);
243 }
244 Self::from_seed_words(&key)
245 }
246
247 fn from_seed_words(key: &[u32]) -> Self {
248 let mut random = Self {
249 index: PYTHON_MT_STATE_SIZE,
250 state: [0; PYTHON_MT_STATE_SIZE],
251 };
252 random.init_by_array(key);
253 random
254 }
255
256 fn init_genrand(&mut self, seed: u32) {
257 self.state[0] = seed;
263 for index in 1..PYTHON_MT_STATE_SIZE {
264 self.state[index] = 1_812_433_253_u32
265 .wrapping_mul(self.state[index - 1] ^ (self.state[index - 1] >> 30))
266 .wrapping_add(index as u32);
267 }
268 self.index = PYTHON_MT_STATE_SIZE;
269 }
270
271 fn init_by_array(&mut self, key: &[u32]) {
272 debug_assert!(!key.is_empty());
290 self.init_genrand(19_650_218);
291 let mut state_index = 1;
292 let mut key_index = 0;
293 for _ in 0..PYTHON_MT_STATE_SIZE.max(key.len()) {
294 self.state[state_index] = (self.state[state_index]
295 ^ (self.state[state_index - 1] ^ (self.state[state_index - 1] >> 30))
296 .wrapping_mul(1_664_525))
297 .wrapping_add(key[key_index])
298 .wrapping_add(key_index as u32);
299 state_index += 1;
300 key_index += 1;
301 if state_index >= PYTHON_MT_STATE_SIZE {
302 self.state[0] = self.state[PYTHON_MT_STATE_SIZE - 1];
303 state_index = 1;
304 }
305 if key_index >= key.len() {
306 key_index = 0;
307 }
308 }
309 for _ in 0..PYTHON_MT_STATE_SIZE - 1 {
310 self.state[state_index] = (self.state[state_index]
311 ^ (self.state[state_index - 1] ^ (self.state[state_index - 1] >> 30))
312 .wrapping_mul(1_566_083_941))
313 .wrapping_sub(state_index as u32);
314 state_index += 1;
315 if state_index >= PYTHON_MT_STATE_SIZE {
316 self.state[0] = self.state[PYTHON_MT_STATE_SIZE - 1];
317 state_index = 1;
318 }
319 }
320 self.state[0] = 0x8000_0000;
321 }
322
323 fn genrand_uint32(&mut self) -> u32 {
324 if self.index >= PYTHON_MT_STATE_SIZE {
339 for index in 0..PYTHON_MT_STATE_SIZE - PYTHON_MT_MIDDLE_WORD {
340 let value = (self.state[index] & PYTHON_MT_UPPER_MASK)
341 | (self.state[index + 1] & PYTHON_MT_LOWER_MASK);
342 self.state[index] = self.state[index + PYTHON_MT_MIDDLE_WORD]
343 ^ (value >> 1)
344 ^ if value & 1 == 0 {
345 0
346 } else {
347 PYTHON_MT_MATRIX_A
348 };
349 }
350 for index in PYTHON_MT_STATE_SIZE - PYTHON_MT_MIDDLE_WORD..PYTHON_MT_STATE_SIZE - 1 {
351 let value = (self.state[index] & PYTHON_MT_UPPER_MASK)
352 | (self.state[index + 1] & PYTHON_MT_LOWER_MASK);
353 self.state[index] = self.state
354 [index + PYTHON_MT_MIDDLE_WORD - PYTHON_MT_STATE_SIZE]
355 ^ (value >> 1)
356 ^ if value & 1 == 0 {
357 0
358 } else {
359 PYTHON_MT_MATRIX_A
360 };
361 }
362 let value = (self.state[PYTHON_MT_STATE_SIZE - 1] & PYTHON_MT_UPPER_MASK)
363 | (self.state[0] & PYTHON_MT_LOWER_MASK);
364 self.state[PYTHON_MT_STATE_SIZE - 1] = self.state[PYTHON_MT_MIDDLE_WORD - 1]
365 ^ (value >> 1)
366 ^ if value & 1 == 0 {
367 0
368 } else {
369 PYTHON_MT_MATRIX_A
370 };
371 self.index = 0;
372 }
373
374 let mut value = self.state[self.index];
381 self.index += 1;
382 value ^= value >> 11;
383 value ^= (value << 7) & 0x9d2c_5680;
384 value ^= (value << 15) & 0xefc6_0000;
385 value ^= value >> 18;
386 value
387 }
388}
389
390impl RandomBitsSource for PythonRandom {
391 fn getrandbits(&mut self, bit_count: usize) -> Result<ConfigurationBits, EnumerationError> {
392 if bit_count == 0 {
402 return Ok(ConfigurationBits::zero());
403 }
404 if bit_count <= 32 {
407 return Ok(ConfigurationBits::from_biguint(BigUint::from(
408 self.genrand_uint32() >> (32 - bit_count),
409 )));
410 }
411
412 let word_count = (bit_count - 1) / 32 + 1;
423 let mut remaining = bit_count;
424 let mut value = BigUint::from(0_u8);
425 for word_index in 0..word_count {
426 let mut word = self.genrand_uint32();
427 if remaining < 32 {
428 word >>= 32 - remaining;
429 }
430 value |= BigUint::from(word) << (word_index * 32);
431 remaining = remaining.saturating_sub(32);
432 }
433 Ok(ConfigurationBits::from_biguint(value))
434 }
435}
436
437fn cpython_tuple_hash(values: &[i64]) -> i64 {
438 const XXPRIME_1: u64 = 11_400_714_785_074_694_791;
439 const XXPRIME_2: u64 = 14_029_467_366_897_019_727;
440 const XXPRIME_5: u64 = 2_870_177_450_012_600_261;
441
442 let mut accumulator = XXPRIME_5;
453 for &value in values {
454 let lane = value as u64;
455 accumulator = accumulator.wrapping_add(lane.wrapping_mul(XXPRIME_2));
456 accumulator = accumulator.rotate_left(31);
457 accumulator = accumulator.wrapping_mul(XXPRIME_1);
458 }
459 accumulator = accumulator.wrapping_add((values.len() as u64) ^ (XXPRIME_5 ^ 3_527_539));
462 if accumulator == u64::MAX {
467 1_546_275_796
468 } else {
469 accumulator as i64
470 }
471}
472
473fn default_python_random_seed(molecule: &Molecule) -> BigInt {
474 let adjacency = AdjacencyList::from_topology(molecule.num_atoms(), molecule.bonds());
479 let mut atom_invariants = molecule
480 .atoms()
481 .iter()
482 .map(|atom| {
483 (
484 adjacency.neighbors_of(atom.id().index()).len(),
485 atom.atomic_number(),
486 )
487 })
488 .collect::<Vec<_>>();
489 atom_invariants.sort_unstable();
490 let inner_hashes = atom_invariants
491 .into_iter()
492 .map(|(degree, atomic_number)| {
493 cpython_tuple_hash(&[degree as i64, i64::from(atomic_number)])
494 })
495 .collect::<Vec<_>>();
496 BigInt::from(cpython_tuple_hash(&inner_hashes))
497}
498
499fn theoretical_configuration_count(center_count: usize) -> BigUint {
500 BigUint::from(1_u8) << center_count
501}
502
503pub(crate) struct RangeBitsGenerator {
504 next: BigUint,
505 end: BigUint,
506}
507
508impl RangeBitsGenerator {
509 pub(crate) fn new(center_count: usize) -> Self {
510 Self {
515 next: BigUint::from(0_u8),
516 end: theoretical_configuration_count(center_count),
517 }
518 }
519}
520
521impl Iterator for RangeBitsGenerator {
522 type Item = ConfigurationBits;
523
524 fn next(&mut self) -> Option<Self::Item> {
525 if self.next >= self.end {
529 return None;
530 }
531 let value = self.next.clone();
532 self.next += 1_u8;
533 Some(ConfigurationBits::from_biguint(value))
534 }
535
536 fn size_hint(&self) -> (usize, Option<usize>) {
537 let remaining = &self.end - &self.next;
538 match usize::try_from(&remaining) {
539 Ok(remaining) => (remaining, Some(remaining)),
540 Err(_) => (usize::MAX, None),
541 }
542 }
543}
544
545pub(crate) struct UniqueRandomBitsGenerator<R> {
546 center_count: usize,
547 _max_isomers: usize,
548 random: R,
549 already_seen: HashSet<ConfigurationBits>,
550 theoretical_count: BigUint,
551}
552
553impl<R> UniqueRandomBitsGenerator<R> {
554 pub(crate) fn new(center_count: usize, max_isomers: usize, random: R) -> Self {
555 Self {
563 center_count,
564 _max_isomers: max_isomers,
565 random,
566 already_seen: HashSet::new(),
567 theoretical_count: theoretical_configuration_count(center_count),
568 }
569 }
570}
571
572impl<R: RandomBitsSource> Iterator for UniqueRandomBitsGenerator<R> {
573 type Item = Result<ConfigurationBits, EnumerationError>;
574
575 fn next(&mut self) -> Option<Self::Item> {
576 while BigUint::from(self.already_seen.len()) < self.theoretical_count {
582 let bits = match self.random.getrandbits(self.center_count) {
584 Ok(bits) => bits,
585 Err(error) => return Some(Err(error)),
586 };
587 if !self.already_seen.insert(bits.clone()) {
590 continue;
591 }
592
593 return Some(Ok(bits));
596 }
597 None
598 }
599}
600
601pub(crate) fn select_stereo_flippers(
602 molecule: &mut Molecule,
603 options: FlipperSelectionOptions,
604) -> Result<Vec<StereoFlipper>, EnumerationError> {
605 let stereo_info = find_potential_stereo_in_workspace(molecule, false, true)?;
633 select_stereo_flippers_from_info(molecule, &stereo_info, options)
634}
635
636fn select_stereo_flippers_from_info(
637 molecule: &mut Molecule,
638 stereo_info: &[StereoInfo],
639 options: FlipperSelectionOptions,
640) -> Result<Vec<StereoFlipper>, EnumerationError> {
641 let mut flippers = Vec::new();
642
643 if !options.only_stereo_groups {
644 for info in stereo_info {
645 if options.only_unassigned
646 && !matches!(
647 info.specified(),
648 StereoSpecified::Unspecified | StereoSpecified::Unknown
649 )
650 {
651 continue;
652 }
653 match (info.stereo_type(), info.center()) {
654 (StereoType::AtomTetrahedral, StereoCenter::Atom(atom)) => {
655 flippers.push(StereoFlipper::Atom { atom });
656 }
657 (StereoType::BondDouble, StereoCenter::Bond(bond)) => {
658 let bond_state = molecule
659 .bonds()
660 .get(bond.index())
661 .ok_or(EnumerationError::InvalidFlipperBond { bond })?;
662 if bond_state.stereo_atoms().is_none() {
663 let Some(ControllingAtom::Atom(begin_controller)) =
664 info.controlling_atoms().first().copied()
665 else {
666 continue;
667 };
668 let Some(ControllingAtom::Atom(end_controller)) =
669 info.controlling_atoms().get(2).copied()
670 else {
671 continue;
672 };
673 let bond_state = molecule
674 .topology_block_mut()
675 .bonds
676 .get_mut(bond.index())
677 .ok_or(EnumerationError::InvalidFlipperBond { bond })?;
678 bond_state.set_stereo_atoms(Some([begin_controller, end_controller]));
679 }
680 flippers.push(StereoFlipper::Bond { bond });
681 }
682 (StereoType::BondAtropisomer, StereoCenter::Bond(_)) => {}
684 _ => {}
685 }
686 }
687 }
688
689 if options.only_unassigned {
690 for group in molecule.stereo_groups() {
691 if group.kind() == StereoGroupKind::Absolute {
692 continue;
693 }
694 let mut original_parities = Vec::with_capacity(group.atoms().len());
695 for atom in group.atoms() {
696 let atom_state = molecule
697 .atoms()
698 .get(atom.index())
699 .ok_or(EnumerationError::InvalidFlipperAtom { atom: *atom })?;
700 original_parities.push((*atom, atom_state.chiral_tag()));
701 }
702 flippers.push(StereoFlipper::StereoGroup { original_parities });
703 }
704 }
705
706 Ok(flippers)
707}
708
709#[derive(Debug)]
710pub(crate) struct EnumerationWorkspace {
711 molecule: Molecule,
712 flippers: Vec<StereoFlipper>,
713}
714
715impl EnumerationWorkspace {
716 pub(crate) fn prepare(
717 source: &Molecule,
718 options: FlipperSelectionOptions,
719 ) -> Result<Self, EnumerationError> {
720 let mut molecule = source.clone();
722
723 let topology = molecule.topology_block_mut();
729 for atom in &mut topology.atoms {
730 atom.clear_prop("_CIPCode");
731 }
732 for bond in &mut topology.bonds {
733 if matches!(
734 bond.direction(),
735 BondDirection::EitherDouble | BondDirection::Unknown
736 ) {
737 bond.set_direction(BondDirection::None);
738 }
739 }
740
741 let flippers = select_stereo_flippers(&mut molecule, options)?;
744
745 if !flippers.is_empty() {
752 molecule
754 .properties_mut()
755 .set_prop("_MolFileChiralFlag", "1");
756 }
757
758 Ok(Self { molecule, flippers })
759 }
760
761 pub(crate) fn center_count(&self) -> usize {
762 self.flippers.len()
763 }
764
765 pub(crate) fn molecule(&self) -> &Molecule {
766 &self.molecule
767 }
768
769 pub(crate) fn into_molecule(self) -> Molecule {
770 self.molecule
771 }
772
773 pub(crate) fn apply_configuration(
774 &mut self,
775 configuration: &ConfigurationBits,
776 ) -> Result<(), EnumerationError> {
777 for (index, flipper) in self.flippers.iter().enumerate() {
782 flipper.flip(&mut self.molecule, configuration.bit(index))?;
783 }
784 Ok(())
785 }
786
787 pub(crate) fn finalize_configuration(
788 &self,
789 unique: bool,
790 isomers_seen: &mut HashSet<String>,
791 ) -> Result<Option<Molecule>, EnumerationError> {
792 let mut isomer = self.molecule.clone();
800 if !isomer.stereo_groups().is_empty() {
801 isomer.topology_block_mut().stereo_groups.clear();
802 }
803
804 crate::notation::smiles::set_double_bond_neighbor_directions_from_stereo(&mut isomer)?;
806
807 clear_computed_props_preserving_rings(&mut isomer);
809
810 crate::notation::smiles::assign_stereochemistry_cleanup_subset(&mut isomer, true)?;
816
817 if unique {
824 let canonical_isomeric_smiles =
825 MoleculeReadParts::from_molecule(&isomer).canonical_isomeric_smiles()?;
826 if !isomers_seen.insert(canonical_isomeric_smiles) {
827 return Ok(None);
828 }
829 }
830
831 Ok(Some(isomer))
832 }
833
834 pub(crate) fn apply_embedding_filter(
835 &self,
836 mut isomer: Molecule,
837 configuration: &ConfigurationBits,
838 ) -> Result<Option<Molecule>, EnumerationError> {
839 let with_hydrogens = isomer.with_hydrogens()?;
842
843 let (embedded_with_hydrogens, conformer_id) =
848 crate::distgeom::rd_distgeom_embed_molecule_wrapper(
849 &with_hydrogens,
850 0,
851 configuration.embedding_seed(),
852 true,
853 false,
854 2.0,
855 true,
856 1,
857 std::collections::BTreeMap::new(),
858 1e-3,
859 false,
860 true,
861 true,
862 true,
863 false,
864 false,
865 true,
866 2,
867 true,
868 )?;
869
870 if conformer_id < 0 {
872 return Ok(None);
876 }
877
878 let heavy_atom_count = isomer.num_atoms();
882 let embedded_coordinates = embedded_with_hydrogens
883 .conformers_3d()
884 .first()
885 .ok_or_else(|| {
886 crate::DgBoundsError::CoordinateUpdateFailed(
887 "EmbedMolecule returned a nonnegative conformer id without coordinates"
888 .to_string(),
889 )
890 })?
891 .coordinates();
892 if embedded_coordinates.len() < heavy_atom_count {
893 return Err(crate::DgBoundsError::CoordinateUpdateFailed(
894 "embedded hydrogen-expanded conformer has fewer rows than the source molecule"
895 .to_string(),
896 )
897 .into());
898 }
899 let coordinates = embedded_coordinates[..heavy_atom_count].to_vec();
900
901 let coordinate_block = isomer.coordinate_block_mut();
907 coordinate_block
908 .conformers_3d
909 .push(crate::Conformer3D::new(0, coordinates, true));
910 coordinate_block.source_coordinate_dim = Some(crate::CoordinateDimension::ThreeD);
911
912 Ok(Some(isomer))
919 }
920
921 pub(crate) fn finalize_configuration_with_embedding(
922 &self,
923 configuration: &ConfigurationBits,
924 unique: bool,
925 try_embedding: bool,
926 isomers_seen: &mut HashSet<String>,
927 ) -> Result<Option<Molecule>, EnumerationError> {
928 let Some(isomer) = self.finalize_configuration(unique, isomers_seen)? else {
929 return Ok(None);
932 };
933
934 if try_embedding {
936 self.apply_embedding_filter(isomer, configuration)
937 } else {
938 Ok(Some(isomer))
942 }
943 }
944}
945
946fn clear_computed_props_preserving_rings(molecule: &mut Molecule) {
947 let rings = molecule.derived_cache_mut().rings.take();
955 *molecule.derived_cache_mut() = DerivedCacheBlock::default();
956 molecule.derived_cache_mut().rings = rings;
957
958 molecule.properties_mut().clear_computed_props();
960 molecule.clear_computed_property_cache();
961
962 let topology = molecule.topology_block_mut();
971 for atom in &mut topology.atoms {
972 atom.clear_computed_props();
973 }
974 for bond in &mut topology.bonds {
975 bond.clear_computed_props();
976 }
977}
978
979#[derive(Debug, Clone, PartialEq, Eq, Hash)]
989pub struct StereoisomerOptions {
990 pub try_embedding: bool,
991 pub only_unassigned: bool,
992 pub only_stereo_groups: bool,
993 pub max_isomers: usize,
994 pub random_seed: Option<BigInt>,
995 pub unique: bool,
996}
997
998impl Default for StereoisomerOptions {
999 fn default() -> Self {
1000 Self {
1009 try_embedding: false,
1010 only_unassigned: true,
1011 only_stereo_groups: false,
1012 max_isomers: 1024,
1013 random_seed: None,
1014 unique: true,
1015 }
1016 }
1017}
1018
1019impl StereoisomerOptions {
1020 fn flipper_selection(&self) -> FlipperSelectionOptions {
1021 FlipperSelectionOptions {
1022 only_unassigned: self.only_unassigned,
1023 only_stereo_groups: self.only_stereo_groups,
1024 }
1025 }
1026}
1027
1028enum ConfigurationSource {
1029 Exhaustive(RangeBitsGenerator),
1030 Random(UniqueRandomBitsGenerator<PythonRandom>),
1031 Callback(Box<dyn Iterator<Item = Result<ConfigurationBits, EnumerationError>> + Send + Sync>),
1032}
1033
1034impl Iterator for ConfigurationSource {
1035 type Item = Result<ConfigurationBits, EnumerationError>;
1036
1037 fn next(&mut self) -> Option<Self::Item> {
1038 match self {
1039 Self::Exhaustive(source) => source.next().map(Ok),
1040 Self::Random(source) => source.next(),
1041 Self::Callback(source) => source.next(),
1042 }
1043 }
1044}
1045
1046pub struct StereoisomerIterator {
1052 workspace: Option<EnumerationWorkspace>,
1053 configurations: Option<ConfigurationSource>,
1054 pending_no_center: Option<Molecule>,
1055 isomers_seen: HashSet<String>,
1056 unique: bool,
1057 try_embedding: bool,
1058 max_isomers: usize,
1059 yielded: usize,
1060 finished: bool,
1061}
1062
1063impl StereoisomerIterator {
1064 fn new(molecule: &Molecule, options: StereoisomerOptions) -> Result<Self, EnumerationError> {
1065 Self::new_with_configuration_source(molecule, options, None)
1066 }
1067
1068 fn new_with_configuration_source(
1069 molecule: &Molecule,
1070 options: StereoisomerOptions,
1071 callback: Option<Box<dyn FnMut(usize) -> Result<BigUint, String> + Send + Sync + 'static>>,
1072 ) -> Result<Self, EnumerationError> {
1073 let workspace = EnumerationWorkspace::prepare(molecule, options.flipper_selection())?;
1082 let center_count = workspace.center_count();
1083
1084 if center_count == 0 {
1088 return Ok(Self {
1089 workspace: None,
1090 configurations: None,
1091 pending_no_center: Some(workspace.into_molecule()),
1092 isomers_seen: HashSet::new(),
1093 unique: options.unique,
1094 try_embedding: options.try_embedding,
1095 max_isomers: options.max_isomers,
1096 yielded: 0,
1097 finished: false,
1098 });
1099 }
1100
1101 let configuration_count = theoretical_configuration_count(center_count);
1111 let configurations = if options.max_isomers == 0
1112 || configuration_count <= BigUint::from(options.max_isomers)
1113 {
1114 ConfigurationSource::Exhaustive(RangeBitsGenerator::new(center_count))
1115 } else if let Some(callback) = callback {
1116 ConfigurationSource::Callback(Box::new(UniqueRandomBitsGenerator::new(
1117 center_count,
1118 options.max_isomers,
1119 CallbackRandomBitsSource { callback },
1120 )))
1121 } else {
1122 let seed = options
1123 .random_seed
1124 .as_ref()
1125 .cloned()
1126 .unwrap_or_else(|| default_python_random_seed(workspace.molecule()));
1127 ConfigurationSource::Random(UniqueRandomBitsGenerator::new(
1128 center_count,
1129 options.max_isomers,
1130 PythonRandom::from_integer_seed(&seed),
1131 ))
1132 };
1133
1134 Ok(Self {
1135 workspace: Some(workspace),
1136 configurations: Some(configurations),
1137 pending_no_center: None,
1138 isomers_seen: HashSet::new(),
1139 unique: options.unique,
1140 try_embedding: options.try_embedding,
1141 max_isomers: options.max_isomers,
1142 yielded: 0,
1143 finished: false,
1144 })
1145 }
1146
1147 #[must_use]
1148 pub fn yielded_count(&self) -> usize {
1149 self.yielded
1150 }
1151}
1152
1153impl Iterator for StereoisomerIterator {
1154 type Item = Result<Molecule, EnumerationError>;
1155
1156 fn next(&mut self) -> Option<Self::Item> {
1157 if self.finished {
1158 return None;
1159 }
1160
1161 if let Some(isomer) = self.pending_no_center.take() {
1162 self.finished = true;
1163 self.yielded = 1;
1164 return Some(
1165 crate::invariants::enforce_molecule_invariants(&isomer)
1166 .map(|()| isomer)
1167 .map_err(EnumerationError::from),
1168 );
1169 }
1170
1171 if self.max_isomers != 0 && self.yielded >= self.max_isomers {
1174 self.finished = true;
1175 return None;
1176 }
1177
1178 loop {
1179 let configuration = match self.configurations.as_mut()?.next() {
1181 Some(Ok(configuration)) => configuration,
1182 Some(Err(error)) => {
1183 self.finished = true;
1184 return Some(Err(error));
1185 }
1186 None => {
1187 self.finished = true;
1188 return None;
1189 }
1190 };
1191
1192 let workspace = self
1193 .workspace
1194 .as_mut()
1195 .expect("enumeration workspace exists whenever configurations exist");
1196 if let Err(error) = workspace.apply_configuration(&configuration) {
1197 self.finished = true;
1198 return Some(Err(error));
1199 }
1200
1201 let isomer = match workspace.finalize_configuration_with_embedding(
1202 &configuration,
1203 self.unique,
1204 self.try_embedding,
1205 &mut self.isomers_seen,
1206 ) {
1207 Ok(Some(isomer)) => isomer,
1208 Ok(None) => continue,
1209 Err(error) => {
1210 self.finished = true;
1211 return Some(Err(error));
1212 }
1213 };
1214
1215 if let Err(error) = crate::invariants::enforce_molecule_invariants(&isomer) {
1216 self.finished = true;
1217 return Some(Err(error.into()));
1218 }
1219
1220 self.yielded += 1;
1224 return Some(Ok(isomer));
1225 }
1226 }
1227}
1228
1229impl std::iter::FusedIterator for StereoisomerIterator {}
1230
1231pub fn stereoisomer_count(
1233 molecule: &Molecule,
1234 options: &StereoisomerOptions,
1235) -> Result<BigUint, EnumerationError> {
1236 let mut workspace = molecule.clone();
1241 let flippers = select_stereo_flippers(&mut workspace, options.flipper_selection())?;
1242 Ok(theoretical_configuration_count(flippers.len()))
1243}
1244
1245pub fn enumerate_stereoisomers(
1247 molecule: &Molecule,
1248 options: StereoisomerOptions,
1249) -> Result<StereoisomerIterator, EnumerationError> {
1250 StereoisomerIterator::new(molecule, options)
1251}
1252
1253pub fn enumerate_stereoisomers_with_random_bits<F>(
1260 molecule: &Molecule,
1261 options: StereoisomerOptions,
1262 callback: F,
1263) -> Result<StereoisomerIterator, EnumerationError>
1264where
1265 F: FnMut(usize) -> Result<BigUint, String> + Send + Sync + 'static,
1266{
1267 StereoisomerIterator::new_with_configuration_source(molecule, options, Some(Box::new(callback)))
1268}
1269
1270#[cfg(test)]
1272mod tests {
1273 use std::collections::VecDeque;
1274
1275 use super::*;
1276 use crate::{
1277 AtomSpec, BondOrder, BondSpec, Conformer3D, Element, Molecule, MoleculeBuilder,
1278 StereoDescriptor, StereoGroup,
1279 };
1280
1281 fn stereo_info(
1282 stereo_type: StereoType,
1283 specified: StereoSpecified,
1284 center: StereoCenter,
1285 controlling_atoms: Vec<ControllingAtom>,
1286 ) -> StereoInfo {
1287 StereoInfo::new(
1288 stereo_type,
1289 specified,
1290 center,
1291 StereoDescriptor::None,
1292 0,
1293 controlling_atoms,
1294 )
1295 .unwrap()
1296 }
1297
1298 struct ScriptedRandomBits {
1299 values: VecDeque<Result<BigUint, String>>,
1300 requested_bit_counts: Vec<usize>,
1301 }
1302
1303 impl ScriptedRandomBits {
1304 fn values(values: impl IntoIterator<Item = u64>) -> Self {
1305 Self {
1306 values: values
1307 .into_iter()
1308 .map(|value| Ok(BigUint::from(value)))
1309 .collect(),
1310 requested_bit_counts: Vec::new(),
1311 }
1312 }
1313
1314 fn error(message: impl Into<String>) -> Self {
1315 Self {
1316 values: VecDeque::from([Err(message.into())]),
1317 requested_bit_counts: Vec::new(),
1318 }
1319 }
1320 }
1321
1322 impl RandomBitsSource for ScriptedRandomBits {
1323 fn getrandbits(&mut self, bit_count: usize) -> Result<ConfigurationBits, EnumerationError> {
1324 self.requested_bit_counts.push(bit_count);
1325 match self.values.pop_front() {
1326 Some(Ok(value)) => Ok(ConfigurationBits::from_biguint(value)),
1327 Some(Err(message)) => {
1328 Err(EnumerationError::RandomBitsSource { bit_count, message })
1329 }
1330 None => Err(EnumerationError::RandomBitsSource {
1331 bit_count,
1332 message: "scripted source exhausted".to_owned(),
1333 }),
1334 }
1335 }
1336 }
1337
1338 #[test]
1339 fn python_range_bits_generator_reproduces_zero_center_and_lsb_first_order() {
1340 let zero_center_values = RangeBitsGenerator::new(0)
1341 .map(|bits| bits.value().clone())
1342 .collect::<Vec<_>>();
1343 assert_eq!(zero_center_values, vec![BigUint::from(0_u8)]);
1344
1345 let values = RangeBitsGenerator::new(3).collect::<Vec<_>>();
1346 assert_eq!(values.len(), 8);
1347 for (value, bits) in values.iter().enumerate() {
1348 assert_eq!(bits.value(), &BigUint::from(value));
1349 assert_eq!(bits.bit(0), value & 0b001 != 0);
1350 assert_eq!(bits.bit(1), value & 0b010 != 0);
1351 assert_eq!(bits.bit(2), value & 0b100 != 0);
1352 }
1353 }
1354
1355 #[test]
1356 fn python_range_bits_generator_preserves_arbitrary_width_counts_and_laziness() {
1357 assert_eq!(
1358 theoretical_configuration_count(130),
1359 BigUint::from(1_u8) << 130
1360 );
1361
1362 let prefix = RangeBitsGenerator::new(130)
1363 .take(4)
1364 .map(|bits| bits.value().clone())
1365 .collect::<Vec<_>>();
1366 assert_eq!(
1367 prefix,
1368 [0_u8, 1, 2, 3]
1369 .into_iter()
1370 .map(BigUint::from)
1371 .collect::<Vec<_>>()
1372 );
1373
1374 let huge_prefix = RangeBitsGenerator::new(1_000_000)
1377 .take(3)
1378 .map(|bits| bits.value().clone())
1379 .collect::<Vec<_>>();
1380 assert_eq!(
1381 huge_prefix,
1382 [0_u8, 1, 2]
1383 .into_iter()
1384 .map(BigUint::from)
1385 .collect::<Vec<_>>()
1386 );
1387 }
1388
1389 #[test]
1390 fn python_unique_random_bits_generator_rejects_duplicates_and_exhausts_finitely() {
1391 let random = ScriptedRandomBits::values([3, 1, 3, 0, 2]);
1392 let mut generator = UniqueRandomBitsGenerator::new(2, 1, random);
1393 let values = generator
1394 .by_ref()
1395 .map(|result| result.unwrap().value().clone())
1396 .collect::<Vec<_>>();
1397 assert_eq!(
1398 values,
1399 [3_u8, 1, 0, 2]
1400 .into_iter()
1401 .map(BigUint::from)
1402 .collect::<Vec<_>>()
1403 );
1404 assert_eq!(generator.random.requested_bit_counts, vec![2; 5]);
1405 assert!(generator.next().is_none());
1406
1407 let mut zero_centers =
1408 UniqueRandomBitsGenerator::new(0, usize::MAX, ScriptedRandomBits::values([0, 0]));
1409 assert_eq!(
1410 zero_centers.next().unwrap().unwrap().value(),
1411 &BigUint::from(0_u8)
1412 );
1413 assert!(zero_centers.next().is_none());
1414 assert_eq!(zero_centers.random.requested_bit_counts, vec![0]);
1415 }
1416
1417 #[test]
1418 fn python_unique_random_bits_generator_propagates_source_errors_structurally() {
1419 let mut generator =
1420 UniqueRandomBitsGenerator::new(130, 1024, ScriptedRandomBits::error("fixture failure"));
1421 assert_eq!(
1422 generator.next().unwrap().unwrap_err(),
1423 EnumerationError::RandomBitsSource {
1424 bit_count: 130,
1425 message: "fixture failure".to_owned(),
1426 }
1427 );
1428 assert_eq!(generator.random.requested_bit_counts, vec![130]);
1429 }
1430
1431 fn python_random_values(seed: &BigInt, widths: &[usize]) -> Vec<BigUint> {
1432 let mut random = PythonRandom::from_integer_seed(seed);
1433 widths
1434 .iter()
1435 .map(|&width| random.getrandbits(width).unwrap().value().clone())
1436 .collect()
1437 }
1438
1439 fn decimal_biguint(value: &str) -> BigUint {
1440 value.parse().unwrap()
1441 }
1442
1443 #[test]
1444 fn python_random_matches_cpython_integer_seed_and_getrandbits_sequence_exactly() {
1445 let widths = [0, 1, 2, 31, 32, 33, 64, 65, 130];
1446 let values = python_random_values(&BigInt::from(0xdead_beef_u32), &widths);
1447 let expected = [
1448 "0",
1449 "0",
1450 "1",
1451 "1008527992",
1452 "3864962434",
1453 "6868874809",
1454 "16491411380257451511",
1455 "25412851181257523183",
1456 "668769172828971426222707781417953853951",
1457 ]
1458 .map(decimal_biguint);
1459 assert_eq!(values, expected);
1460 }
1461
1462 #[test]
1463 fn python_random_uses_all_integer_seed_bits_and_absolute_negative_seed_semantics() {
1464 let seed = (BigInt::from(1_u8) << 100) + BigInt::from(0x1234_5678_u32);
1465 let widths = [0, 1, 2, 31, 32, 33, 64, 65, 130];
1466 let positive = python_random_values(&seed, &widths);
1467 let negative = python_random_values(&(-seed), &widths);
1468 let expected = [
1469 "0",
1470 "1",
1471 "0",
1472 "552449039",
1473 "185676661",
1474 "7120073926",
1475 "18340008931984535783",
1476 "34270592875609026320",
1477 "1287945476240184999319519200333061618674",
1478 ]
1479 .map(decimal_biguint);
1480 assert_eq!(positive, expected);
1481 assert_eq!(negative, expected);
1482 }
1483
1484 #[test]
1485 fn python_default_random_seed_is_exact_and_invariant_to_input_atom_order() {
1486 let cco = Molecule::from_smiles("CCO").unwrap();
1487 let occ = Molecule::from_smiles("OCC").unwrap();
1488 assert_ne!(
1489 cco.atoms()
1490 .iter()
1491 .map(|atom| atom.atomic_number())
1492 .collect::<Vec<_>>(),
1493 occ.atoms()
1494 .iter()
1495 .map(|atom| atom.atomic_number())
1496 .collect::<Vec<_>>()
1497 );
1498
1499 let cco_seed = default_python_random_seed(&cco);
1500 let occ_seed = default_python_random_seed(&occ);
1501 assert_eq!(cco_seed, BigInt::from(1_554_427_866_021_819_617_i64));
1502 assert_eq!(cco_seed, occ_seed);
1503
1504 let expected = [1_u8, 7, 7, 5, 3, 1, 4, 2, 2, 0, 3, 3]
1505 .into_iter()
1506 .map(BigUint::from)
1507 .collect::<Vec<_>>();
1508 let first = python_random_values(&cco_seed, &[3; 12]);
1509 let repeated = python_random_values(&cco_seed, &[3; 12]);
1510 assert_eq!(first, expected);
1511 assert_eq!(repeated, expected);
1512 }
1513
1514 struct CounterRandom {
1515 next_value: BigUint,
1516 }
1517
1518 impl RandomBitsSource for CounterRandom {
1519 fn getrandbits(&mut self, bit_count: usize) -> Result<ConfigurationBits, EnumerationError> {
1520 let value = self.next_value.clone();
1521 self.next_value += 1_u8;
1522 if value >= theoretical_configuration_count(bit_count) {
1523 return Err(EnumerationError::RandomBitsSource {
1524 bit_count,
1525 message: "counter random source exhausted the declared bit width".to_owned(),
1526 });
1527 }
1528 Ok(ConfigurationBits::from_biguint(value))
1529 }
1530 }
1531
1532 #[test]
1533 fn python_random_custom_getrandbits_boundary_matches_rdkit_counter_fixture() {
1534 let random = CounterRandom {
1535 next_value: BigUint::from(0_u8),
1536 };
1537 let values = UniqueRandomBitsGenerator::new(3, 3, random)
1538 .take(3)
1539 .map(|result| result.unwrap().value().clone())
1540 .collect::<Vec<_>>();
1541 assert_eq!(
1542 values,
1543 [0_u8, 1, 2]
1544 .into_iter()
1545 .map(BigUint::from)
1546 .collect::<Vec<_>>()
1547 );
1548 }
1549
1550 #[test]
1551 fn python_random_instances_are_parallel_and_repeat_call_isolated() {
1552 let seed = BigInt::from(-5_473_470_236_788_694_370_i64);
1553 let expected = [6_u8, 1, 4, 6, 4, 2, 0, 7, 6, 5, 2, 6]
1554 .into_iter()
1555 .map(BigUint::from)
1556 .collect::<Vec<_>>();
1557 let outputs = std::thread::scope(|scope| {
1558 (0..8)
1559 .map(|_| {
1560 let seed = &seed;
1561 scope.spawn(move || python_random_values(seed, &[3; 12]))
1562 })
1563 .collect::<Vec<_>>()
1564 .into_iter()
1565 .map(|thread| thread.join().unwrap())
1566 .collect::<Vec<_>>()
1567 });
1568 assert_eq!(outputs, vec![expected; 8]);
1569 }
1570
1571 fn tetrahedral_center(
1572 builder: &mut MoleculeBuilder,
1573 tag: ChiralTag,
1574 cip_code: Option<&str>,
1575 ) -> AtomId {
1576 let mut center = AtomSpec::new(Element::C)
1577 .with_no_implicit(true)
1578 .with_chiral_tag(tag)
1579 .with_prop("keep_atom", "center");
1580 if let Some(cip_code) = cip_code {
1581 center = center.with_prop("_CIPCode", cip_code);
1582 }
1583 let center = builder.add_atom(center);
1584 for element in [Element::F, Element::CL, Element::BR, Element::I] {
1585 let ligand = builder.add_atom(AtomSpec::new(element));
1586 builder
1587 .add_bond(BondSpec::new(center, ligand, BondOrder::Single))
1588 .unwrap();
1589 }
1590 center
1591 }
1592
1593 #[test]
1594 fn python_enumerator_preprocessing_preserves_no_center_source_state_and_owned_blocks() {
1595 let mut builder = MoleculeBuilder::new()
1596 .with_name("preprocessing fixture")
1597 .with_property("keep_molecule", "yes")
1598 .with_property("_MolFileChiralFlag", "0");
1599 let atoms = (0..4)
1600 .map(|index| {
1601 let spec = if index == 0 {
1602 AtomSpec::new(Element::C)
1603 .with_prop("_CIPCode", "R")
1604 .with_prop("keep_atom", "yes")
1605 } else {
1606 AtomSpec::new(Element::C)
1607 };
1608 builder.add_atom(spec)
1609 })
1610 .collect::<Vec<_>>();
1611 for (index, direction) in [
1612 BondDirection::EitherDouble,
1613 BondDirection::Unknown,
1614 BondDirection::BeginWedge,
1615 ]
1616 .into_iter()
1617 .enumerate()
1618 {
1619 builder
1620 .add_bond(
1621 BondSpec::new(atoms[index], atoms[index + 1], BondOrder::Single)
1622 .with_direction(direction)
1623 .with_prop("keep_bond", index.to_string()),
1624 )
1625 .unwrap();
1626 }
1627 let coordinates = vec![
1628 [0.0, 0.0, 0.0],
1629 [1.0, 0.0, 0.0],
1630 [2.0, 1.0, 0.0],
1631 [3.0, 1.0, 1.0],
1632 ];
1633 builder
1634 .add_conformer(
1635 Conformer3D::new(7, coordinates.clone(), true).with_prop("keep_conf", "yes"),
1636 )
1637 .unwrap();
1638 let source = builder.build().unwrap();
1639 let source_before = source.clone();
1640
1641 let workspace = EnumerationWorkspace::prepare(
1642 &source,
1643 FlipperSelectionOptions {
1644 only_unassigned: true,
1645 only_stereo_groups: false,
1646 },
1647 )
1648 .unwrap();
1649
1650 assert_eq!(source, source_before);
1651 assert_eq!(workspace.center_count(), 0);
1652 assert_eq!(workspace.molecule().atoms()[0].prop("_CIPCode"), None);
1653 assert_eq!(
1654 workspace.molecule().atoms()[0].prop("keep_atom"),
1655 Some("yes")
1656 );
1657 assert_eq!(
1658 workspace
1659 .molecule()
1660 .bonds()
1661 .iter()
1662 .map(crate::Bond::direction)
1663 .collect::<Vec<_>>(),
1664 vec![
1665 BondDirection::None,
1666 BondDirection::None,
1667 BondDirection::BeginWedge,
1668 ]
1669 );
1670 assert_eq!(workspace.molecule().bonds()[2].prop("keep_bond"), Some("2"));
1671 assert_eq!(workspace.molecule().prop("keep_molecule"), Some("yes"));
1672 assert_eq!(workspace.molecule().prop("_MolFileChiralFlag"), Some("0"));
1673 assert_eq!(workspace.molecule().conformers_3d().len(), 1);
1674 assert_eq!(
1675 workspace.molecule().conformers_3d()[0].coordinates(),
1676 coordinates
1677 );
1678 assert_eq!(
1679 workspace.molecule().conformers_3d()[0]
1680 .props()
1681 .get("keep_conf")
1682 .map(String::as_str),
1683 Some("yes")
1684 );
1685 }
1686
1687 #[test]
1688 fn python_enumerator_preprocessing_filters_fully_assigned_and_partial_centers_exactly() {
1689 let mut assigned_builder = MoleculeBuilder::new();
1690 let assigned =
1691 tetrahedral_center(&mut assigned_builder, ChiralTag::TetrahedralCw, Some("R"));
1692 let assigned_source = assigned_builder.build().unwrap();
1693 let assigned_before = assigned_source.clone();
1694 let assigned_only = EnumerationWorkspace::prepare(
1695 &assigned_source,
1696 FlipperSelectionOptions {
1697 only_unassigned: true,
1698 only_stereo_groups: false,
1699 },
1700 )
1701 .unwrap();
1702 assert_eq!(assigned_only.center_count(), 0);
1703 assert_eq!(
1704 assigned_only.molecule().atoms()[assigned.index()].prop("_CIPCode"),
1705 None
1706 );
1707 assert_eq!(assigned_source, assigned_before);
1708
1709 let mut partial_builder = MoleculeBuilder::new();
1710 let retained =
1711 tetrahedral_center(&mut partial_builder, ChiralTag::TetrahedralCw, Some("S"));
1712 let enumerated =
1713 tetrahedral_center(&mut partial_builder, ChiralTag::Unspecified, Some("stale"));
1714 let partial_source = partial_builder.build().unwrap();
1715 let partial_before = partial_source.clone();
1716 let mut partial = EnumerationWorkspace::prepare(
1717 &partial_source,
1718 FlipperSelectionOptions {
1719 only_unassigned: true,
1720 only_stereo_groups: false,
1721 },
1722 )
1723 .unwrap();
1724
1725 assert_eq!(partial.center_count(), 1);
1726 assert_eq!(
1727 partial.flippers,
1728 vec![StereoFlipper::Atom { atom: enumerated }]
1729 );
1730 assert_eq!(partial.molecule().prop("_MolFileChiralFlag"), Some("1"));
1731 partial
1732 .apply_configuration(&ConfigurationBits::from_biguint(BigUint::from(0_u8)))
1733 .unwrap();
1734 assert_eq!(
1735 partial.molecule().atoms()[retained.index()].chiral_tag(),
1736 ChiralTag::TetrahedralCw
1737 );
1738 assert_eq!(
1739 partial.molecule().atoms()[enumerated.index()].chiral_tag(),
1740 ChiralTag::TetrahedralCcw
1741 );
1742 partial
1743 .apply_configuration(&ConfigurationBits::from_biguint(BigUint::from(1_u8)))
1744 .unwrap();
1745 assert_eq!(
1746 partial.molecule().atoms()[enumerated.index()].chiral_tag(),
1747 ChiralTag::TetrahedralCw
1748 );
1749 assert_eq!(partial_source, partial_before);
1750 }
1751
1752 #[test]
1753 fn python_enumerator_preprocessing_clears_either_double_and_applies_stereoany_bond_bits() {
1754 let mut builder = MoleculeBuilder::new();
1755 let begin = builder.add_atom(AtomSpec::new(Element::C).with_no_implicit(true));
1756 let end = builder.add_atom(AtomSpec::new(Element::C).with_no_implicit(true));
1757 let begin_first = builder.add_atom(AtomSpec::new(Element::F));
1758 let begin_second = builder.add_atom(AtomSpec::new(Element::CL));
1759 let end_first = builder.add_atom(AtomSpec::new(Element::BR));
1760 let end_second = builder.add_atom(AtomSpec::new(Element::I));
1761 let double_bond = builder
1762 .add_bond(
1763 BondSpec::new(begin, end, BondOrder::Double)
1764 .with_stereo(BondStereo::Any)
1765 .with_direction(BondDirection::EitherDouble),
1766 )
1767 .unwrap();
1768 builder
1769 .add_bond(
1770 BondSpec::new(begin, begin_first, BondOrder::Single)
1771 .with_direction(BondDirection::Unknown),
1772 )
1773 .unwrap();
1774 builder
1775 .add_bond(BondSpec::new(begin, begin_second, BondOrder::Single))
1776 .unwrap();
1777 builder
1778 .add_bond(BondSpec::new(end, end_first, BondOrder::Single))
1779 .unwrap();
1780 builder
1781 .add_bond(BondSpec::new(end, end_second, BondOrder::Single))
1782 .unwrap();
1783 let source = builder.build().unwrap();
1784 let source_before = source.clone();
1785 let mut workspace = EnumerationWorkspace::prepare(
1786 &source,
1787 FlipperSelectionOptions {
1788 only_unassigned: true,
1789 only_stereo_groups: false,
1790 },
1791 )
1792 .unwrap();
1793
1794 assert_eq!(workspace.center_count(), 1);
1795 assert_eq!(
1796 workspace.flippers,
1797 vec![StereoFlipper::Bond { bond: double_bond }]
1798 );
1799 assert_eq!(
1800 workspace.molecule().bonds()[double_bond.index()].direction(),
1801 BondDirection::None
1802 );
1803 assert_eq!(
1804 workspace.molecule().bonds()[1].direction(),
1805 BondDirection::None
1806 );
1807 assert_eq!(
1808 workspace.molecule().bonds()[double_bond.index()].stereo(),
1809 BondStereo::Any
1810 );
1811 assert!(
1812 workspace.molecule().bonds()[double_bond.index()]
1813 .stereo_atoms()
1814 .is_some()
1815 );
1816 workspace
1817 .apply_configuration(&ConfigurationBits::from_biguint(BigUint::from(0_u8)))
1818 .unwrap();
1819 assert_eq!(
1820 workspace.molecule().bonds()[double_bond.index()].stereo(),
1821 BondStereo::Trans
1822 );
1823 workspace
1824 .apply_configuration(&ConfigurationBits::from_biguint(BigUint::from(1_u8)))
1825 .unwrap();
1826 assert_eq!(
1827 workspace.molecule().bonds()[double_bond.index()].stereo(),
1828 BondStereo::Cis
1829 );
1830 assert_eq!(source, source_before);
1831 }
1832
1833 #[test]
1834 fn python_enumerator_preprocessing_selects_enhanced_stereo_groups_once_in_group_order() {
1835 let mut builder = MoleculeBuilder::new();
1836 let first = tetrahedral_center(&mut builder, ChiralTag::TetrahedralCw, Some("R"));
1837 let second = tetrahedral_center(&mut builder, ChiralTag::TetrahedralCcw, Some("S"));
1838 builder
1839 .add_stereo_group(StereoGroup::new(
1840 StereoGroupKind::Absolute,
1841 vec![first],
1842 Vec::new(),
1843 ))
1844 .unwrap();
1845 builder
1846 .add_stereo_group(StereoGroup::new(
1847 StereoGroupKind::Or,
1848 vec![first, second],
1849 Vec::new(),
1850 ))
1851 .unwrap();
1852 let source = builder.build().unwrap();
1853 let source_before = source.clone();
1854 let mut workspace = EnumerationWorkspace::prepare(
1855 &source,
1856 FlipperSelectionOptions {
1857 only_unassigned: true,
1858 only_stereo_groups: true,
1859 },
1860 )
1861 .unwrap();
1862
1863 assert_eq!(workspace.center_count(), 1);
1864 assert_eq!(workspace.molecule().stereo_groups(), source.stereo_groups());
1865 assert!(matches!(
1866 &workspace.flippers[0],
1867 StereoFlipper::StereoGroup { original_parities }
1868 if original_parities == &vec![
1869 (first, ChiralTag::TetrahedralCw),
1870 (second, ChiralTag::TetrahedralCcw),
1871 ]
1872 ));
1873 workspace
1874 .apply_configuration(&ConfigurationBits::from_biguint(BigUint::from(0_u8)))
1875 .unwrap();
1876 assert_eq!(
1877 workspace.molecule().atoms()[first.index()].chiral_tag(),
1878 ChiralTag::TetrahedralCcw
1879 );
1880 assert_eq!(
1881 workspace.molecule().atoms()[second.index()].chiral_tag(),
1882 ChiralTag::TetrahedralCw
1883 );
1884 workspace
1885 .apply_configuration(&ConfigurationBits::from_biguint(BigUint::from(1_u8)))
1886 .unwrap();
1887 assert_eq!(
1888 workspace.molecule().atoms()[first.index()].chiral_tag(),
1889 ChiralTag::TetrahedralCw
1890 );
1891 assert_eq!(
1892 workspace.molecule().atoms()[second.index()].chiral_tag(),
1893 ChiralTag::TetrahedralCcw
1894 );
1895 assert_eq!(source, source_before);
1896 }
1897
1898 #[test]
1899 fn python_enumerator_finalization_clears_computed_state_preserves_rings_and_removes_groups() {
1900 let mut source = Molecule::from_smiles("FC(Cl)C(Cl)F")
1901 .unwrap()
1902 .with_assigned_rings()
1903 .unwrap();
1904 source
1905 .properties_mut()
1906 .set_prop("keep_molecule", "persistent");
1907 source
1908 .properties_mut()
1909 .set_computed_prop("drop_molecule", "computed");
1910 source.topology_block_mut().atoms[1].set_prop("keep_atom", "persistent");
1911 source.topology_block_mut().atoms[1].set_computed_prop("drop_atom", "computed");
1912 source.topology_block_mut().bonds[0].set_prop("keep_bond", "persistent");
1913 source.topology_block_mut().bonds[0].set_computed_prop("drop_bond", "computed");
1914 source
1915 .topology_block_mut()
1916 .stereo_groups
1917 .push(StereoGroup::new(
1918 StereoGroupKind::Or,
1919 vec![AtomId::new(1), AtomId::new(3)],
1920 Vec::new(),
1921 ));
1922 let source_before = source.clone();
1923 let source_rings = source.derived_cache().rings.clone();
1924
1925 let mut workspace = EnumerationWorkspace::prepare(
1926 &source,
1927 FlipperSelectionOptions {
1928 only_unassigned: false,
1929 only_stereo_groups: false,
1930 },
1931 )
1932 .unwrap();
1933 workspace
1934 .apply_configuration(&ConfigurationBits::from_biguint(BigUint::from(0_u8)))
1935 .unwrap();
1936 let output = workspace
1937 .finalize_configuration(false, &mut HashSet::new())
1938 .unwrap()
1939 .unwrap();
1940
1941 assert_eq!(source, source_before);
1942 assert!(output.stereo_groups().is_empty());
1943 assert_eq!(output.derived_cache().rings, source_rings);
1944 assert_eq!(output.prop("keep_molecule"), Some("persistent"));
1945 assert_eq!(output.prop("drop_molecule"), None);
1946 assert_eq!(output.atoms()[1].prop("keep_atom"), Some("persistent"));
1947 assert_eq!(output.atoms()[1].prop("drop_atom"), None);
1948 assert_eq!(output.bonds()[0].prop("keep_bond"), Some("persistent"));
1949 assert_eq!(output.bonds()[0].prop("drop_bond"), None);
1950 assert_eq!(output.prop("_MolFileChiralFlag"), Some("1"));
1951 assert_eq!(output.prop("_StereochemDone"), Some("1"));
1952 assert!(output.properties().is_prop_computed("_StereochemDone"));
1953 for center in [1, 3] {
1954 assert!(output.atoms()[center].prop("_CIPCode").is_some());
1955 assert_eq!(output.atoms()[center].prop("_ChiralityPossible"), Some("1"));
1956 }
1957 }
1958
1959 #[test]
1960 fn python_enumerator_finalization_continues_on_meso_canonical_duplicates_in_source_order() {
1961 let source = Molecule::from_smiles("FC(Cl)C(Cl)F").unwrap();
1962 let mut workspace = EnumerationWorkspace::prepare(
1963 &source,
1964 FlipperSelectionOptions {
1965 only_unassigned: true,
1966 only_stereo_groups: false,
1967 },
1968 )
1969 .unwrap();
1970 assert_eq!(workspace.center_count(), 2);
1971
1972 let mut seen = HashSet::new();
1973 let mut outputs = Vec::new();
1974 let mut emitted_by_configuration = Vec::new();
1975 for configuration in 0_u8..4 {
1976 workspace
1977 .apply_configuration(&ConfigurationBits::from_biguint(BigUint::from(
1978 configuration,
1979 )))
1980 .unwrap();
1981 let finalized = workspace.finalize_configuration(true, &mut seen).unwrap();
1982 emitted_by_configuration.push(finalized.is_some());
1983 if let Some(isomer) = finalized {
1984 outputs.push(
1985 MoleculeReadParts::from_molecule(&isomer)
1986 .canonical_isomeric_smiles()
1987 .unwrap(),
1988 );
1989 }
1990 }
1991
1992 assert_eq!(emitted_by_configuration, vec![true, true, true, false]);
1993 assert_eq!(
1994 outputs,
1995 vec[C@@H](F)Cl",
1997 "F[C@@H](Cl)[C@@H](F)Cl",
1998 "F[C@H](Cl)[C@H](F)Cl",
1999 ]
2000 );
2001 assert_eq!(seen.len(), 3);
2002 }
2003
2004 #[test]
2005 fn python_enumerator_preserves_tetrahedral_center_when_double_bond_configurations_change() {
2006 let source =
2007 Molecule::from_smiles("Cl.N=C(N)c1ccc(C=CC(=O)NCC(O)CNC(=O)C=Cc2ccc(C(=N)N)cc2)cc1")
2008 .unwrap();
2009 let source_before = source.clone();
2010
2011 let non_unique = enumerate_stereoisomers(
2012 &source,
2013 StereoisomerOptions {
2014 max_isomers: 0,
2015 unique: false,
2016 ..Default::default()
2017 },
2018 )
2019 .unwrap()
2020 .collect::<Result<Vec<_>, _>>()
2021 .unwrap();
2022 assert_eq!(non_unique.len(), 8);
2023 for isomer in &non_unique[2..6] {
2024 assert!(
2025 isomer.atoms()[14].prop("_CIPCode").is_some(),
2026 "enumeration finalization must retain the source-assigned CIP code"
2027 );
2028 }
2029 assert_eq!(
2030 non_unique
2031 .iter()
2032 .map(|isomer| isomer.atoms()[14].chiral_tag())
2033 .collect::<Vec<_>>(),
2034 vec![
2035 ChiralTag::Unspecified,
2036 ChiralTag::Unspecified,
2037 ChiralTag::TetrahedralCcw,
2038 ChiralTag::TetrahedralCw,
2039 ChiralTag::TetrahedralCcw,
2040 ChiralTag::TetrahedralCw,
2041 ChiralTag::Unspecified,
2042 ChiralTag::Unspecified,
2043 ]
2044 );
2045
2046 let unique = enumerate_stereoisomers(
2047 &source,
2048 StereoisomerOptions {
2049 max_isomers: 0,
2050 unique: true,
2051 ..Default::default()
2052 },
2053 )
2054 .unwrap()
2055 .collect::<Result<Vec<_>, _>>()
2056 .unwrap();
2057 assert_eq!(
2058 unique
2059 .iter()
2060 .map(|isomer| {
2061 MoleculeReadParts::from_molecule(isomer).canonical_isomeric_smiles()
2062 })
2063 .collect::<Result<Vec<_>, _>>()
2064 .unwrap(),
2065 vecCNC(=O)/C=C/c2ccc(C(=N)N)cc2)cc1",
2068 "Cl.N=C(N)c1ccc(/C=C\\C(=O)NC[C@@H](O)CNC(=O)/C=C/c2ccc(C(=N)N)cc2)cc1",
2069 "Cl.N=C(N)c1ccc(/C=C\\C(=O)NCC(O)CNC(=O)/C=C\\c2ccc(C(=N)N)cc2)cc1",
2070 ]
2071 );
2072 assert_eq!(source, source_before);
2073 }
2074
2075 #[test]
2076 fn python_enumerator_finalization_sets_double_bond_directions_and_handles_cumulenes() {
2077 let mut builder = MoleculeBuilder::new();
2078 let begin = builder.add_atom(AtomSpec::new(Element::C).with_no_implicit(true));
2079 let end = builder.add_atom(AtomSpec::new(Element::C).with_no_implicit(true));
2080 let begin_first = builder.add_atom(AtomSpec::new(Element::F));
2081 let begin_second = builder.add_atom(AtomSpec::new(Element::CL));
2082 let end_first = builder.add_atom(AtomSpec::new(Element::BR));
2083 let end_second = builder.add_atom(AtomSpec::new(Element::I));
2084 let double_bond = builder
2085 .add_bond(
2086 BondSpec::new(begin, end, BondOrder::Double)
2087 .with_stereo(BondStereo::Any)
2088 .with_stereo_atoms(begin_first, end_first),
2089 )
2090 .unwrap();
2091 for (center, ligand) in [
2092 (begin, begin_first),
2093 (begin, begin_second),
2094 (end, end_first),
2095 (end, end_second),
2096 ] {
2097 builder
2098 .add_bond(BondSpec::new(center, ligand, BondOrder::Single))
2099 .unwrap();
2100 }
2101 let source = builder.build().unwrap();
2102 let mut workspace = EnumerationWorkspace::prepare(
2103 &source,
2104 FlipperSelectionOptions {
2105 only_unassigned: true,
2106 only_stereo_groups: false,
2107 },
2108 )
2109 .unwrap();
2110 workspace
2111 .apply_configuration(&ConfigurationBits::from_biguint(BigUint::from(0_u8)))
2112 .unwrap();
2113 let trans = workspace
2114 .finalize_configuration(false, &mut HashSet::new())
2115 .unwrap()
2116 .unwrap();
2117 assert_eq!(trans.bonds()[double_bond.index()].stereo(), BondStereo::E);
2118 assert!(trans.bonds().iter().any(|bond| matches!(
2119 bond.direction(),
2120 BondDirection::EndDownRight | BondDirection::EndUpRight
2121 )));
2122
2123 let cumulene = Molecule::from_smiles("CC=C=CC").unwrap();
2124 let cumulene_workspace = EnumerationWorkspace {
2125 molecule: cumulene,
2126 flippers: Vec::new(),
2127 };
2128 let cumulene_output = cumulene_workspace
2129 .finalize_configuration(false, &mut HashSet::new())
2130 .unwrap()
2131 .unwrap();
2132 assert_eq!(
2133 MoleculeReadParts::from_molecule(&cumulene_output)
2134 .canonical_isomeric_smiles()
2135 .unwrap(),
2136 "CC=C=CC"
2137 );
2138 assert_eq!(cumulene_output.prop("_StereochemDone"), Some("1"));
2139 }
2140
2141 #[test]
2142 fn python_enumerator_try_embedding_masks_configuration_to_signed_cxx_seed() {
2143 let cases = [
2144 (0_u64, 0_i32),
2145 (1, 1),
2146 (0x7fff_ffff, 0x7fff_ffff),
2147 (0x8000_0000, 0),
2148 (0xffff_ffff, 0x7fff_ffff),
2149 (0x1_0000_0005, 5),
2150 ];
2151 for (configuration, expected) in cases {
2152 assert_eq!(
2153 ConfigurationBits::from_biguint(BigUint::from(configuration)).embedding_seed(),
2154 expected,
2155 "configuration {configuration:#x}"
2156 );
2157 }
2158 }
2159
2160 #[test]
2161 fn python_enumerator_try_embedding_matches_rdkit_rejection_map_and_exhausts_finitely() {
2162 let source = Molecule::from_smiles("BrC=CC1OC(C2)(F)C2(Cl)C1").unwrap();
2166 let source_before = source.clone();
2167 let mut workspace = EnumerationWorkspace::prepare(
2168 &source,
2169 FlipperSelectionOptions {
2170 only_unassigned: true,
2171 only_stereo_groups: false,
2172 },
2173 )
2174 .unwrap();
2175 assert_eq!(workspace.center_count(), 4);
2176
2177 let mut seen = HashSet::new();
2178 let mut succeeded = Vec::new();
2179 let mut rejected = Vec::new();
2180 let mut attempts = 0_usize;
2181 for configuration in RangeBitsGenerator::new(workspace.center_count()) {
2182 attempts += 1;
2183 workspace.apply_configuration(&configuration).unwrap();
2184 match workspace
2185 .finalize_configuration_with_embedding(&configuration, true, true, &mut seen)
2186 .unwrap()
2187 {
2188 Some(isomer) => {
2189 assert_eq!(isomer.conformers_3d().len(), 1);
2190 assert_eq!(
2191 isomer.conformers_3d()[0].coordinates().len(),
2192 source.num_atoms()
2193 );
2194 succeeded.push(configuration.value().clone());
2195 }
2196 None => rejected.push(configuration.value().clone()),
2197 }
2198 }
2199
2200 assert_eq!(attempts, 16);
2201 assert_eq!(
2202 succeeded,
2203 [0_u8, 1, 6, 7, 8, 9, 14, 15]
2204 .into_iter()
2205 .map(BigUint::from)
2206 .collect::<Vec<_>>()
2207 );
2208 assert_eq!(
2209 rejected,
2210 [2_u8, 3, 4, 5, 10, 11, 12, 13]
2211 .into_iter()
2212 .map(BigUint::from)
2213 .collect::<Vec<_>>()
2214 );
2215 assert_eq!(succeeded.len(), 8);
2216 assert_eq!(source, source_before);
2217
2218 let mut seen = HashSet::new();
2221 let mut emitted = Vec::new();
2222 let mut attempts = 0_usize;
2223 for configuration in RangeBitsGenerator::new(workspace.center_count()) {
2224 attempts += 1;
2225 workspace.apply_configuration(&configuration).unwrap();
2226 if workspace
2227 .finalize_configuration_with_embedding(&configuration, true, true, &mut seen)
2228 .unwrap()
2229 .is_some()
2230 {
2231 emitted.push(configuration.value().clone());
2232 if emitted.len() == 2 {
2233 break;
2234 }
2235 }
2236 }
2237 assert_eq!(attempts, 2);
2238 assert_eq!(emitted, vec![BigUint::from(0_u8), BigUint::from(1_u8)]);
2239 }
2240
2241 #[test]
2242 fn python_enumerator_try_embedding_preserves_existing_conformers_and_copies_heavy_rows() {
2243 let base = Molecule::from_smiles("CC(F)Cl").unwrap();
2244 let existing_coordinates = (0..base.num_atoms())
2245 .map(|index| [index as f64, index as f64 + 0.25, index as f64 + 0.5])
2246 .collect::<Vec<_>>();
2247 let source = base
2248 .with_added_3d_conformer(existing_coordinates.clone(), true)
2249 .unwrap();
2250 let source_before = source.clone();
2251 let mut workspace = EnumerationWorkspace::prepare(
2252 &source,
2253 FlipperSelectionOptions {
2254 only_unassigned: true,
2255 only_stereo_groups: false,
2256 },
2257 )
2258 .unwrap();
2259 assert_eq!(workspace.center_count(), 1);
2260
2261 let configuration = ConfigurationBits::from_biguint(BigUint::from(1_u8));
2262 workspace.apply_configuration(&configuration).unwrap();
2263 let finalized = workspace
2264 .finalize_configuration(false, &mut HashSet::new())
2265 .unwrap()
2266 .unwrap();
2267 let embedded_a = workspace
2268 .apply_embedding_filter(finalized.clone(), &configuration)
2269 .unwrap()
2270 .unwrap();
2271 let embedded_b = workspace
2272 .apply_embedding_filter(finalized, &configuration)
2273 .unwrap()
2274 .unwrap();
2275
2276 assert_eq!(embedded_a.num_atoms(), source.num_atoms());
2277 assert_eq!(embedded_a.conformers_3d().len(), 2);
2278 assert_eq!(
2279 embedded_a.conformers_3d()[0].coordinates(),
2280 existing_coordinates
2281 );
2282 assert_eq!(embedded_a.conformers_3d()[0].id(), 0);
2283 assert_eq!(embedded_a.conformers_3d()[1].id(), 0);
2284 assert_eq!(
2285 embedded_a.conformers_3d()[1].coordinates().len(),
2286 source.num_atoms()
2287 );
2288 assert!(
2289 embedded_a.conformers_3d()[1]
2290 .coordinates()
2291 .iter()
2292 .flatten()
2293 .all(|coordinate| coordinate.is_finite())
2294 );
2295 assert_eq!(
2296 embedded_a.conformers_3d()[1].coordinates(),
2297 embedded_b.conformers_3d()[1].coordinates(),
2298 "the configuration-derived explicit seed must be deterministic"
2299 );
2300 assert_eq!(source, source_before);
2301 }
2302
2303 #[test]
2304 fn python_flippers_repeat_exact_atom_bond_and_group_setter_directions() {
2305 let mut builder = MoleculeBuilder::new();
2306 let atom =
2307 builder.add_atom(AtomSpec::new(Element::C).with_chiral_tag(ChiralTag::TetrahedralCw));
2308 let other =
2309 builder.add_atom(AtomSpec::new(Element::C).with_chiral_tag(ChiralTag::TetrahedralCcw));
2310 let untouched =
2311 builder.add_atom(AtomSpec::new(Element::F).with_chiral_tag(ChiralTag::Other));
2312 let end_controller = builder.add_atom(AtomSpec::new(Element::CL));
2313 let bond = builder
2314 .add_bond(
2315 BondSpec::new(atom, other, BondOrder::Double)
2316 .with_stereo_atoms(untouched, end_controller),
2317 )
2318 .unwrap();
2319 builder
2320 .add_bond(BondSpec::new(atom, untouched, BondOrder::Single))
2321 .unwrap();
2322 builder
2323 .add_bond(BondSpec::new(other, end_controller, BondOrder::Single))
2324 .unwrap();
2325 let mut molecule = builder.build().unwrap();
2326
2327 let atom_flipper = StereoFlipper::Atom { atom };
2328 atom_flipper.flip(&mut molecule, false).unwrap();
2329 assert_eq!(
2330 molecule.atoms()[atom.index()].chiral_tag(),
2331 ChiralTag::TetrahedralCcw
2332 );
2333 atom_flipper.flip(&mut molecule, true).unwrap();
2334 atom_flipper.flip(&mut molecule, true).unwrap();
2335 assert_eq!(
2336 molecule.atoms()[atom.index()].chiral_tag(),
2337 ChiralTag::TetrahedralCw
2338 );
2339
2340 let bond_flipper = StereoFlipper::Bond { bond };
2341 bond_flipper.flip(&mut molecule, false).unwrap();
2342 assert_eq!(molecule.bonds()[bond.index()].stereo(), BondStereo::Trans);
2343 bond_flipper.flip(&mut molecule, true).unwrap();
2344 bond_flipper.flip(&mut molecule, true).unwrap();
2345 assert_eq!(molecule.bonds()[bond.index()].stereo(), BondStereo::Cis);
2346
2347 let group_flipper = StereoFlipper::StereoGroup {
2348 original_parities: vec![
2349 (atom, ChiralTag::TetrahedralCw),
2350 (other, ChiralTag::TetrahedralCcw),
2351 (untouched, ChiralTag::Other),
2352 ],
2353 };
2354 group_flipper.flip(&mut molecule, false).unwrap();
2355 assert_eq!(
2356 molecule.atoms()[atom.index()].chiral_tag(),
2357 ChiralTag::TetrahedralCcw
2358 );
2359 assert_eq!(
2360 molecule.atoms()[other.index()].chiral_tag(),
2361 ChiralTag::TetrahedralCw
2362 );
2363 assert_eq!(
2364 molecule.atoms()[untouched.index()].chiral_tag(),
2365 ChiralTag::Other
2366 );
2367 group_flipper.flip(&mut molecule, true).unwrap();
2368 group_flipper.flip(&mut molecule, true).unwrap();
2369 assert_eq!(
2370 molecule.atoms()[atom.index()].chiral_tag(),
2371 ChiralTag::TetrahedralCw
2372 );
2373 assert_eq!(
2374 molecule.atoms()[other.index()].chiral_tag(),
2375 ChiralTag::TetrahedralCcw
2376 );
2377 assert_eq!(
2378 molecule.atoms()[untouched.index()].chiral_tag(),
2379 ChiralTag::Other
2380 );
2381 }
2382
2383 fn grouped_selection_molecule() -> (Molecule, AtomId, AtomId, AtomId, AtomId, BondId) {
2384 let mut builder = MoleculeBuilder::new();
2385 let assigned =
2386 builder.add_atom(AtomSpec::new(Element::C).with_chiral_tag(ChiralTag::TetrahedralCw));
2387 let unassigned = builder.add_atom(AtomSpec::new(Element::C));
2388 let begin_controller =
2389 builder.add_atom(AtomSpec::new(Element::F).with_chiral_tag(ChiralTag::TetrahedralCcw));
2390 let end_controller = builder.add_atom(AtomSpec::new(Element::CL));
2391 let double_bond = builder
2392 .add_bond(BondSpec::new(assigned, unassigned, BondOrder::Double))
2393 .unwrap();
2394 builder
2395 .add_bond(BondSpec::new(assigned, begin_controller, BondOrder::Single))
2396 .unwrap();
2397 builder
2398 .add_bond(BondSpec::new(unassigned, end_controller, BondOrder::Single))
2399 .unwrap();
2400 builder
2401 .add_stereo_group(StereoGroup::new(
2402 StereoGroupKind::Absolute,
2403 vec![assigned],
2404 Vec::new(),
2405 ))
2406 .unwrap();
2407 builder
2408 .add_stereo_group(StereoGroup::new(
2409 StereoGroupKind::Or,
2410 vec![assigned, unassigned],
2411 Vec::new(),
2412 ))
2413 .unwrap();
2414 builder
2415 .add_stereo_group(StereoGroup::new(
2416 StereoGroupKind::And,
2417 vec![begin_controller],
2418 Vec::new(),
2419 ))
2420 .unwrap();
2421 (
2422 builder.build().unwrap(),
2423 assigned,
2424 unassigned,
2425 begin_controller,
2426 end_controller,
2427 double_bond,
2428 )
2429 }
2430
2431 #[test]
2432 fn python_flipper_selection_filters_records_and_groups_in_source_order() {
2433 let (mut molecule, assigned, unassigned, begin_controller, end_controller, double_bond) =
2434 grouped_selection_molecule();
2435 let records = vec![
2436 stereo_info(
2437 StereoType::AtomTetrahedral,
2438 StereoSpecified::Specified,
2439 StereoCenter::Atom(assigned),
2440 Vec::new(),
2441 ),
2442 stereo_info(
2443 StereoType::AtomTetrahedral,
2444 StereoSpecified::Unspecified,
2445 StereoCenter::Atom(unassigned),
2446 Vec::new(),
2447 ),
2448 stereo_info(
2449 StereoType::BondDouble,
2450 StereoSpecified::Unknown,
2451 StereoCenter::Bond(double_bond),
2452 vec![
2453 ControllingAtom::Atom(begin_controller),
2454 ControllingAtom::Missing,
2455 ControllingAtom::Atom(end_controller),
2456 ControllingAtom::Missing,
2457 ],
2458 ),
2459 ];
2460
2461 let all = select_stereo_flippers_from_info(
2462 &mut molecule.clone(),
2463 &records,
2464 FlipperSelectionOptions {
2465 only_unassigned: false,
2466 only_stereo_groups: false,
2467 },
2468 )
2469 .unwrap();
2470 assert_eq!(
2471 all,
2472 vec![
2473 StereoFlipper::Atom { atom: assigned },
2474 StereoFlipper::Atom { atom: unassigned },
2475 StereoFlipper::Bond { bond: double_bond },
2476 ]
2477 );
2478
2479 let unassigned_only = select_stereo_flippers_from_info(
2480 &mut molecule,
2481 &records,
2482 FlipperSelectionOptions {
2483 only_unassigned: true,
2484 only_stereo_groups: false,
2485 },
2486 )
2487 .unwrap();
2488 assert_eq!(unassigned_only.len(), 4);
2489 assert_eq!(
2490 &unassigned_only[..2],
2491 &[
2492 StereoFlipper::Atom { atom: unassigned },
2493 StereoFlipper::Bond { bond: double_bond },
2494 ]
2495 );
2496 assert!(matches!(
2497 &unassigned_only[2],
2498 StereoFlipper::StereoGroup { original_parities }
2499 if original_parities == &vec![
2500 (assigned, ChiralTag::TetrahedralCw),
2501 (unassigned, ChiralTag::Unspecified),
2502 ]
2503 ));
2504 assert!(matches!(
2505 &unassigned_only[3],
2506 StereoFlipper::StereoGroup { original_parities }
2507 if original_parities == &vec![(begin_controller, ChiralTag::TetrahedralCcw)]
2508 ));
2509
2510 let groups_only = select_stereo_flippers_from_info(
2511 &mut molecule,
2512 &records,
2513 FlipperSelectionOptions {
2514 only_unassigned: true,
2515 only_stereo_groups: true,
2516 },
2517 )
2518 .unwrap();
2519 assert_eq!(groups_only, unassigned_only[2..]);
2520
2521 let disabled_groups_only = select_stereo_flippers_from_info(
2522 &mut molecule,
2523 &records,
2524 FlipperSelectionOptions {
2525 only_unassigned: false,
2526 only_stereo_groups: true,
2527 },
2528 )
2529 .unwrap();
2530 assert!(disabled_groups_only.is_empty());
2531 }
2532
2533 #[test]
2534 fn python_bond_flipper_selection_initializes_preserves_and_rejects_controllers_exactly() {
2535 let (molecule, _, _, begin_controller, end_controller, double_bond) =
2536 grouped_selection_molecule();
2537 let complete = stereo_info(
2538 StereoType::BondDouble,
2539 StereoSpecified::Unspecified,
2540 StereoCenter::Bond(double_bond),
2541 vec![
2542 ControllingAtom::Atom(begin_controller),
2543 ControllingAtom::Missing,
2544 ControllingAtom::Atom(end_controller),
2545 ControllingAtom::Missing,
2546 ],
2547 );
2548 let options = FlipperSelectionOptions {
2549 only_unassigned: false,
2550 only_stereo_groups: false,
2551 };
2552
2553 let mut initialized = molecule.clone();
2554 let flippers = select_stereo_flippers_from_info(
2555 &mut initialized,
2556 std::slice::from_ref(&complete),
2557 options,
2558 )
2559 .unwrap();
2560 assert_eq!(flippers, vec![StereoFlipper::Bond { bond: double_bond }]);
2561 assert_eq!(
2562 initialized.bonds()[double_bond.index()].stereo_atoms(),
2563 Some([begin_controller, end_controller])
2564 );
2565
2566 let mut preinitialized = molecule.clone();
2567 preinitialized.topology_block_mut().bonds[double_bond.index()]
2568 .set_stereo_atoms(Some([end_controller, begin_controller]));
2569 select_stereo_flippers_from_info(
2570 &mut preinitialized,
2571 std::slice::from_ref(&complete),
2572 options,
2573 )
2574 .unwrap();
2575 assert_eq!(
2576 preinitialized.bonds()[double_bond.index()].stereo_atoms(),
2577 Some([end_controller, begin_controller])
2578 );
2579
2580 for missing in [0, 2] {
2581 let mut controllers = complete.controlling_atoms().to_vec();
2582 controllers[missing] = ControllingAtom::Missing;
2583 let incomplete = stereo_info(
2584 StereoType::BondDouble,
2585 StereoSpecified::Unspecified,
2586 StereoCenter::Bond(double_bond),
2587 controllers,
2588 );
2589 let mut candidate = molecule.clone();
2590 let flippers = select_stereo_flippers_from_info(
2591 &mut candidate,
2592 std::slice::from_ref(&incomplete),
2593 options,
2594 )
2595 .unwrap();
2596 assert!(flippers.is_empty());
2597 assert_eq!(candidate.bonds()[double_bond.index()].stereo_atoms(), None);
2598 }
2599 }
2600
2601 #[test]
2602 fn python_flipper_selection_preserves_represented_atropisomers() {
2603 let mut builder = MoleculeBuilder::new();
2604 let begin = builder.add_atom(AtomSpec::new(Element::C));
2605 let end = builder.add_atom(AtomSpec::new(Element::C));
2606 let begin_controller = builder.add_atom(AtomSpec::new(Element::F));
2607 let end_controller = builder.add_atom(AtomSpec::new(Element::CL));
2608 let bond = builder
2609 .add_bond(BondSpec::new(begin, end, BondOrder::Single).with_stereo(BondStereo::AtropCw))
2610 .unwrap();
2611 builder
2612 .add_bond(BondSpec::new(begin, begin_controller, BondOrder::Single))
2613 .unwrap();
2614 builder
2615 .add_bond(BondSpec::new(end, end_controller, BondOrder::Single))
2616 .unwrap();
2617 let mut molecule = builder.build().unwrap();
2618 let info = stereo_info(
2619 StereoType::BondAtropisomer,
2620 StereoSpecified::Specified,
2621 StereoCenter::Bond(bond),
2622 vec![
2623 ControllingAtom::Atom(begin_controller),
2624 ControllingAtom::Missing,
2625 ControllingAtom::Atom(end_controller),
2626 ControllingAtom::Missing,
2627 ],
2628 );
2629
2630 let flippers = select_stereo_flippers_from_info(
2631 &mut molecule,
2632 &[info],
2633 FlipperSelectionOptions {
2634 only_unassigned: false,
2635 only_stereo_groups: false,
2636 },
2637 )
2638 .unwrap();
2639 assert!(flippers.is_empty());
2640 assert_eq!(molecule.bonds()[bond.index()].stereo(), BondStereo::AtropCw);
2641 assert_eq!(molecule.bonds()[bond.index()].stereo_atoms(), None);
2642 }
2643}