Coverage Report

Created: 2026-09-28 07:21

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/rdkit/Code/GraphMol/FindStereo.cpp
Line
Count
Source
1
//
2
//  Copyright (C) 2020 Greg Landrum and T5 Informatics GmbH
3
//
4
//   @@ All Rights Reserved @@
5
//  This file is part of the RDKit.
6
//  The contents are covered by the terms of the BSD license
7
//  which is included in the file license.txt, found at the root
8
//  of the RDKit source tree.
9
//
10
#include <GraphMol/RDKitBase.h>
11
#include <GraphMol/new_canon.h>
12
#include <RDGeneral/types.h>
13
#include <algorithm>
14
#include <numeric>
15
#include <RDGeneral/utils.h>
16
#include <RDGeneral/Invariant.h>
17
#include <RDGeneral/RDLog.h>
18
19
#include <boost/dynamic_bitset.hpp>
20
#include "Chirality.h"
21
#include <GraphMol/QueryOps.h>
22
23
namespace RDKit {
24
namespace Chirality {
25
26
namespace detail {
27
28
0
bool isAtomPotentialNontetrahedralCenter(const Atom *atom) {
29
0
  PRECONDITION(atom, "atom is null");
30
0
  auto nzdegree = Chirality::detail::getAtomNonzeroDegree(atom);
31
0
  auto impHDegree = atom->getTotalNumHs();
32
0
  auto tnzdegree = nzdegree + impHDegree;
33
0
  auto anum = atom->getAtomicNum();
34
0
  if (tnzdegree > 6 || tnzdegree < 2 || (anum < 12 && anum != 4)) {
35
0
    return false;
36
0
  }
37
0
  auto chiralType = atom->getChiralTag();
38
0
  if (chiralType >= Atom::ChiralType::CHI_SQUAREPLANAR &&
39
0
      chiralType <= Atom::ChiralType::CHI_OCTAHEDRAL) {
40
0
    return true;
41
0
  }
42
43
  // with at least four neighbors but nothing specified we can start to imagine
44
  // that it might be enhanced stereo
45
0
  if (chiralType == Atom::ChiralType::CHI_UNSPECIFIED && tnzdegree >= 4) {
46
0
    return true;
47
0
  }
48
49
0
  return false;
50
0
}
51
0
bool isAtomPotentialTetrahedralCenter(const Atom *atom) {
52
0
  PRECONDITION(atom, "atom is null");
53
0
  auto nzDegree = getAtomNonzeroDegree(atom);
54
0
  auto tnzDegree = nzDegree + atom->getTotalNumHs();
55
0
  if (tnzDegree > 4) {
56
0
    return false;
57
0
  } else {
58
0
    const auto &mol = atom->getOwningMol();
59
0
    if (nzDegree == 4) {
60
      // chirality is always possible with 4 nbrs
61
0
      return true;
62
0
    } else if (nzDegree <= 1) {
63
      // chirality is never possible with 0 or 1 nbr
64
0
      return false;
65
0
    } else if (nzDegree < 3 &&
66
0
               (atom->getAtomicNum() != 15 && atom->getAtomicNum() != 33)) {
67
      // less than three neighbors is never stereogenic
68
      // unless it is a phosphine/arsine with implicit H
69
0
      return false;
70
0
    } else if (atom->getAtomicNum() == 15 || atom->getAtomicNum() == 33) {
71
      // from logical flow: degree is 2 or 3 (implicit H)
72
      // Since InChI Software v. 1.02-standard (2009), phosphines and arsines
73
      // are always treated as stereogenic even with H atom neighbors.
74
      // Accept automatically.
75
0
      return true;
76
0
    } else if (nzDegree == 3) {
77
      // three-coordinate with a single H we'll accept automatically:
78
0
      if (atom->getTotalNumHs() == 1) {
79
0
        if (detail::has_protium_neighbor(mol, atom)) {
80
          // more than one H is never stereogenic
81
0
          return false;
82
0
        }
83
0
        return true;
84
0
      } else {
85
        // otherwise we default to not being a legal center
86
0
        bool legalCenter = false;
87
        // but there are a few special cases we'll accept
88
        // sulfur or selenium with either a positive charge or a double
89
        // bond:
90
0
        if ((atom->getAtomicNum() == 16 || atom->getAtomicNum() == 34) &&
91
0
            (atom->getValence(Atom::ValenceType::EXPLICIT) == 4 ||
92
0
             (atom->getValence(Atom::ValenceType::EXPLICIT) == 3 &&
93
0
              atom->getFormalCharge() == 1))) {
94
0
          legalCenter = true;
95
0
        } else if (atom->getAtomicNum() == 7) {
96
          // three-coordinate N additional requirements:
97
          //   in a ring of size 3  (from InChI)
98
          // OR
99
          /// is a bridgehead atom (RDKit extension)
100
          // Also: cannot be SP2 hybridized or have a conjugated bond
101
          //   (this was Github #7434)
102
0
          if (atom->getHybridization() == Atom::HybridizationType::SP3 &&
103
0
              !MolOps::atomHasConjugatedBond(atom) &&
104
0
              (mol.getRingInfo()->isAtomInRingOfSize(atom->getIdx(), 3) ||
105
0
               queryIsAtomBridgehead(atom))) {
106
0
            legalCenter = true;
107
0
          }
108
0
        }
109
0
        return legalCenter;
110
0
      }
111
0
    } else {
112
0
      return false;
113
0
    }
114
0
  }
115
0
}
116
117
bool isAtomPotentialStereoAtom(const Atom *atom,
118
0
                               bool allowNontetrahehdralStereo) {
119
0
  return isAtomPotentialTetrahedralCenter(atom) ||
120
0
         (allowNontetrahehdralStereo &&
121
0
          isAtomPotentialNontetrahedralCenter(atom));
122
0
}
123
124
0
bool isAtomPotentialStereoAtom(const Atom *atom) {
125
0
  return isAtomPotentialStereoAtom(atom, getAllowNontetrahedralChirality());
126
0
}
127
128
0
StereoInfo getStereoInfo(const Bond *bond) {
129
0
  PRECONDITION(bond, "bond is null");
130
0
  StereoInfo sinfo;
131
0
  const auto beginAtom = bond->getBeginAtom();
132
0
  const auto endAtom = bond->getEndAtom();
133
0
  if (bond->getBondType() == Bond::BondType::DOUBLE) {
134
0
    if (beginAtom->getDegree() < 1 || endAtom->getDegree() < 1 ||
135
0
        beginAtom->getDegree() > 3 || endAtom->getDegree() > 3) {
136
0
      throw ValueErrorException("invalid atom degree in getStereoInfo(bond)");
137
0
    }
138
139
0
    sinfo.type = StereoType::Bond_Double;
140
0
    sinfo.centeredOn = bond->getIdx();
141
0
    sinfo.controllingAtoms.reserve(4);
142
143
0
    bool seenSquiggleBond = false;
144
0
    const auto &mol = bond->getOwningMol();
145
146
0
    auto explore_bond_end = [&mol, &bond, &sinfo,
147
0
                             &seenSquiggleBond](const Atom *atom) {
148
0
      for (const auto nbr : mol.atomBonds(atom)) {
149
0
        if (nbr->getIdx() != bond->getIdx()) {
150
0
          if (nbr->getBondDir() == Bond::BondDir::UNKNOWN) {
151
0
            seenSquiggleBond = true;
152
0
          }
153
0
          sinfo.controllingAtoms.push_back(
154
0
              nbr->getOtherAtomIdx(atom->getIdx()));
155
0
        }
156
0
      }
157
158
0
      for (unsigned i = atom->getDegree(); i < 3; ++i) {
159
0
        sinfo.controllingAtoms.push_back(Atom::NOATOM);
160
0
      }
161
0
    };
162
163
0
    explore_bond_end(beginAtom);
164
0
    explore_bond_end(endAtom);
165
166
0
    if (!seenSquiggleBond) {
167
      // check to see if either the begin or end atoms has the _UnknownStereo
168
      // property set. This happens if there was a squiggle bond to an H
169
0
      int explicitUnknownStereo = 0;
170
0
      if ((bond->getBeginAtom()->getPropIfPresent<int>(
171
0
               common_properties::_UnknownStereo, explicitUnknownStereo) &&
172
0
           explicitUnknownStereo) ||
173
0
          (bond->getEndAtom()->getPropIfPresent<int>(
174
0
               common_properties::_UnknownStereo, explicitUnknownStereo) &&
175
0
           explicitUnknownStereo)) {
176
0
        seenSquiggleBond = true;
177
0
      }
178
0
    }
179
180
0
    Bond::BondStereo stereo = bond->getStereo();
181
0
    if (stereo == Bond::BondStereo::STEREOANY ||
182
0
        bond->getBondDir() == Bond::BondDir::EITHERDOUBLE || seenSquiggleBond) {
183
0
      sinfo.specified = Chirality::StereoSpecified::Unknown;
184
0
    } else if (stereo != Bond::BondStereo::STEREONONE) {
185
0
      if (stereo == Bond::BondStereo::STEREOE ||
186
0
          stereo == Bond::BondStereo::STEREOZ) {
187
0
        stereo = Chirality::translateEZLabelToCisTrans(stereo);
188
0
      }
189
0
      sinfo.specified = Chirality::StereoSpecified::Specified;
190
0
      const auto satoms = bond->getStereoAtoms();
191
0
      if (satoms.size() != 2) {
192
0
        throw ValueErrorException("only can support 2 stereo neighbors");
193
0
      }
194
0
      bool firstAtBegin;
195
0
      if (satoms[0] == static_cast<int>(sinfo.controllingAtoms[0])) {
196
0
        firstAtBegin = true;
197
0
      } else if (satoms[0] == static_cast<int>(sinfo.controllingAtoms[1])) {
198
0
        firstAtBegin = false;
199
0
      } else {
200
0
        throw ValueErrorException("controlling atom mismatch at begin");
201
0
      }
202
0
      bool firstAtEnd;
203
0
      if (satoms[1] == static_cast<int>(sinfo.controllingAtoms[2])) {
204
0
        firstAtEnd = true;
205
0
      } else if (satoms[1] == static_cast<int>(sinfo.controllingAtoms[3])) {
206
0
        firstAtEnd = false;
207
0
      } else {
208
0
        throw ValueErrorException("controlling atom mismatch at end");
209
0
      }
210
0
      auto mismatch = firstAtBegin ^ firstAtEnd;
211
0
      if (mismatch) {
212
0
        stereo = (stereo == Bond::BondStereo::STEREOCIS
213
0
                      ? Bond::BondStereo::STEREOTRANS
214
0
                      : Bond::BondStereo::STEREOCIS);
215
0
      }
216
0
      switch (stereo) {
217
0
        case Bond::BondStereo::STEREOCIS:
218
0
          sinfo.descriptor = Chirality::StereoDescriptor::Bond_Cis;
219
0
          break;
220
0
        case Bond::BondStereo::STEREOTRANS:
221
0
          sinfo.descriptor = Chirality::StereoDescriptor::Bond_Trans;
222
0
          break;
223
0
        default:
224
0
          UNDER_CONSTRUCTION("unrecognized bond stereo type");
225
0
      }
226
0
    } else {
227
0
      sinfo.specified = Chirality::StereoSpecified::Unspecified;
228
0
    }
229
0
  } else if (bond->getBondType() == Bond::BondType::SINGLE &&
230
0
             (bond->getStereo() == Bond::BondStereo::STEREOATROPCCW ||
231
0
              bond->getStereo() == Bond::BondStereo::STEREOATROPCW)) {
232
0
    if (beginAtom->getDegree() < 2 || endAtom->getDegree() < 2 ||
233
0
        beginAtom->getDegree() > 3 || endAtom->getDegree() > 3) {
234
0
      throw ValueErrorException("invalid atom degree in getStereoInfo(bond)");
235
0
    }
236
237
0
    sinfo.type = StereoType::Bond_Atropisomer;
238
0
    sinfo.centeredOn = bond->getIdx();
239
0
    sinfo.controllingAtoms.reserve(4);
240
241
0
    const auto &mol = bond->getOwningMol();
242
0
    for (const auto nbr : mol.atomBonds(beginAtom)) {
243
0
      if (nbr->getIdx() != bond->getIdx()) {
244
0
        sinfo.controllingAtoms.push_back(
245
0
            nbr->getOtherAtomIdx(beginAtom->getIdx()));
246
0
      }
247
0
    }
248
0
    if (beginAtom->getDegree() == 2) {
249
0
      sinfo.controllingAtoms.push_back(Atom::NOATOM);
250
0
    }
251
0
    for (const auto nbr : mol.atomBonds(endAtom)) {
252
0
      if (nbr->getIdx() != bond->getIdx()) {
253
0
        sinfo.controllingAtoms.push_back(
254
0
            nbr->getOtherAtomIdx(endAtom->getIdx()));
255
0
      }
256
0
    }
257
0
    if (endAtom->getDegree() == 2) {
258
0
      sinfo.controllingAtoms.push_back(Atom::NOATOM);
259
0
    }
260
261
0
    Bond::BondStereo stereo = bond->getStereo();
262
0
    sinfo.specified = Chirality::StereoSpecified::Specified;
263
0
    switch (stereo) {
264
0
      case Bond::BondStereo::STEREOATROPCW:
265
0
        sinfo.descriptor = Chirality::StereoDescriptor::Bond_AtropCW;
266
0
        break;
267
0
      case Bond::BondStereo::STEREOATROPCCW:
268
0
        sinfo.descriptor = Chirality::StereoDescriptor::Bond_AtropCCW;
269
0
        break;
270
0
      default:
271
0
        UNDER_CONSTRUCTION("unrecognized bond stereo type");
272
0
    }
273
0
  } else {
274
0
    sinfo.type = StereoType::Unspecified;
275
0
  }
276
277
0
  return sinfo;
278
0
}
279
280
0
StereoInfo getStereoInfo(const Atom *atom) {
281
0
  PRECONDITION(atom, "atom is null");
282
0
  StereoInfo sinfo;
283
284
0
  sinfo.type = StereoType::Atom_Tetrahedral;
285
0
  sinfo.centeredOn = atom->getIdx();
286
0
  sinfo.controllingAtoms.reserve(atom->getDegree());
287
288
0
  const auto &mol = atom->getOwningMol();
289
0
  int explicitUnknownStereo = 0;
290
0
  for (const auto &nbri : boost::make_iterator_range(mol.getAtomBonds(atom))) {
291
0
    const auto &bnd = mol[nbri];
292
0
    if (bnd->getBondDir() == Bond::UNKNOWN) {
293
0
      explicitUnknownStereo = 1;
294
0
    } else if (!explicitUnknownStereo) {
295
0
      bnd->getPropIfPresent<int>(common_properties::_UnknownStereo,
296
0
                                 explicitUnknownStereo);
297
0
    }
298
0
    sinfo.controllingAtoms.push_back(bnd->getOtherAtomIdx(atom->getIdx()));
299
0
  }
300
0
  std::vector<unsigned> origNbrOrder = sinfo.controllingAtoms;
301
0
  std::sort(sinfo.controllingAtoms.begin(), sinfo.controllingAtoms.end());
302
303
0
  if (explicitUnknownStereo) {
304
0
    sinfo.specified = StereoSpecified::Unknown;
305
0
  } else {
306
0
    Atom::ChiralType stereo = atom->getChiralTag();
307
0
    if (stereo == Atom::ChiralType::CHI_TETRAHEDRAL_CCW ||
308
0
        stereo == Atom::ChiralType::CHI_TETRAHEDRAL_CW) {
309
0
      sinfo.specified = StereoSpecified::Specified;
310
0
      unsigned nSwaps =
311
0
          countSwapsToInterconvert(origNbrOrder, sinfo.controllingAtoms);
312
0
      if (nSwaps % 2) {
313
0
        stereo = (stereo == Atom::ChiralType::CHI_TETRAHEDRAL_CCW
314
0
                      ? Atom::ChiralType::CHI_TETRAHEDRAL_CW
315
0
                      : Atom::ChiralType::CHI_TETRAHEDRAL_CCW);
316
0
      }
317
0
      switch (stereo) {
318
0
        case Atom::ChiralType::CHI_TETRAHEDRAL_CCW:
319
0
          sinfo.descriptor = StereoDescriptor::Tet_CCW;
320
0
          break;
321
0
        case Atom::ChiralType::CHI_TETRAHEDRAL_CW:
322
0
          sinfo.descriptor = StereoDescriptor::Tet_CW;
323
0
          break;
324
0
        default:
325
0
          UNDER_CONSTRUCTION("unrecognized chiral flag");
326
0
      }
327
0
    } else if (getAllowNontetrahedralChirality() &&
328
0
               isAtomPotentialNontetrahedralCenter(atom)) {
329
0
      if (stereo == Atom::CHI_UNSPECIFIED) {
330
0
        switch (atom->getTotalDegree()) {
331
0
          case 4:
332
            // don't assume non-tetrahedral chirality
333
0
            stereo = Atom::ChiralType::CHI_TETRAHEDRAL;
334
0
            break;
335
0
          case 5:
336
0
            stereo = Atom::ChiralType::CHI_TRIGONALBIPYRAMIDAL;
337
0
            break;
338
0
          case 6:
339
0
            stereo = Atom::ChiralType::CHI_OCTAHEDRAL;
340
0
            break;
341
0
          default:
342
0
            break;
343
0
        }
344
0
      }
345
0
      sinfo.descriptor = StereoDescriptor::None;
346
0
      switch (stereo) {
347
0
        case Atom::ChiralType::CHI_TETRAHEDRAL:
348
0
          sinfo.type = StereoType::Atom_Tetrahedral;
349
0
          break;
350
0
        case Atom::ChiralType::CHI_SQUAREPLANAR:
351
0
          sinfo.type = StereoType::Atom_SquarePlanar;
352
0
          break;
353
0
        case Atom::ChiralType::CHI_TRIGONALBIPYRAMIDAL:
354
0
          sinfo.type = StereoType::Atom_TrigonalBipyramidal;
355
0
          break;
356
0
        case Atom::ChiralType::CHI_OCTAHEDRAL:
357
0
          sinfo.type = StereoType::Atom_Octahedral;
358
0
          break;
359
0
        default:
360
0
          break;
361
0
      }
362
0
      unsigned int permutation;
363
0
      if (atom->getPropIfPresent(common_properties::_chiralPermutation,
364
0
                                 permutation)) {
365
0
        sinfo.permutation = permutation;
366
0
        if (!permutation) {
367
          // a permutation of zero is an explicit statement that the chirality
368
          // is unknown
369
0
          sinfo.specified = Chirality::StereoSpecified::Unknown;
370
0
        } else {
371
0
          sinfo.specified = Chirality::StereoSpecified::Specified;
372
0
        }
373
0
      }
374
0
    }
375
0
  }
376
0
  return sinfo;
377
0
}
378
379
0
bool isBondPotentialStereoBond(const Bond *bond) {
380
0
  PRECONDITION(bond, "bond is null");
381
0
  if (bond->getBondType() != Bond::BondType::DOUBLE) {
382
0
    return false;
383
0
  }
384
385
  // at the moment the condition for being a potential stereo bond is that
386
  // each of the beginning and end neighbors must have at least 2 explicit
387
  // neighbors but no more than 3 total neighbors.
388
  // if it's a ring bond, the smallest ring it's in must have at least 8
389
  // members
390
  //  (this is common with InChI)
391
0
  const auto beginAtom = bond->getBeginAtom();
392
0
  auto begDegree = beginAtom->getTotalDegree();
393
0
  const auto endAtom = bond->getEndAtom();
394
0
  auto endDegree = endAtom->getTotalDegree();
395
0
  if (begDegree > 1 && begDegree < 4 && endDegree > 1 && endDegree < 4 &&
396
0
      beginAtom->getTotalNumHs(true) < 2 && endAtom->getTotalNumHs(true) < 2) {
397
    // check rings
398
0
    const auto ri = bond->getOwningMol().getRingInfo();
399
0
    for (const auto &bring : ri->bondRings()) {
400
0
      if (bring.size() < minRingSizeForDoubleBondStereo &&
401
0
          std::find(bring.begin(), bring.end(), bond->getIdx()) !=
402
0
              bring.end()) {
403
0
        return false;
404
0
      }
405
0
    }
406
0
    return true;
407
0
  } else {
408
0
    return false;
409
0
  }
410
0
}
411
}  // namespace detail
412
413
0
std::string getBondSymbol(const Bond *bond) {
414
  // FIX: this is not complete
415
0
  PRECONDITION(bond, "bad bond");
416
0
  std::string res;
417
0
  if (bond->getIsAromatic()) {
418
0
    res = ":";
419
0
  } else {
420
0
    switch (bond->getBondType()) {
421
0
      case Bond::BondType::SINGLE:
422
0
        res = "-";
423
0
        break;
424
0
      case Bond::BondType::DOUBLE:
425
0
        res = "=";
426
0
        break;
427
0
      case Bond::BondType::TRIPLE:
428
0
        res = "#";
429
0
        break;
430
0
      case Bond::BondType::AROMATIC:
431
0
        res = ":";
432
0
        break;
433
0
      default:
434
0
        res = "?";
435
0
        break;
436
0
    }
437
0
  }
438
0
  return res;
439
0
}
440
441
namespace {
442
enum class AtomCompareState : std::uint32_t {
443
  None,
444
  Unknown,
445
  Possible,
446
  CCW,
447
  CW,
448
  Stereo,
449
};
450
451
inline Canon::AtomCompareCode makeAtomCompareCode(const Atom &atom,
452
0
                                                  std::uint32_t symbolRank) {
453
0
  int atomMapNumber = 0;
454
0
  if (atom.getAtomicNum() == 0) {
455
0
    atom.getPropIfPresent(common_properties::molAtomMapNumber, atomMapNumber);
456
0
  }
457
  // Pack isotope, symbol rank, and biased formal charge in comparison order.
458
0
  const auto charge = static_cast<std::uint8_t>(atom.getFormalCharge() + 128);
459
0
  return {atomMapNumber, atom.getDegree(),
460
0
          (std::uint64_t{atom.getIsotope()} << 40) |
461
0
              (std::uint64_t{symbolRank} << 8) | charge,
462
0
          0};
463
0
}
464
465
void setAtomCompareState(Canon::AtomCompareCode &code, AtomCompareState state,
466
0
                         std::uint32_t discriminator = 0) {
467
0
  code.secondary = (static_cast<std::uint64_t>(state) << 32) | discriminator;
468
0
}
469
470
bool areStereobondControllingAtomsDupes(
471
    const ROMol &mol, const Bond &bond, unsigned controllingAtom1,
472
    unsigned controllingAtom2, const std::vector<unsigned> &atomRanks,
473
    const boost::dynamic_bitset<> &possibleAtoms,
474
    const boost::dynamic_bitset<> &knownAtoms,
475
    const boost::dynamic_bitset<> &possibleBonds,
476
0
    const boost::dynamic_bitset<> &knownBonds) {
477
0
  PRECONDITION(
478
0
      controllingAtom1 != Atom::NOATOM && controllingAtom2 != Atom::NOATOM,
479
0
      "Missing a controlling atom");
480
481
0
  if (atomRanks[controllingAtom1] != atomRanks[controllingAtom2]) {
482
0
    return false;
483
0
  }
484
485
  // Now that we know we have 2 neighbors with the same rank, check whether
486
  // there is a common even sized ring between the controlling atoms in which
487
  // the atom opposite of the bond may be chiral, which would break the tie
488
0
  auto ringInfo = mol.getRingInfo();
489
0
  auto atom1Members = ringInfo->atomMembers(controllingAtom1);
490
0
  auto atom2Members = ringInfo->atomMembers(controllingAtom2);
491
492
0
  auto it1 = atom1Members.begin();
493
0
  auto it2 = atom2Members.begin();
494
0
  while (it1 != atom1Members.end() && it2 != atom2Members.end()) {
495
0
    if (*it1 < *it2) {
496
0
      ++it1;
497
0
      continue;
498
0
    } else if (*it1 > *it2) {
499
0
      ++it2;
500
0
      continue;
501
0
    }
502
503
0
    auto ring = ringInfo->atomRings().at(*it1);
504
0
    ++it1;
505
0
    ++it2;
506
507
0
    if (ring.size() % 2) {
508
      // The common ring is odd-sized, so we can't have a tie-breaking atom
509
      // directly across the ring, so skip this ring.
510
0
      continue;
511
0
    }
512
513
0
    for (auto bondEnd : {bond.getBeginAtomIdx(), bond.getEndAtomIdx()}) {
514
0
      auto bondEndPosItr = std::find(ring.begin(), ring.end(), bondEnd);
515
0
      if (bondEndPosItr != ring.end()) {
516
0
        auto bondEndPos = bondEndPosItr - ring.begin();
517
0
        auto oppositePos = (bondEndPos + ring.size() / 2) % ring.size();
518
519
0
        auto oppositeIdx = ring[oppositePos];
520
0
        if (possibleAtoms[oppositeIdx] || knownAtoms[oppositeIdx]) {
521
0
          return false;
522
0
        }
523
524
        // atropisomer test: find the bond from the opposite atom that in not in
525
        // the ring (if there are three bonds total)
526
527
0
        if (mol.getAtomWithIdx(oppositeIdx)->getDegree() == 3) {
528
0
          for (const auto &nbr :
529
0
               mol.atomBonds(mol.getAtomWithIdx(oppositeIdx))) {
530
0
            auto outOtherAtom = nbr->getOtherAtomIdx(oppositeIdx);
531
0
            auto bondOutPosItr =
532
0
                std::find(ring.begin(), ring.end(), outOtherAtom);
533
0
            if (bondOutPosItr == ring.end()) {
534
0
              if (possibleBonds[nbr->getIdx()] || knownBonds[nbr->getIdx()]) {
535
0
                return false;
536
0
              }
537
0
            }
538
0
          }
539
0
        }
540
0
      }
541
0
    }
542
0
  }
543
0
  return true;
544
0
}
545
546
}  // namespace
547
548
namespace {
549
void initAtomInfo(
550
    ROMol &mol, bool flagPossible, bool cleanIt,
551
    boost::dynamic_bitset<> &knownAtoms,
552
    std::vector<Canon::AtomCompareCode> &atomCompareCodes,
553
0
    boost::dynamic_bitset<> &possibleAtoms) {
554
0
  std::vector<std::string> uniqueSymbols;
555
0
  std::vector<std::uint32_t> symbolIds(mol.getNumAtoms());
556
0
  for (const auto atom : mol.atoms()) {
557
0
    auto symbol = atom->getSymbol();
558
0
    auto iter = std::ranges::find(uniqueSymbols, symbol);
559
0
    if (iter == uniqueSymbols.end()) {
560
0
      symbolIds[atom->getIdx()] =
561
0
          static_cast<std::uint32_t>(uniqueSymbols.size());
562
0
      uniqueSymbols.push_back(std::move(symbol));
563
0
    } else {
564
0
      symbolIds[atom->getIdx()] =
565
0
          static_cast<std::uint32_t>(iter - uniqueSymbols.begin());
566
0
    }
567
0
  }
568
0
  std::vector<std::uint32_t> symbolOrder(uniqueSymbols.size());
569
0
  std::iota(symbolOrder.begin(), symbolOrder.end(), 0);
570
0
  std::ranges::sort(
571
0
      symbolOrder, {}, [&uniqueSymbols](auto symbolId) -> const std::string & {
572
0
        return uniqueSymbols[symbolId];
573
0
      });
574
0
  std::vector<std::uint32_t> symbolRanks(uniqueSymbols.size());
575
0
  for (std::uint32_t rank = 0; rank < symbolOrder.size(); ++rank) {
576
0
    symbolRanks[symbolOrder[rank]] = rank;
577
0
  }
578
579
0
  bool allowNontetrahedralStereo = getAllowNontetrahedralChirality();
580
0
  for (const auto atom : mol.atoms()) {
581
0
    if (atom->needsUpdatePropertyCache()) {
582
0
      atom->updatePropertyCache(false);
583
0
    }
584
585
0
    auto aidx = atom->getIdx();
586
0
    atomCompareCodes[aidx] =
587
0
        makeAtomCompareCode(*atom, symbolRanks[symbolIds[aidx]]);
588
0
    if (detail::isAtomPotentialStereoAtom(atom, allowNontetrahedralStereo)) {
589
0
      auto sinfo = detail::getStereoInfo(atom);
590
0
      switch (sinfo.specified) {
591
0
        case Chirality::StereoSpecified::Unknown:
592
0
          knownAtoms.set(aidx);
593
0
          setAtomCompareState(atomCompareCodes[aidx], AtomCompareState::Unknown,
594
0
                              aidx);
595
0
          break;
596
0
        case Chirality::StereoSpecified::Specified:
597
0
          knownAtoms.set(aidx);
598
0
          if (sinfo.descriptor == StereoDescriptor::Tet_CCW) {
599
0
            setAtomCompareState(atomCompareCodes[aidx], AtomCompareState::CCW);
600
0
          } else if (sinfo.descriptor == StereoDescriptor::Tet_CW) {
601
0
            setAtomCompareState(atomCompareCodes[aidx], AtomCompareState::CW);
602
0
          } else {
603
0
            setAtomCompareState(atomCompareCodes[aidx],
604
0
                                AtomCompareState::Stereo);
605
0
          }
606
0
          break;
607
0
        case Chirality::StereoSpecified::Unspecified:
608
0
          if (flagPossible) {
609
0
            possibleAtoms.set(aidx);
610
0
            if (!cleanIt) {
611
0
              setAtomCompareState(atomCompareCodes[aidx],
612
0
                                  AtomCompareState::Possible, aidx);
613
0
            }
614
0
          }
615
0
          break;
616
0
        default:
617
0
          throw ValueErrorException("bad StereoInfo.specified type");
618
0
      }
619
0
    } else if (cleanIt) {
620
0
      atom->setChiralTag(Atom::ChiralType::CHI_UNSPECIFIED);
621
0
    }
622
0
  }
623
0
}
624
625
void initBondInfo(ROMol &mol, bool flagPossible, bool cleanIt,
626
                  boost::dynamic_bitset<> &knownBonds,
627
                  std::vector<std::string> &bondSymbols,
628
0
                  boost::dynamic_bitset<> &possibleBonds) {
629
0
  for (const auto bond : mol.bonds()) {
630
0
    auto bidx = bond->getIdx();
631
0
    bondSymbols[bidx] = getBondSymbol(bond);
632
0
    if (detail::isBondPotentialStereoBond(bond)) {
633
0
      auto sinfo = detail::getStereoInfo(bond);
634
0
      switch (sinfo.specified) {
635
0
        case Chirality::StereoSpecified::Unknown:
636
0
          knownBonds.set(bidx);
637
0
          bondSymbols[bidx] += "_" + std::to_string(bidx);
638
0
          break;
639
0
        case Chirality::StereoSpecified::Specified:
640
0
          knownBonds.set(bidx);
641
0
          if (sinfo.descriptor == StereoDescriptor::Bond_Cis) {
642
0
            bondSymbols[bidx] += "_cis";
643
0
          } else if (sinfo.descriptor == StereoDescriptor::Bond_Trans) {
644
0
            bondSymbols[bidx] += "_trans";
645
0
          } else {
646
0
            bondSymbols[bidx] += "_STEREO";
647
0
          }
648
0
          break;
649
0
        case Chirality::StereoSpecified::Unspecified:
650
0
          if (flagPossible) {
651
0
            possibleBonds.set(bidx);
652
0
            if (!cleanIt) {
653
0
              bondSymbols[bidx] += "_" + std::to_string(bidx);
654
0
            }
655
0
          }
656
0
          break;
657
0
        default:
658
0
          throw ValueErrorException("bad StereoInfo.specified type");
659
0
      }
660
0
    } else {
661
0
      auto currentStereo = bond->getStereo();
662
0
      if (currentStereo != Bond::BondStereo::STEREOATROPCW &&
663
0
          currentStereo != Bond::BondStereo::STEREOATROPCCW) {
664
0
        if (cleanIt) {
665
0
          bond->setStereo(Bond::BondStereo::STEREONONE);
666
0
        }
667
0
      } else {
668
0
        knownBonds.set(bidx);
669
0
        if (currentStereo == Bond::BondStereo::STEREOATROPCW) {
670
0
          bondSymbols[bidx] += "_atropcw";
671
0
        } else if (currentStereo == Bond::BondStereo::STEREOATROPCCW) {
672
0
          bondSymbols[bidx] += "_atropccw";
673
0
        }
674
0
      }
675
0
    }
676
0
  }
677
0
}
678
void flagRingStereo(ROMol &mol,
679
                    std::vector<unsigned int> &possibleRingStereoAtoms,
680
                    std::vector<unsigned int> &possibleRingStereoBonds,
681
                    const boost::dynamic_bitset<> &knownAtoms,
682
                    const boost::dynamic_bitset<> *possibleAtoms,
683
                    const boost::dynamic_bitset<> &knownBonds,
684
0
                    const boost::dynamic_bitset<> *possibleBonds) {
685
  // flag possible ring stereo cases. The relevant cases here are:
686
  //    1) even-sized rings with possible (or specified) atoms opposite each
687
  //       other, like CC1CC(C)C1 or CC1CCC(C)CC1
688
  //    2) atoms sharing a bond which fuses two or more rings, like the
689
  //    central
690
  //       bond in C1CCC2CCCCC2C1
691
692
0
  auto ringInfo = mol.getRingInfo();
693
0
  boost::dynamic_bitset<> possibleAtomsInRing(mol.getNumAtoms());
694
0
  for (unsigned int ridx = 0; ridx < ringInfo->atomRings().size(); ++ridx) {
695
0
    const auto &aring = ringInfo->atomRings()[ridx];
696
0
    const auto &bring = ringInfo->bondRings()[ridx];
697
0
    unsigned int nHere = 0;
698
0
    auto sz = aring.size();
699
0
    bool ringIsOddSized = sz % 2;
700
0
    auto halfSize = sz / 2 + ringIsOddSized;
701
702
0
    possibleAtomsInRing.reset();
703
0
    for (unsigned int ai = 0; ai < sz; ++ai) {
704
0
      auto aidx = aring[ai];
705
0
      if (!knownAtoms[aidx] && (!possibleAtoms || !possibleAtoms->test(aidx))) {
706
0
        continue;
707
0
      }
708
709
0
      for (unsigned int ringDivisor : {2, 3}) {
710
0
        bool ringIsMultipleOfDivisor = ((sz % ringDivisor) == 0);
711
0
        auto incrementSize = sz / ringDivisor;
712
713
0
        if (ringIsMultipleOfDivisor) {
714
          // find the two indices of the atoms 1/3 the way around the ring
715
716
0
          unsigned int otherFoundByBondCount = 0;
717
0
          unsigned int otherFoundByAtomCount = 0;
718
0
          for (unsigned int indexIncrement = incrementSize; indexIncrement < sz;
719
0
               indexIncrement += incrementSize) {
720
0
            auto otherIdx = aring[(ai + indexIncrement) % sz];
721
0
            auto otherAtom = mol.getAtomWithIdx(otherIdx);
722
723
0
            for (auto bond : mol.atomBonds(otherAtom)) {
724
0
              auto bidx = bond->getIdx();
725
0
              if ((knownBonds[bidx] ||
726
0
                   (possibleBonds && possibleBonds->test(bidx))) &&
727
0
                  std::find(bring.begin(), bring.end(), bidx) == bring.end()) {
728
0
                otherFoundByBondCount++;
729
0
                break;
730
0
              }
731
0
            }
732
0
            if (otherFoundByBondCount == 0) {
733
0
              if (knownAtoms[otherIdx] ||
734
0
                  (possibleAtoms && possibleAtoms->test(otherIdx))) {
735
0
                otherFoundByAtomCount++;
736
0
              }
737
0
            }
738
0
          }
739
740
0
          if (otherFoundByBondCount == ringDivisor - 1 ||
741
0
              otherFoundByAtomCount == ringDivisor - 1) {
742
0
            nHere += 1 + otherFoundByBondCount;
743
0
            for (unsigned int indexIncrement = 0; indexIncrement < sz;
744
0
                 indexIncrement += incrementSize) {
745
0
              possibleAtomsInRing.set(aring[(ai + indexIncrement) % sz]);
746
0
            }
747
0
            if (ringDivisor == 2) {
748
0
              mol.getAtomWithIdx(aidx)->setProp(
749
0
                  common_properties::_ringStereoOtherAtom,
750
0
                  aring[(ai + incrementSize) % sz]);
751
0
            }
752
753
0
            continue;
754
0
          }
755
0
        }
756
0
      }
757
758
      // if the atom is in more than one ring, explore the common edge to
759
      // see if we can find another potentially chiral atom
760
0
      if (ringInfo->numAtomRings(aidx) > 1) {
761
0
        auto previousOtherIdx = aidx;
762
0
        for (size_t step = 1; step <= halfSize; ++step) {
763
0
          auto otherIdx = aring[(ai + step) % sz];
764
0
          auto bnd = mol.getBondBetweenAtoms(previousOtherIdx, otherIdx);
765
0
          if (ringInfo->numBondRings(bnd->getIdx()) < 2) {
766
            // We reached the end of the common edge.
767
0
            break;
768
0
          }
769
0
          if (knownAtoms[otherIdx] ||
770
0
              (possibleAtoms && possibleAtoms->test(otherIdx))) {
771
            // We found another chiral atom, no need to keep
772
            // searching.
773
0
            nHere += 2;
774
0
            possibleAtomsInRing.set(aidx);
775
0
            possibleAtomsInRing.set(otherIdx);
776
0
            mol.getAtomWithIdx(aidx)->setProp(
777
0
                common_properties::_ringStereoOtherAtom, otherIdx);
778
0
            mol.getAtomWithIdx(otherIdx)->setProp(
779
0
                common_properties::_ringStereoOtherAtom, aidx);
780
0
            break;
781
0
          }
782
0
          previousOtherIdx = otherIdx;
783
0
        }
784
0
      }
785
0
    }
786
    // if the ring contains at least two atoms with possible stereo,
787
    // then each of those possibleAtoms should be included for ring stereo
788
0
    if (nHere > 1) {
789
0
      for (auto aidx : aring) {
790
0
        if (possibleAtomsInRing[aidx]) {
791
0
          ++possibleRingStereoAtoms[aidx];
792
0
        }
793
0
      }
794
0
      for (auto bidx : bring) {
795
0
        ++possibleRingStereoBonds[bidx];
796
0
      }
797
0
    }
798
0
  }
799
0
}
800
801
bool updateAtoms(
802
    ROMol &mol, const std::vector<unsigned int> &aranks,
803
    std::vector<Canon::AtomCompareCode> &atomCompareCodes,
804
    boost::dynamic_bitset<> &possibleAtoms, boost::dynamic_bitset<> &knownAtoms,
805
    boost::dynamic_bitset<> &fixedAtoms,
806
    std::vector<unsigned int> &possibleRingStereoAtoms,
807
    std::vector<unsigned int> &possibleRingStereoBonds,
808
0
    std::vector<StereoInfo> &sinfos) {
809
0
  bool needAnotherRound = false;
810
0
  for (const auto atom : mol.atoms()) {
811
0
    auto aidx = atom->getIdx();
812
0
    if (knownAtoms[aidx] || possibleAtoms[aidx]) {
813
0
      auto sinfo = detail::getStereoInfo(atom);
814
0
      if (fixedAtoms[aidx]) {
815
0
        sinfos.push_back(std::move(sinfo));
816
0
      } else {
817
0
        std::vector<unsigned int> nbrs;
818
0
        nbrs.reserve(sinfo.controllingAtoms.size());
819
0
        bool haveADupe = false;
820
0
        if (sinfo.type == StereoType::Atom_Tetrahedral) {
821
0
          for (auto nbrIdx : sinfo.controllingAtoms) {
822
0
            auto rnk = aranks[nbrIdx];
823
0
            if (std::find(nbrs.begin(), nbrs.end(), rnk) != nbrs.end()) {
824
              // ok, we just hit a duplicate rank. If the atom we're
825
              // concerned about is a candidate for ring stereo and the bond
826
              // to the atom with the duplicate rank is a ring bond
827
0
              if (possibleRingStereoAtoms[aidx]) {
828
0
                auto bnd = mol.getBondBetweenAtoms(aidx, nbrIdx);
829
0
                if (!bnd || !possibleRingStereoBonds[bnd->getIdx()]) {
830
0
                  haveADupe = true;
831
0
                  break;
832
0
                }
833
0
              } else {
834
0
                haveADupe = true;
835
0
                break;
836
0
              }
837
0
            } else {
838
0
              nbrs.push_back(rnk);
839
0
            }
840
0
          }
841
0
        }
842
0
        if (!haveADupe) {
843
0
          auto acs = atomCompareCodes[aidx];
844
0
          if (!possibleAtoms[aidx]) {
845
0
            auto sortednbrs = nbrs;
846
0
            std::sort(sortednbrs.begin(), sortednbrs.end());
847
            // FIX: only works for tetrahedral at the moment
848
0
            if (sinfo.type == Chirality::StereoType::Atom_Tetrahedral) {
849
0
              auto nSwaps = countSwapsToInterconvert(nbrs, sortednbrs);
850
0
              if (nSwaps % 2 &&
851
0
                  (sinfo.descriptor == Chirality::StereoDescriptor::Tet_CCW ||
852
0
                   sinfo.descriptor == Chirality::StereoDescriptor::Tet_CW)) {
853
0
                sinfo.descriptor =
854
0
                    sinfo.descriptor == Chirality::StereoDescriptor::Tet_CCW
855
0
                        ? Chirality::StereoDescriptor::Tet_CW
856
0
                        : Chirality::StereoDescriptor::Tet_CCW;
857
0
              }
858
0
              if (sinfo.descriptor == Chirality::StereoDescriptor::Tet_CW) {
859
0
                setAtomCompareState(acs, AtomCompareState::CW);
860
0
              } else if (sinfo.descriptor ==
861
0
                         Chirality::StereoDescriptor::Tet_CCW) {
862
0
                setAtomCompareState(acs, AtomCompareState::CCW);
863
0
              }
864
0
            }
865
0
            fixedAtoms.set(aidx);
866
0
          }
867
0
          if (atomCompareCodes[aidx] != acs) {
868
0
            atomCompareCodes[aidx] = acs;
869
0
            needAnotherRound = true;
870
0
          }
871
0
          sinfos.push_back(std::move(sinfo));
872
0
        } else {
873
          // Only do another round if we change anything here
874
0
          needAnotherRound |= possibleAtoms[aidx];
875
0
          possibleAtoms[aidx] = 0;
876
0
          setAtomCompareState(atomCompareCodes[aidx], AtomCompareState::None);
877
878
          // if this was creating possible ring stereo, update that info now
879
0
          if (possibleRingStereoAtoms[aidx]) {
880
0
            possibleRingStereoAtoms[aidx] = 0;
881
0
            needAnotherRound = true;
882
            // we're no longer in any ring with possible ring stereo. Go
883
            // update all the other atoms/bonds in rings that we're in:
884
0
            for (unsigned int ridx = 0;
885
0
                 ridx < mol.getRingInfo()->atomRings().size(); ++ridx) {
886
0
              const auto &aring = mol.getRingInfo()->atomRings()[ridx];
887
0
              unsigned int nHere = 0;
888
0
              for (auto raidx : aring) {
889
                // Ring stereo changed, so un-fix atoms in this ring so we can
890
                // recheck them in the next iteration, for the case that they
891
                // are no longer after the current atom was declared
892
                // non-chiral
893
0
                fixedAtoms[raidx] = false;
894
0
                nHere += (possibleRingStereoAtoms[raidx] > 0);
895
0
              }
896
0
              if (nHere <= 1) {
897
                // if the ring can't transmit stereo anymore, update the
898
                // counts
899
0
                if (nHere == 1) {
900
                  // update the last potential ring stereo atom in the ring,
901
                  // since it can't have ring stereo alone.
902
0
                  for (auto raidx : aring) {
903
0
                    if (possibleRingStereoAtoms[raidx]) {
904
0
                      --possibleRingStereoAtoms[raidx];
905
0
                      break;
906
0
                    }
907
0
                  }
908
0
                }
909
0
                for (auto rbidx : mol.getRingInfo()->bondRings()[ridx]) {
910
0
                  if (possibleRingStereoBonds[rbidx]) {
911
0
                    --possibleRingStereoBonds[rbidx];
912
0
                  }
913
0
                }
914
0
              }
915
0
            }
916
0
          }
917
0
        }
918
0
      }
919
0
    }
920
0
  }
921
0
  return needAnotherRound;
922
0
}
923
924
bool updateBonds(ROMol &mol, const std::vector<unsigned int> &aranks,
925
                 std::vector<std::string> &bondSymbols,
926
                 const boost::dynamic_bitset<> &possibleAtoms,
927
                 boost::dynamic_bitset<> &possibleBonds,
928
                 const boost::dynamic_bitset<> &knownAtoms,
929
                 const boost::dynamic_bitset<> &knownBonds,
930
                 boost::dynamic_bitset<> &fixedBonds,
931
0
                 std::vector<StereoInfo> &sinfos) {
932
0
  bool needAnotherRound = false;
933
0
  for (const auto bond : mol.bonds()) {
934
0
    auto bidx = bond->getIdx();
935
0
    if (knownBonds[bidx] || possibleBonds[bidx]) {
936
0
      auto sinfo = detail::getStereoInfo(bond);
937
0
      if (sinfo.type == Chirality::StereoType::Unspecified) {
938
0
        continue;  // not a double bond nor an atropisomer bond
939
0
      }
940
941
0
      ASSERT_INVARIANT(sinfo.controllingAtoms.size() == 4,
942
0
                       "bad controlling atoms size");
943
944
0
      if ((sinfo.controllingAtoms[0] == Atom::NOATOM &&
945
0
           sinfo.controllingAtoms[1] == Atom::NOATOM) ||
946
0
          (sinfo.controllingAtoms[2] == Atom::NOATOM &&
947
0
           sinfo.controllingAtoms[3] == Atom::NOATOM)) {
948
        // we have a bond with no neighbors on one side, which means it must
949
        // have a single implicit H on that side. Since the H is implicit,
950
        // there is no way to know whether it is cis or trans.
951
0
        ASSERT_INVARIANT(
952
0
            sinfo.specified != StereoSpecified::Specified,
953
0
            "stereo bond without neighbors can only be unspecified");
954
0
        fixedBonds.set(bidx);
955
0
      }
956
957
0
      if (fixedBonds[bidx]) {
958
0
        sinfos.push_back(std::move(sinfo));
959
0
      } else {
960
0
        bool haveADupe = false;
961
0
        bool needsSwap = false;
962
0
        if (sinfo.controllingAtoms[0] != Atom::NOATOM &&
963
0
            sinfo.controllingAtoms[1] != Atom::NOATOM) {
964
0
          if (areStereobondControllingAtomsDupes(
965
0
                  mol, *bond, sinfo.controllingAtoms[0],
966
0
                  sinfo.controllingAtoms[1], aranks, possibleAtoms, knownAtoms,
967
0
                  possibleBonds, knownBonds)) {
968
0
            haveADupe = true;
969
0
          } else if (aranks[sinfo.controllingAtoms[0]] <
970
0
                     aranks[sinfo.controllingAtoms[1]]) {
971
0
            std::swap(sinfo.controllingAtoms[0], sinfo.controllingAtoms[1]);
972
0
            needsSwap = !needsSwap;
973
0
          }
974
0
        }
975
0
        if (sinfo.controllingAtoms[2] != Atom::NOATOM &&
976
0
            sinfo.controllingAtoms[3] != Atom::NOATOM) {
977
0
          if (areStereobondControllingAtomsDupes(
978
0
                  mol, *bond, sinfo.controllingAtoms[2],
979
0
                  sinfo.controllingAtoms[3], aranks, possibleAtoms, knownAtoms,
980
0
                  possibleBonds, knownBonds)) {
981
0
            haveADupe = true;
982
0
          } else if (aranks[sinfo.controllingAtoms[2]] <
983
0
                     aranks[sinfo.controllingAtoms[3]]) {
984
0
            std::swap(sinfo.controllingAtoms[2], sinfo.controllingAtoms[3]);
985
0
            needsSwap = !needsSwap;
986
0
          }
987
0
        }
988
0
        if (!haveADupe) {
989
0
          if (needsSwap && (sinfo.descriptor == StereoDescriptor::Bond_Cis ||
990
0
                            sinfo.descriptor == StereoDescriptor::Bond_Trans)) {
991
0
            sinfo.descriptor = sinfo.descriptor == StereoDescriptor::Bond_Cis
992
0
                                   ? StereoDescriptor::Bond_Trans
993
0
                                   : StereoDescriptor::Bond_Cis;
994
0
          }
995
0
          auto gbs = bondSymbols[bidx];
996
0
          if (sinfo.specified == StereoSpecified::Specified) {
997
0
            switch (sinfo.descriptor) {
998
0
              case StereoDescriptor::Bond_Cis:
999
0
                gbs += "_cis";
1000
0
                break;
1001
0
              case StereoDescriptor::Bond_Trans:
1002
0
                gbs += "_trans";
1003
0
                break;
1004
0
              default:
1005
0
                break;
1006
0
            }
1007
0
          } else if (sinfo.specified == StereoSpecified::Unknown) {
1008
0
            gbs += "_unk";
1009
0
          }
1010
0
          if (bondSymbols[bidx] != gbs) {
1011
0
            bondSymbols[bidx] = gbs;
1012
0
            needAnotherRound = true;
1013
0
          }
1014
0
          if (!possibleBonds[bidx]) {
1015
0
            fixedBonds.set(bidx);
1016
0
          }
1017
0
          sinfos.push_back(std::move(sinfo));
1018
0
        } else if (possibleBonds[bidx]) {
1019
0
          possibleBonds[bidx] = 0;
1020
0
          bondSymbols[bidx] = getBondSymbol(bond);
1021
0
          needAnotherRound = true;
1022
0
        }
1023
0
      }
1024
0
    }
1025
0
  }
1026
0
  return needAnotherRound;
1027
0
}
1028
1029
void cleanMolStereo(ROMol &mol, const boost::dynamic_bitset<> &fixedAtoms,
1030
                    const boost::dynamic_bitset<> &knownAtoms,
1031
                    const boost::dynamic_bitset<> &fixedBonds,
1032
0
                    const boost::dynamic_bitset<> &knownBonds) {
1033
0
  for (auto atom : mol.atoms()) {
1034
0
    const auto i = atom->getIdx();
1035
0
    if (!fixedAtoms[i] && knownAtoms[i]) {
1036
0
      switch (atom->getChiralTag()) {
1037
0
        case Atom::ChiralType::CHI_TETRAHEDRAL_CCW:
1038
0
        case Atom::ChiralType::CHI_TETRAHEDRAL_CW:
1039
0
          atom->setChiralTag(Atom::ChiralType::CHI_UNSPECIFIED);
1040
0
          for (auto nbrBond : mol.atomBonds(atom)) {
1041
0
            auto bondDir = nbrBond->getBondDir();
1042
0
            if (bondDir == Bond::BondDir::BEGINDASH ||
1043
0
                bondDir == Bond::BondDir::BEGINWEDGE) {
1044
0
              nbrBond->setBondDir(Bond::BondDir::NONE);
1045
0
            }
1046
0
          }
1047
0
          break;
1048
0
        case Atom::ChiralType::CHI_TETRAHEDRAL:
1049
0
        case Atom::ChiralType::CHI_SQUAREPLANAR:
1050
0
        case Atom::ChiralType::CHI_TRIGONALBIPYRAMIDAL:
1051
0
        case Atom::ChiralType::CHI_OCTAHEDRAL:
1052
0
          atom->setProp(common_properties::_chiralPermutation, 0);
1053
0
          break;
1054
0
        default:
1055
0
          break;
1056
0
      }
1057
0
    }
1058
0
  }
1059
1060
0
  bool removedStereo = false;
1061
0
  for (auto bond : mol.bonds()) {
1062
0
    const auto i = bond->getIdx();
1063
0
    if (!fixedBonds[i] && knownBonds[i]) {
1064
0
      bond->setStereo(Bond::BondStereo::STEREONONE);
1065
0
      bond->setBondDir(Bond::BondDir::NONE);
1066
0
      bond->getStereoAtoms().clear();
1067
0
      removedStereo = true;
1068
0
    }
1069
0
  }
1070
1071
  // remove any slash bond dirs that do not have a stereo neighbor
1072
1073
0
  if (removedStereo) {
1074
0
    for (auto bond : mol.bonds()) {
1075
0
      auto bondDir = bond->getBondDir();
1076
0
      if (bondDir == Bond::BondDir::ENDDOWNRIGHT ||
1077
0
          bondDir == Bond::BondDir::ENDUPRIGHT) {
1078
0
        bool dirOk = false;
1079
0
        for (auto bondEnd : {bond->getBeginAtom(), bond->getEndAtom()}) {
1080
0
          for (auto nbrBond : mol.atomBonds(bondEnd)) {
1081
0
            if (nbrBond != bond &&
1082
0
                nbrBond->getStereo() != Bond::BondStereo::STEREONONE) {
1083
0
              dirOk = true;
1084
0
              break;
1085
0
            }
1086
0
          }
1087
0
          if (!dirOk) {
1088
0
            bond->setBondDir(Bond::BondDir::NONE);
1089
0
          }
1090
0
        }
1091
0
      }
1092
0
    }
1093
0
  }
1094
0
}
1095
}  // namespace
1096
1097
void findChiralAtomSpecialCases(ROMol &mol,
1098
                                boost::dynamic_bitset<> &possibleSpecialCases,
1099
                                const std::vector<unsigned int> &atomRanks);
1100
1101
std::vector<StereoInfo> runCleanup(ROMol &mol, bool flagPossible,
1102
0
                                   bool cleanIt) {
1103
  // This potentially does two passes of "canonicalization" to identify
1104
  // stereo atoms/bonds:
1105
  //   - if cleanIt is true we start with a pass which ignores possible
1106
  //   stereo atoms/bonds and which removes the stereo spec from any
1107
  //   atom/bond which doesn't have unique neighbors
1108
  //   - if flagPossible is true we do a pass where each unspecified
1109
  //   possible stereocenter is treated as if it were different from all
1110
  //   others. This allows us to identify every possible stereo atom/bond
1111
1112
0
  boost::dynamic_bitset<> knownAtoms(mol.getNumAtoms());
1113
0
  std::vector<Canon::AtomCompareCode> atomCompareCodes(mol.getNumAtoms());
1114
0
  boost::dynamic_bitset<> possibleAtoms(mol.getNumAtoms());
1115
0
  initAtomInfo(mol, flagPossible, cleanIt, knownAtoms, atomCompareCodes,
1116
0
               possibleAtoms);
1117
1118
0
  std::vector<std::string> bondSymbols(mol.getNumBonds());
1119
0
  boost::dynamic_bitset<> knownBonds(mol.getNumBonds());
1120
0
  boost::dynamic_bitset<> possibleBonds(mol.getNumBonds());
1121
0
  initBondInfo(mol, flagPossible, cleanIt, knownBonds, bondSymbols,
1122
0
               possibleBonds);
1123
1124
  // copy the original sets of possible atoms/bonds. We need them in the
1125
  // second pass
1126
0
  auto origPossibleAtoms = possibleAtoms;
1127
0
  auto origPossibleBonds = possibleBonds;
1128
1129
  // tracks the number of rings with possible ring stereo that the atom is
1130
  // in (only set for potential stereoatoms)
1131
0
  std::vector<unsigned int> possibleRingStereoAtoms(mol.getNumAtoms());
1132
  // tracks the number of rings with possible ring stereo that the bond is
1133
  // in (set for all bonds)
1134
0
  std::vector<unsigned int> possibleRingStereoBonds(mol.getNumBonds());
1135
1136
  // identify atoms which can be involved in ring stereo
1137
0
  flagRingStereo(mol, possibleRingStereoAtoms, possibleRingStereoBonds,
1138
0
                 knownAtoms, cleanIt ? nullptr : &possibleAtoms, knownBonds,
1139
0
                 cleanIt ? nullptr : &possibleBonds);
1140
1141
  // our return value
1142
0
  std::vector<StereoInfo> res;
1143
0
  if (!mol.getNumAtoms()) {
1144
0
    return res;
1145
0
  }
1146
1147
  // these are used to track which atoms/bonds have been altered
1148
0
  boost::dynamic_bitset<> fixedAtoms(mol.getNumAtoms());
1149
0
  boost::dynamic_bitset<> fixedBonds(mol.getNumBonds());
1150
1151
  // used to tell rankFragmentAtoms to use all atoms:
1152
0
  boost::dynamic_bitset<> atomsInPlay(mol.getNumAtoms());
1153
0
  atomsInPlay.set();
1154
0
  boost::dynamic_bitset<> bondsInPlay(mol.getNumBonds());
1155
0
  bondsInPlay.set();
1156
1157
0
  std::vector<Canon::canon_atom> canonAtoms(mol.getNumAtoms());
1158
0
  std::vector<int> canonNeighborIds(2 * mol.getNumBonds());
1159
0
  Canon::detail::initFragmentCanonAtoms(mol, canonAtoms, false, nullptr,
1160
0
                                        &bondSymbols, atomsInPlay, bondsInPlay,
1161
0
                                        canonNeighborIds, true);
1162
0
  Canon::AtomCompareFunctor ftor(&canonAtoms.front(), mol, &atomsInPlay,
1163
0
                                 &bondsInPlay, atomCompareCodes.data());
1164
0
  std::vector<int> atomOrder(mol.getNumAtoms());
1165
0
  std::vector<unsigned int> aranks(mol.getNumAtoms());
1166
0
  const auto updateRanks = [&]() {
1167
0
    const bool includeChirality = false;
1168
0
    const bool breakTies = false;
1169
0
    Canon::detail::rankWithFunctor(ftor, breakTies, atomOrder, true,
1170
0
                                   includeChirality, false, &atomsInPlay,
1171
0
                                   &bondsInPlay);
1172
0
    for (unsigned int i = 0; i < mol.getNumAtoms(); ++i) {
1173
0
      aranks[atomOrder[i]] = canonAtoms[atomOrder[i]].index;
1174
0
    }
1175
0
  };
1176
1177
0
  bool needAnotherRound = true;
1178
0
  while (needAnotherRound) {
1179
0
    res.clear();
1180
    // Apply the canonical atom ranking code with basic connectivity invariants.
1181
    // The necessary condition for chirality is that an atom's neighbors have
1182
    // unique ranks. The molecular topology is constant across cleanup rounds,
1183
    // so reuse the initialized canonical atoms while the symbol values change.
1184
0
    updateRanks();
1185
    // check if any new atoms definitely now have stereo; do another loop if
1186
    // so
1187
0
    needAnotherRound = updateAtoms(
1188
0
        mol, aranks, atomCompareCodes, possibleAtoms, knownAtoms, fixedAtoms,
1189
0
        possibleRingStereoAtoms, possibleRingStereoBonds, res);
1190
    // check if any new bonds definitely now have stereo; do another loop if
1191
    // so
1192
0
    needAnotherRound |=
1193
0
        updateBonds(mol, aranks, bondSymbols, possibleAtoms, possibleBonds,
1194
0
                    knownAtoms, knownBonds, fixedBonds, res);
1195
0
  }
1196
1197
0
  if (cleanIt) {
1198
    // remove stereo specs from atoms/bonds which should not have them
1199
0
    cleanMolStereo(mol, fixedAtoms, knownAtoms, fixedBonds, knownBonds);
1200
0
  }
1201
0
  if (flagPossible && (possibleAtoms != origPossibleAtoms ||
1202
0
                       possibleBonds != origPossibleBonds)) {
1203
    // if we're doing "flagPossible" mode and have done some cleanup, then
1204
    // we need to do another iteration
1205
1206
0
    possibleAtoms = origPossibleAtoms;
1207
    // flag every center/bond where we removed stereo as possible:
1208
0
    for (auto i = 0u; i < mol.getNumAtoms(); ++i) {
1209
0
      if (!fixedAtoms[i] && knownAtoms[i]) {
1210
0
        possibleAtoms[i] = 1;
1211
0
        knownAtoms[i] = 0;
1212
0
      }
1213
0
      if (possibleAtoms[i]) {
1214
0
        setAtomCompareState(atomCompareCodes[i], AtomCompareState::Possible, i);
1215
0
      }
1216
0
    }
1217
0
    possibleBonds = origPossibleBonds;
1218
0
    for (auto i = 0u; i < mol.getNumBonds(); ++i) {
1219
0
      if (!fixedBonds[i] && knownBonds[i]) {
1220
0
        possibleBonds[i] = 1;
1221
0
        knownBonds[i] = 0;
1222
0
      }
1223
0
      if (possibleBonds[i]) {
1224
0
        bondSymbols[i] += "_" + std::to_string(i);
1225
0
      }
1226
0
    }
1227
1228
0
    flagRingStereo(mol, possibleRingStereoAtoms, possibleRingStereoBonds,
1229
0
                   knownAtoms, &possibleAtoms, knownBonds, &possibleBonds);
1230
1231
0
    needAnotherRound = true;
1232
0
    while (needAnotherRound) {
1233
0
      res.clear();
1234
1235
      // std::copy(bondSymbols.begin(), bondSymbols.end(),
1236
      //           std::ostream_iterator<std::string>(std::cerr, " "));
1237
      // std::cerr << std::endl;
1238
1239
0
      updateRanks();
1240
0
      needAnotherRound = updateAtoms(
1241
0
          mol, aranks, atomCompareCodes, possibleAtoms, knownAtoms, fixedAtoms,
1242
0
          possibleRingStereoAtoms, possibleRingStereoBonds, res);
1243
0
      needAnotherRound |=
1244
0
          updateBonds(mol, aranks, bondSymbols, possibleAtoms, possibleBonds,
1245
0
                      knownAtoms, knownBonds, fixedBonds, res);
1246
0
    }
1247
0
  }
1248
1249
0
  boost::dynamic_bitset<> possibleSpecialCases(mol.getNumAtoms());
1250
0
  findChiralAtomSpecialCases(mol, possibleSpecialCases, aranks);
1251
1252
0
  for (const auto atom : mol.atoms()) {
1253
0
    atom->setProp<unsigned int>(common_properties::_ChiralAtomRank,
1254
0
                                aranks[atom->getIdx()], true);
1255
0
  }
1256
1257
0
  return res;
1258
0
}
1259
1260
//}  // namespace
1261
1262
std::vector<StereoInfo> findPotentialStereo(ROMol &mol, bool cleanIt,
1263
0
                                            bool findPossible) {
1264
0
  if (!mol.getRingInfo()->isSymmSssr()) {
1265
0
    MolOps::symmetrizeSSSR(mol);
1266
0
  }
1267
1268
0
  if (mol.needsUpdatePropertyCache()) {
1269
0
    mol.updatePropertyCache(false);
1270
0
  }
1271
0
  std::vector<StereoInfo> res = runCleanup(mol, findPossible, cleanIt);
1272
0
  mol.setProp("_potentialStereo", res, true);
1273
0
  return res;
1274
0
}
1275
1276
// const_casts are always ugly, but we know that findPotentialStereo()
1277
// doesn't modify the molecule if cleanIt is false:
1278
0
std::vector<StereoInfo> findPotentialStereo(const ROMol &mol) {
1279
0
  bool cleanIt = false;
1280
0
  return findPotentialStereo(const_cast<ROMol &>(mol), cleanIt);
1281
0
}
1282
1283
}  // namespace Chirality
1284
}  // namespace RDKit