Coverage Report

Created: 2026-09-28 07:21

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/rdkit/Code/GraphMol/RWMol.cpp
Line
Count
Source
1
//
2
//  Copyright (C) 2003-2024 Greg Landrum and other RDKit contributors
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
11
#include <RDGeneral/BoostStartInclude.h>
12
#include <boost/tokenizer.hpp>
13
#include <RDGeneral/BoostEndInclude.h>
14
15
// our stuff
16
#include <RDGeneral/Invariant.h>
17
#include <RDGeneral/RDLog.h>
18
#include "RWMol.h"
19
#include "Atom.h"
20
#include "Bond.h"
21
#include "BondIterators.h"
22
#include "RingInfo.h"
23
#include "SubstanceGroup.h"
24
25
namespace RDKit {
26
27
namespace {
28
void insertStereoGroups(RWMol &mol, const ROMol &other,
29
0
                        unsigned int origNumAtoms, unsigned int origNumBonds) {
30
0
  if (other.getStereoGroups().empty()) {
31
0
    return;
32
0
  }
33
0
  std::vector<RDKit::Atom *> abs_atoms;
34
0
  std::vector<RDKit::Bond *> abs_bonds;
35
0
  std::vector<RDKit::StereoGroup> new_groups;
36
0
  new_groups.reserve(mol.getStereoGroups().size());
37
0
  for (const auto &sg : mol.getStereoGroups()) {
38
    // The sdf specification forbids more than one ABS stereo group, but we
39
    // don't enforce that in our code. But if we see more than one ABS groups
40
    // here, just merge the atoms and bonds in them into one group. Other stereo
41
    // groups are just forwarded.
42
0
    if (sg.getGroupType() == RDKit::StereoGroupType::STEREO_ABSOLUTE) {
43
0
      auto &atoms = sg.getAtoms();
44
0
      auto &bonds = sg.getBonds();
45
0
      abs_atoms.insert(abs_atoms.end(), atoms.begin(), atoms.end());
46
0
      abs_bonds.insert(abs_bonds.end(), bonds.begin(), bonds.end());
47
0
    } else {
48
0
      new_groups.emplace_back(sg);
49
0
    }
50
0
  }
51
52
0
  for (const auto &sg : other.getStereoGroups()) {
53
    // update the stereo group's atom and bond indices
54
0
    std::vector<RDKit::Atom *> new_atoms;
55
0
    std::vector<RDKit::Bond *> new_bonds;
56
0
    for (auto atom : sg.getAtoms()) {
57
0
      auto idx = atom->getIdx() + origNumAtoms;
58
0
      new_atoms.push_back(mol.getAtomWithIdx(idx));
59
0
    }
60
0
    for (auto bond : sg.getBonds()) {
61
0
      auto idx = bond->getIdx() + origNumBonds;
62
0
      new_bonds.push_back(mol.getBondWithIdx(idx));
63
0
    }
64
65
    // Collect all ABS atoms and bonds so they are added as a single group
66
0
    if (sg.getGroupType() == RDKit::StereoGroupType::STEREO_ABSOLUTE) {
67
0
      abs_atoms.insert(abs_atoms.end(), new_atoms.begin(), new_atoms.end());
68
0
      abs_bonds.insert(abs_bonds.end(), new_bonds.begin(), new_bonds.end());
69
0
    } else {
70
0
      RDKit::StereoGroup new_group(sg.getGroupType(), new_atoms, new_bonds,
71
0
                                   sg.getReadId());
72
      // default write ID to 0 to avoid id clashes. We can use
73
      // assignStereoGroupIds() later on to assign new IDs
74
0
      new_group.setWriteId(0);
75
0
      new_groups.push_back(std::move(new_group));
76
0
    }
77
0
  }
78
0
  if (!abs_atoms.empty() || !abs_bonds.empty()) {
79
0
    new_groups.emplace_back(RDKit::StereoGroupType::STEREO_ABSOLUTE, abs_atoms,
80
0
                            abs_bonds);
81
0
  }
82
0
  mol.setStereoGroups(new_groups);
83
0
}
84
85
void insertSubstanceGroups(RWMol &mol, const RWMol &other,
86
                           unsigned int origNumAtoms,
87
0
                           unsigned int origNumBonds) {
88
0
  for (auto sgroup : getSubstanceGroups(other)) {
89
0
    sgroup.setOwningMol(&mol);
90
91
    // update the sgroup's atom and bond indices
92
0
    auto atom_indices = sgroup.getAtoms();
93
0
    std::transform(atom_indices.begin(), atom_indices.end(),
94
0
                   atom_indices.begin(),
95
0
                   [&origNumAtoms](unsigned int old_index) {
96
0
                     return origNumAtoms + old_index;
97
0
                   });
98
0
    sgroup.setAtoms(atom_indices);
99
100
0
    auto bond_indices = sgroup.getBonds();
101
0
    std::transform(bond_indices.begin(), bond_indices.end(),
102
0
                   bond_indices.begin(),
103
0
                   [&origNumBonds](unsigned int old_index) {
104
0
                     return origNumBonds + old_index;
105
0
                   });
106
0
    sgroup.setBonds(bond_indices);
107
108
    // patoms
109
0
    auto patom_indices = sgroup.getParentAtoms();
110
0
    std::transform(patom_indices.begin(), patom_indices.end(),
111
0
                   patom_indices.begin(),
112
0
                   [&origNumAtoms](unsigned int old_index) {
113
0
                     return origNumAtoms + old_index;
114
0
                   });
115
0
    sgroup.setParentAtoms(patom_indices);
116
117
    // cstates (these are references, can be updated in place)
118
0
    for (auto &cstate : sgroup.getCStates()) {
119
0
      cstate.bondIdx = origNumBonds + cstate.bondIdx;
120
0
    }
121
122
    // attachment points (can also be updated in place)
123
0
    for (auto &sap : sgroup.getAttachPoints()) {
124
0
      sap.aIdx = origNumAtoms + sap.aIdx;
125
0
      if (sap.lvIdx != -1) {
126
0
        sap.lvIdx = static_cast<int>(origNumAtoms + sap.lvIdx);
127
0
      }
128
0
    }
129
130
0
    addSubstanceGroup(mol, sgroup);
131
0
  }
132
0
}
133
}  // namespace
134
135
0
RWMol &RWMol::operator=(const RWMol &other) {
136
0
  if (this != &other) {
137
0
    this->clear();
138
0
    numBonds = 0;
139
0
    initFromOther(other, false, -1);
140
0
  }
141
0
  return *this;
142
0
}
143
144
0
void RWMol::insertMol(const ROMol &other) {
145
0
  auto origNumAtoms = getNumAtoms();
146
0
  auto origNumBonds = getNumBonds();
147
0
  for (const auto oatom : other.atoms()) {
148
0
    Atom *newAt = oatom->copy();
149
0
    const bool updateLabel = false;
150
0
    const bool takeOwnership = true;
151
0
    addAtom(newAt, updateLabel, takeOwnership);
152
    // take care of atom-numbering-dependent properties:
153
0
    if (INT_VECT nAtoms;
154
0
        newAt->getPropIfPresent(common_properties::_ringStereoAtoms, nAtoms)) {
155
0
      for (auto &val : nAtoms) {
156
0
        if (val < 0) {
157
0
          val -= rdcast<int>(origNumAtoms);
158
0
        } else {
159
0
          val += origNumAtoms;
160
0
        }
161
0
      }
162
0
      newAt->setProp(common_properties::_ringStereoAtoms, nAtoms, true);
163
0
    }
164
0
    if (unsigned int val;
165
0
        oatom->getPropIfPresent(common_properties::_ringStereoOtherAtom, val)) {
166
0
      newAt->setProp(common_properties::_ringStereoOtherAtom,
167
0
                     val + origNumAtoms, true);
168
0
    }
169
0
  }
170
171
0
  for (const auto obond : other.bonds()) {
172
0
    Bond *bond_p = obond->copy();
173
0
    unsigned int idx1, idx2;
174
0
    idx1 = bond_p->getBeginAtomIdx() + origNumAtoms;
175
0
    idx2 = bond_p->getEndAtomIdx() + origNumAtoms;
176
0
    bond_p->setOwningMol(this);
177
0
    bond_p->setBeginAtomIdx(idx1);
178
0
    bond_p->setEndAtomIdx(idx2);
179
0
    for (auto &v : bond_p->getStereoAtoms()) {
180
0
      v += origNumAtoms;
181
0
    }
182
0
    const bool takeOwnership = true;
183
0
    addBond(bond_p, takeOwnership);
184
0
  }
185
186
  // add atom to any conformers as well, if we have any
187
0
  if (other.getNumConformers() && !getNumConformers()) {
188
0
    for (const auto &oconf : other.d_confs) {
189
0
      auto *nconf = new Conformer(getNumAtoms());
190
0
      nconf->set3D(oconf->is3D());
191
0
      nconf->setId(oconf->getId());
192
0
      for (unsigned int i = 0; i < oconf->getNumAtoms(); ++i) {
193
0
        nconf->setAtomPos(i + origNumAtoms, oconf->getAtomPos(i));
194
0
      }
195
0
      const bool assignId = false;
196
0
      addConformer(nconf, assignId);
197
0
    }
198
0
  } else if (getNumConformers()) {
199
0
    if (other.getNumConformers() == getNumConformers()) {
200
0
      ConformerIterator cfi;
201
0
      ConstConformerIterator ocfi;
202
0
      for (cfi = beginConformers(), ocfi = other.beginConformers();
203
0
           cfi != endConformers(); ++cfi, ++ocfi) {
204
0
        for (unsigned int i = 0; i < (*ocfi)->getNumAtoms(); ++i) {
205
0
          (*cfi)->setAtomPos(i + origNumAtoms, (*ocfi)->getAtomPos(i));
206
0
        }
207
0
      }
208
0
    }
209
0
  }
210
211
  // add stereo groups
212
0
  insertStereoGroups(*this, other, origNumAtoms, origNumBonds);
213
  // add substance groups
214
0
  insertSubstanceGroups(*this, other, origNumAtoms, origNumBonds);
215
0
}
216
217
0
unsigned int RWMol::addAtom(bool updateLabel) {
218
0
  auto *atom_p = new Atom();
219
0
  atom_p->setOwningMol(this);
220
0
  auto which = boost::add_vertex(d_graph);
221
0
  d_graph[which] = atom_p;
222
0
  atom_p->setIdx(which);
223
0
  if (updateLabel) {
224
0
    clearAtomBookmark(ci_RIGHTMOST_ATOM);
225
0
    setAtomBookmark(atom_p, ci_RIGHTMOST_ATOM);
226
0
  }
227
228
  // add atom to any conformers as well, if we have any
229
0
  for (auto &conf : d_confs) {
230
0
    conf->setAtomPos(which, RDGeom::Point3D(0.0, 0.0, 0.0));
231
0
  }
232
0
  return rdcast<unsigned int>(which);
233
0
}
234
235
void RWMol::replaceAtom(unsigned int idx, Atom *atom_pin, bool,
236
15.8k
                        bool preserveProps) {
237
15.8k
  PRECONDITION(atom_pin, "bad atom passed to replaceAtom");
238
15.8k
  URANGE_CHECK(idx, getNumAtoms());
239
15.8k
  auto atom_p = atom_pin->copy();
240
15.8k
  atom_p->setOwningMol(this);
241
15.8k
  atom_p->setIdx(idx);
242
15.8k
  auto vd = boost::vertex(idx, d_graph);
243
15.8k
  if (preserveProps) {
244
6.74k
    const bool replaceExistingData = false;
245
6.74k
    atom_p->updateProps(*d_graph[vd], replaceExistingData);
246
6.74k
  }
247
248
15.8k
  const auto orig_p = d_graph[vd];
249
15.8k
  delete orig_p;
250
15.8k
  d_graph[vd] = atom_p;
251
252
  // handle bookmarks
253
15.8k
  for (auto &ab : d_atomBookmarks) {
254
119k
    for (auto &elem : ab.second) {
255
119k
      if (elem == orig_p) {
256
7.15k
        elem = atom_p;
257
7.15k
      }
258
119k
    }
259
15.5k
  }
260
261
  // handle stereo group
262
15.8k
  for (auto &group : d_stereo_groups) {
263
3.41k
    auto groupId = group.getReadId();
264
3.41k
    auto atoms = group.getAtoms();
265
3.41k
    auto bonds = group.getBonds();
266
3.41k
    auto aiter = std::find(atoms.begin(), atoms.end(), orig_p);
267
4.17k
    while (aiter != atoms.end()) {
268
752
      *aiter = atom_p;
269
752
      ++aiter;
270
752
      aiter = std::find(aiter, atoms.end(), orig_p);
271
752
    }
272
3.41k
    group = StereoGroup(group.getGroupType(), std::move(atoms),
273
3.41k
                        std::move(bonds), groupId);
274
3.41k
  }
275
15.8k
};
276
277
void RWMol::replaceBond(unsigned int idx, Bond *bond_pin, bool preserveProps,
278
4
                        bool keepSGroups) {
279
4
  PRECONDITION(bond_pin, "bad bond passed to replaceBond");
280
4
  URANGE_CHECK(idx, getNumBonds());
281
4
  auto bIter = getEdges();
282
10
  for (unsigned int i = 0; i < idx; i++) {
283
6
    ++bIter.first;
284
6
  }
285
4
  const auto *obond = d_graph[*(bIter.first)];
286
4
  auto *bond_p = bond_pin->copy();
287
4
  bond_p->setOwningMol(this);
288
4
  bond_p->setIdx(idx);
289
4
  bond_p->setBeginAtomIdx(obond->getBeginAtomIdx());
290
4
  bond_p->setEndAtomIdx(obond->getEndAtomIdx());
291
292
  // Update explicit Hs, if set, on both ends. This was github #7128
293
4
  auto orderDifference =
294
4
      bond_p->getBondTypeAsDouble() - obond->getBondTypeAsDouble();
295
4
  if (orderDifference > 0) {
296
8
    for (auto atom : {bond_p->getBeginAtom(), bond_p->getEndAtom()}) {
297
8
      if (auto explicit_hs = atom->getNumExplicitHs(); explicit_hs > 0) {
298
2
        auto new_hs = static_cast<int>(explicit_hs - orderDifference);
299
2
        atom->setNumExplicitHs(std::max(new_hs, 0));
300
2
      }
301
8
    }
302
4
  }
303
304
4
  if (preserveProps) {
305
4
    const bool replaceExistingData = false;
306
4
    bond_p->updateProps(*d_graph[*(bIter.first)], replaceExistingData);
307
4
  }
308
309
4
  const auto orig_p = d_graph[*(bIter.first)];
310
4
  delete orig_p;
311
4
  d_graph[*(bIter.first)] = bond_p;
312
313
4
  if (!keepSGroups) {
314
0
    removeSubstanceGroupsReferencingBond(*this, idx);
315
0
  }
316
317
  // handle bookmarks
318
4
  for (auto &ab : d_bondBookmarks) {
319
0
    for (auto &elem : ab.second) {
320
0
      if (elem == orig_p) {
321
0
        elem = bond_p;
322
0
      }
323
0
    }
324
0
  }
325
4
};
326
327
5.75M
Atom *RWMol::getActiveAtom() {
328
5.75M
  if (hasAtomBookmark(ci_RIGHTMOST_ATOM)) {
329
5.75M
    return getAtomWithBookmark(ci_RIGHTMOST_ATOM);
330
5.75M
  } else {
331
0
    return getLastAtom();
332
0
  }
333
5.75M
};
334
335
218k
void RWMol::setActiveAtom(Atom *at) {
336
218k
  PRECONDITION(at, "NULL atom provided");
337
218k
  clearAtomBookmark(ci_RIGHTMOST_ATOM);
338
218k
  setAtomBookmark(at, ci_RIGHTMOST_ATOM);
339
218k
};
340
218k
void RWMol::setActiveAtom(unsigned int idx) {
341
218k
  setActiveAtom(getAtomWithIdx(idx));
342
218k
};
343
344
0
void RWMol::removeAtom(unsigned int idx) { removeAtom(getAtomWithIdx(idx)); }
345
346
1.76k
void RWMol::removeAtom(Atom *atom, bool clearProps) {
347
1.76k
  PRECONDITION(atom, "NULL atom provided");
348
1.76k
  PRECONDITION(static_cast<RWMol *>(&atom->getOwningMol()) == this,
349
1.76k
               "atom not owned by this molecule");
350
1.76k
  unsigned int idx = atom->getIdx();
351
352
  // remove bonds attached to the atom
353
  //  In batch mode this will schedule bond removal
354
1.76k
  std::vector<std::pair<unsigned int, unsigned int>> nbrs;
355
1.76k
  ADJ_ITER b1, b2;
356
1.76k
  boost::tie(b1, b2) = getAtomNeighbors(atom);
357
3.52k
  while (b1 != b2) {
358
1.76k
    nbrs.emplace_back(atom->getIdx(), rdcast<unsigned int>(*b1));
359
1.76k
    ++b1;
360
1.76k
  }
361
1.76k
  for (auto &nbr : nbrs) {
362
1.76k
    removeBond(nbr.first, nbr.second);
363
1.76k
  }
364
365
1.76k
  if (dp_delAtoms) {
366
    // we're in a batch edit
367
    // if atoms have been added since we started, resize dp_delAtoms
368
0
    if (dp_delAtoms->size() < getNumAtoms()) {
369
0
      dp_delAtoms->resize(getNumAtoms());
370
0
    }
371
0
    dp_delAtoms->set(idx);
372
0
    return;
373
0
  }
374
375
  // remove any bookmarks which point to this atom:
376
1.76k
  ATOM_BOOKMARK_MAP *marks = getAtomBookmarks();
377
1.76k
  auto markI = marks->begin();
378
1.76k
  while (markI != marks->end()) {
379
0
    const ATOM_PTR_LIST &atoms = markI->second;
380
    // we need to copy the iterator then increment it, because the
381
    // deletion we're going to do in clearAtomBookmark will invalidate
382
    // it.
383
0
    auto tmpI = markI;
384
0
    ++markI;
385
0
    if (std::find(atoms.begin(), atoms.end(), atom) != atoms.end()) {
386
0
      clearAtomBookmark(tmpI->first, atom);
387
0
    }
388
0
  }
389
390
  // loop over all atoms with higher indices and update their indices
391
2.80M
  for (unsigned int i = idx + 1; i < getNumAtoms(); i++) {
392
2.80M
    Atom *higher_index_atom = getAtomWithIdx(i);
393
2.80M
    higher_index_atom->setIdx(i - 1);
394
2.80M
  }
395
396
  // do the same with the coordinates in the conformations
397
1.76k
  for (auto conf : d_confs) {
398
1.38k
    RDGeom::POINT3D_VECT &positions = conf->getPositions();
399
1.38k
    auto pi = positions.begin();
400
16.2k
    for (unsigned int i = 0; i < getNumAtoms() - 1; i++) {
401
14.8k
      ++pi;
402
14.8k
      if (i >= idx) {
403
6.21k
        positions[i] = positions[i + 1];
404
6.21k
      }
405
14.8k
    }
406
1.38k
    positions.erase(pi);
407
1.38k
  }
408
  // now deal with bonds:
409
  //   their end indices may need to be decremented and their
410
  //   indices will need to be handled and if they have an
411
  //   ENDPTS prop that includes idx, it will need updating.
412
1.76k
  unsigned int nBonds = 0;
413
1.76k
  EDGE_ITER beg, end;
414
1.76k
  boost::tie(beg, end) = getEdges();
415
1.76k
  std::string sprop;
416
7.29M
  while (beg != end) {
417
7.29M
    Bond *bond = d_graph[*beg++];
418
7.29M
    if (bond->getPropIfPresent(RDKit::common_properties::_MolFileBondEndPts,
419
7.29M
                               sprop)) {
420
      // This would ideally use ParseV3000Array but I'm buggered if I can get
421
      // the linker to find it.
422
      //      std::vector<unsigned int> oats =
423
      //          RDKit::SGroupParsing::ParseV3000Array<unsigned int>(sprop);
424
0
      if ('(' == sprop.front() && ')' == sprop.back()) {
425
0
        sprop = sprop.substr(1, sprop.length() - 2);
426
427
        // This is doing what ParseV3000Array would do.
428
0
        boost::char_separator<char> sep(" ");
429
0
        boost::tokenizer<boost::char_separator<char>> tokens(sprop, sep);
430
0
        unsigned int num_ats = std::stod(*tokens.begin());
431
0
        std::vector<unsigned int> oats;
432
0
        auto beg = tokens.begin();
433
0
        ++beg;
434
0
        std::transform(beg, tokens.end(), std::back_inserter(oats),
435
0
                       [](const std::string &a) { return std::stod(a); });
436
437
0
        auto idx_pos = std::find(oats.begin(), oats.end(), idx + 1);
438
0
        if (idx_pos != oats.end()) {
439
0
          oats.erase(idx_pos);
440
0
          --num_ats;
441
0
        }
442
0
        if (!num_ats) {
443
0
          bond->clearProp(RDKit::common_properties::_MolFileBondEndPts);
444
0
          bond->clearProp(common_properties::_MolFileBondAttach);
445
0
        } else {
446
0
          sprop = "(" + std::to_string(num_ats) + " ";
447
0
          for (auto &i : oats) {
448
0
            if (i > idx + 1) {
449
0
              --i;
450
0
            }
451
0
            sprop += std::to_string(i) + " ";
452
0
          }
453
0
          sprop[sprop.length() - 1] = ')';
454
0
          bond->setProp(RDKit::common_properties::_MolFileBondEndPts, sprop);
455
0
        }
456
0
      }
457
0
    }
458
7.29M
    unsigned int tmpIdx = bond->getBeginAtomIdx();
459
7.29M
    if (tmpIdx > idx) {
460
2.26M
      bond->setBeginAtomIdx(tmpIdx - 1);
461
2.26M
    }
462
7.29M
    tmpIdx = bond->getEndAtomIdx();
463
7.29M
    if (tmpIdx > idx) {
464
2.35M
      bond->setEndAtomIdx(tmpIdx - 1);
465
2.35M
    }
466
7.29M
    bond->setIdx(nBonds++);
467
7.29M
    for (auto bsi = bond->getStereoAtoms().begin();
468
7.29M
         bsi != bond->getStereoAtoms().end(); ++bsi) {
469
296
      if ((*bsi) == rdcast<int>(idx)) {
470
0
        bond->getStereoAtoms().clear();
471
0
        break;
472
296
      } else if ((*bsi) > rdcast<int>(idx)) {
473
127
        --(*bsi);
474
127
      }
475
296
    }
476
7.29M
  }
477
478
1.76k
  removeSubstanceGroupsReferencingAtom(*this, idx);
479
480
  // Remove this atom from any stereo group
481
1.76k
  removeAtomFromGroups(atom, d_stereo_groups);
482
483
  // clear computed properties and reset our ring info structure
484
  // they are pretty likely to be wrong now:
485
1.76k
  if (clearProps) {
486
0
    clearComputedProps(true);
487
0
  }
488
489
1.76k
  atom->setOwningMol(nullptr);
490
491
  // remove all connections to the atom:
492
1.76k
  MolGraph::vertex_descriptor vd = boost::vertex(idx, d_graph);
493
1.76k
  boost::clear_vertex(vd, d_graph);
494
  // finally remove the vertex itself
495
1.76k
  boost::remove_vertex(vd, d_graph);
496
1.76k
  delete atom;
497
1.76k
}
498
499
0
void RWMol::removeAtom(Atom *atom) { removeAtom(atom, true); }
500
501
unsigned int RWMol::addBond(unsigned int atomIdx1, unsigned int atomIdx2,
502
2.65M
                            Bond::BondType bondType) {
503
  // if the atom indices are bad, the next two calls will catch that.
504
2.65M
  auto beginAtom = getAtomWithIdx(atomIdx1);
505
2.65M
  auto endAtom = getAtomWithIdx(atomIdx2);
506
2.65M
  PRECONDITION(atomIdx1 != atomIdx2, "attempt to add self-bond");
507
2.65M
  PRECONDITION(!(boost::edge(atomIdx1, atomIdx2, d_graph).second),
508
2.65M
               "bond already exists");
509
510
2.65M
  auto *b = new Bond(bondType);
511
2.65M
  b->setOwningMol(this);
512
2.65M
  if (bondType == Bond::AROMATIC) {
513
403k
    b->setIsAromatic(1);
514
    //
515
    // assume that aromatic bonds connect aromatic atoms
516
    //   This is relevant for file formats like MOL, where there
517
    //   is no such thing as an aromatic atom, but bonds can be
518
    //   marked aromatic.
519
    //
520
403k
    beginAtom->setIsAromatic(1);
521
403k
    endAtom->setIsAromatic(1);
522
403k
  }
523
2.65M
  auto [which, ok] = boost::add_edge(atomIdx1, atomIdx2, d_graph);
524
2.65M
  d_graph[which] = b;
525
2.65M
  ++numBonds;
526
2.65M
  b->setIdx(numBonds - 1);
527
2.65M
  b->setBeginAtomIdx(atomIdx1);
528
2.65M
  b->setEndAtomIdx(atomIdx2);
529
530
  // the valence values on the begin and end atoms need to be updated:
531
2.65M
  beginAtom->clearPropertyCache();
532
2.65M
  endAtom->clearPropertyCache();
533
534
  // we're in a batch edit, and at least one of the bond ends is scheduled
535
  // for deletion, so mark the new bond for deletion too:
536
2.65M
  if (dp_delAtoms &&
537
0
      ((atomIdx1 < dp_delAtoms->size() && dp_delAtoms->test(atomIdx1)) ||
538
0
       (atomIdx2 < dp_delAtoms->size() && dp_delAtoms->test(atomIdx2)))) {
539
0
    if (dp_delBonds->size() < numBonds) {
540
0
      dp_delBonds->resize(numBonds);
541
0
    }
542
0
    dp_delBonds->set(numBonds - 1);
543
0
  }
544
545
2.65M
  return numBonds;
546
2.65M
}
547
548
0
unsigned int RWMol::addBond(Atom *atom1, Atom *atom2, Bond::BondType bondType) {
549
0
  PRECONDITION(atom1 && atom2, "NULL atom passed in");
550
0
  return addBond(atom1->getIdx(), atom2->getIdx(), bondType);
551
0
}
552
553
1.76k
void RWMol::removeBond(unsigned int aid1, unsigned int aid2) {
554
1.76k
  URANGE_CHECK(aid1, getNumAtoms());
555
1.76k
  URANGE_CHECK(aid2, getNumAtoms());
556
1.76k
  auto *bnd = getBondBetweenAtoms(aid1, aid2);
557
1.76k
  if (!bnd) {
558
0
    return;
559
0
  }
560
1.76k
  auto idx = bnd->getIdx();
561
1.76k
  if (dp_delBonds) {
562
    // we're in a batch edit
563
    // if bonds have been added since we started, resize dp_delBonds
564
0
    if (dp_delBonds->size() < getNumBonds()) {
565
0
      dp_delBonds->resize(getNumBonds());
566
0
    }
567
0
    dp_delBonds->set(idx);
568
0
    return;
569
0
  }
570
571
  // remove any bookmarks which point to this bond:
572
1.76k
  BOND_BOOKMARK_MAP *marks = getBondBookmarks();
573
1.76k
  auto markI = marks->begin();
574
1.76k
  while (markI != marks->end()) {
575
0
    BOND_PTR_LIST &bonds = markI->second;
576
    // we need to copy the iterator then increment it, because the
577
    // deletion we're going to do in clearBondBookmark will invalidate
578
    // it.
579
0
    auto tmpI = markI;
580
0
    ++markI;
581
0
    if (std::find(bonds.begin(), bonds.end(), bnd) != bonds.end()) {
582
0
      clearBondBookmark(tmpI->first, bnd);
583
0
    }
584
0
  }
585
586
  // loop over neighboring double bonds and remove their stereo atom
587
  //  information. This is definitely now invalid (was github issue 8)
588
1.76k
  auto beginAtm = bnd->getBeginAtom();
589
1.76k
  auto endAtm = bnd->getEndAtom();
590
1.76k
  std::vector<std::vector<Atom *>> bond_atoms = {{beginAtm, endAtm},
591
1.76k
                                                 {endAtm, beginAtm}};
592
3.52k
  for (const auto &atoms : bond_atoms) {
593
17.0k
    for (auto obnd : this->atomBonds(atoms[0])) {
594
17.0k
      if (obnd == bnd) {
595
3.52k
        continue;
596
3.52k
      }
597
13.5k
      if (std::find(obnd->getStereoAtoms().begin(),
598
13.5k
                    obnd->getStereoAtoms().end(),
599
13.5k
                    atoms[1]->getIdx()) != obnd->getStereoAtoms().end()) {
600
        // github #6900 if we remove stereo atoms we need to remove
601
        //  the CIS and or TRANS since this requires stereo atoms
602
24
        if (obnd->getStereo() == Bond::BondStereo::STEREOCIS ||
603
24
            obnd->getStereo() == Bond::BondStereo::STEREOTRANS) {
604
0
          obnd->setStereo(Bond::BondStereo::STEREONONE);
605
0
        }
606
607
24
        obnd->getStereoAtoms().clear();
608
24
      }
609
13.5k
    }
610
3.52k
  }
611
  // reset our ring info structure, because it is pretty likely
612
  // to be wrong now:
613
1.76k
  dp_ringInfo->reset();
614
615
1.76k
  removeSubstanceGroupsReferencingBond(*this, idx);
616
1.76k
  removeBondFromGroups(bnd, d_stereo_groups);
617
618
  // loop over all bonds with higher indices and update their indices
619
7.29M
  for (auto bond : bonds()) {
620
7.29M
    if (bond->getIdx() > idx) {
621
2.35M
      bond->setIdx(bond->getIdx() - 1);
622
2.35M
    }
623
7.29M
  }
624
625
1.76k
  auto vd1 = boost::vertex(bnd->getBeginAtomIdx(), d_graph);
626
1.76k
  auto vd2 = boost::vertex(bnd->getEndAtomIdx(), d_graph);
627
1.76k
  boost::remove_edge(vd1, vd2, d_graph);
628
1.76k
  delete bnd;
629
1.76k
  --numBonds;
630
1.76k
}
631
632
93.2k
Bond *RWMol::createPartialBond(unsigned int atomIdx1, Bond::BondType bondType) {
633
93.2k
  URANGE_CHECK(atomIdx1, getNumAtoms());
634
635
93.2k
  auto *b = new Bond(bondType);
636
93.2k
  b->setOwningMol(this);
637
93.2k
  b->setBeginAtomIdx(atomIdx1);
638
639
93.2k
  return b;
640
93.2k
}
641
642
unsigned int RWMol::finishPartialBond(unsigned int atomIdx2, int bondBookmark,
643
0
                                      Bond::BondType bondType) {
644
0
  PRECONDITION(hasBondBookmark(bondBookmark), "no such partial bond");
645
0
  URANGE_CHECK(atomIdx2, getNumAtoms());
646
647
0
  auto *bsp = getBondWithBookmark(bondBookmark);
648
0
  if (bondType == Bond::UNSPECIFIED) {
649
0
    bondType = bsp->getBondType();
650
0
  }
651
652
0
  return addBond(bsp->getBeginAtomIdx(), atomIdx2, bondType);
653
0
}
654
655
0
void RWMol::beginBatchEdit() {
656
0
  if (dp_delAtoms || dp_delBonds) {
657
0
    throw ValueErrorException("Attempt to re-enter batchEdit mode");
658
0
  }
659
0
  dp_delAtoms.reset(new boost::dynamic_bitset<>(getNumAtoms()));
660
0
  dp_delBonds.reset(new boost::dynamic_bitset<>(getNumBonds()));
661
0
}
662
663
0
void RWMol::commitBatchEdit() {
664
0
  if (!(dp_delBonds || dp_delAtoms)) {
665
0
    return;
666
0
  } else if (dp_delBonds->none() && dp_delAtoms->none()) {
667
    // no need to reset ring info & calculated properties,
668
    // since nothing gets removed
669
0
    dp_delBonds.reset();
670
0
    dp_delAtoms.reset();
671
0
    return;
672
0
  }
673
674
0
  batchRemoveBonds();
675
0
  batchRemoveAtoms();
676
677
  // remove ring info
678
0
  dp_ringInfo->reset();
679
680
  // fix properties
681
0
  clearComputedProps(true);
682
0
  dp_delBonds.reset();
683
0
  dp_delAtoms.reset();
684
0
}
685
686
0
void RWMol::batchRemoveBonds() {
687
0
  if (!dp_delBonds || dp_delBonds->none()) {
688
0
    return;
689
0
  }
690
691
0
  auto &delBonds = *dp_delBonds;
692
0
  unsigned int min_idx = getNumBonds();
693
0
  for (unsigned int i = rdcast<unsigned int>(delBonds.size()); i > 0; --i) {
694
0
    if (!delBonds[i - 1]) {
695
0
      continue;
696
0
    }
697
0
    unsigned int idx = rdcast<unsigned int>(i - 1);
698
0
    Bond *bnd = getBondWithIdx(idx);
699
0
    if (!bnd) {
700
0
      continue;
701
0
    }
702
703
0
    min_idx = idx;
704
    // remove any bookmarks which point to this bond:
705
0
    BOND_BOOKMARK_MAP *marks = getBondBookmarks();
706
0
    auto markI = marks->begin();
707
0
    while (markI != marks->end()) {
708
0
      BOND_PTR_LIST &bonds = markI->second;
709
      // we need to copy the iterator then increment it, because the
710
      // deletion we're going to do in clearBondBookmark will invalidate
711
      // it.
712
0
      auto tmpI = markI;
713
0
      ++markI;
714
0
      if (std::find(bonds.begin(), bonds.end(), bnd) != bonds.end()) {
715
0
        clearBondBookmark(tmpI->first, bnd);
716
0
      }
717
0
    }
718
719
    // loop over neighboring double bonds and remove their stereo atom
720
    //  information. This is definitely now invalid (was github issue 8)
721
0
    auto beginAtm = bnd->getBeginAtom();
722
0
    auto endAtm = bnd->getEndAtom();
723
0
    std::vector<std::vector<Atom *>> bond_atoms = {{beginAtm, endAtm},
724
0
                                                   {endAtm, beginAtm}};
725
0
    for (const auto &atoms : bond_atoms) {
726
0
      for (auto obnd : atomBonds(atoms[0])) {
727
0
        if (obnd == bnd) {
728
0
          continue;
729
0
        }
730
0
        if (std::find(obnd->getStereoAtoms().begin(),
731
0
                      obnd->getStereoAtoms().end(),
732
0
                      atoms[1]->getIdx()) != obnd->getStereoAtoms().end()) {
733
          // github #6900 if we remove stereo atoms we need to remove
734
          //  the CIS and or TRANS since this requires stereo atoms
735
0
          if (obnd->getStereo() == Bond::BondStereo::STEREOCIS ||
736
0
              obnd->getStereo() == Bond::BondStereo::STEREOTRANS) {
737
0
            obnd->setStereo(Bond::BondStereo::STEREONONE);
738
0
          }
739
0
          obnd->getStereoAtoms().clear();
740
0
        }
741
0
      }
742
0
    }
743
744
0
    removeSubstanceGroupsReferencingBond(*this, idx);
745
    // Remove this bond from any stereo group
746
0
    removeBondFromGroups(bnd, d_stereo_groups);
747
748
0
    bnd->setOwningMol(nullptr);
749
750
0
    auto vd1 = boost::vertex(bnd->getBeginAtomIdx(), d_graph);
751
0
    auto vd2 = boost::vertex(bnd->getEndAtomIdx(), d_graph);
752
0
    boost::remove_edge(vd1, vd2, d_graph);
753
0
    delete bnd;
754
0
    --numBonds;
755
0
  }
756
757
  // loop over all bonds with higher indices than the minimum modified and
758
  // update their indices
759
0
  auto [firstB, lastB] = this->getEdges();
760
0
  unsigned int next_idx = min_idx;
761
0
  while (firstB != lastB) {
762
0
    Bond *bond = (*this)[*firstB];
763
0
    if (bond->getIdx() > min_idx) {
764
0
      bond->setIdx(next_idx++);
765
0
    }
766
0
    ++firstB;
767
0
  }
768
0
}
769
770
0
void RWMol::batchRemoveAtoms() {
771
0
  if (!dp_delAtoms || dp_delAtoms->none()) {
772
0
    return;
773
0
  }
774
0
  std::vector<Atom *> oldIndices(getNumAtoms());
775
0
  for (auto *atom : atoms()) {
776
0
    oldIndices[atom->getIdx()] = atom;
777
0
  }
778
779
0
  auto &delAtoms = *dp_delAtoms;
780
0
  for (unsigned int i = rdcast<unsigned int>(delAtoms.size()); i > 0; --i) {
781
0
    if (!delAtoms[i - 1]) {
782
0
      continue;
783
0
    }
784
0
    unsigned int idx = i - 1;
785
0
    Atom *atom = getAtomWithIdx(idx);
786
0
    if (!atom) {
787
0
      continue;
788
0
    }
789
790
    // remove any bookmarks which point to this atom:
791
0
    ATOM_BOOKMARK_MAP *marks = getAtomBookmarks();
792
0
    auto markI = marks->begin();
793
0
    while (markI != marks->end()) {
794
0
      const ATOM_PTR_LIST &atoms = markI->second;
795
      // we need to copy the iterator then increment it, because the
796
      // deletion we're going to do in clearAtomBookmark will invalidate
797
      // it.
798
0
      auto tmpI = markI;
799
0
      ++markI;
800
0
      if (std::find(atoms.begin(), atoms.end(), atom) != atoms.end()) {
801
0
        clearAtomBookmark(tmpI->first, atom);
802
0
      }
803
0
    }
804
805
    // now deal with bonds:
806
    //   their end indices may need to be decremented and their
807
    //   indices will need to be handled and if they have an
808
    //   ENDPTS prop that includes idx, it will need updating.
809
0
    EDGE_ITER beg, end;
810
0
    boost::tie(beg, end) = getEdges();
811
0
    std::string sprop;
812
0
    while (beg != end) {
813
0
      Bond *bond = d_graph[*beg++];
814
0
      if (bond->getPropIfPresent(RDKit::common_properties::_MolFileBondEndPts,
815
0
                                 sprop)) {
816
        // This would ideally use ParseV3000Array but I'm buggered if I can
817
        // get the linker to find it.
818
        //      std::vector<unsigned int> oats =
819
        //          RDKit::SGroupParsing::ParseV3000Array<unsigned
820
        //          int>(sprop);
821
0
        if ('(' == sprop.front() && ')' == sprop.back()) {
822
0
          sprop = sprop.substr(1, sprop.length() - 2);
823
824
          // This is doing what ParseV3000Array would do.
825
0
          boost::char_separator<char> sep(" ");
826
0
          boost::tokenizer<boost::char_separator<char>> tokens(sprop, sep);
827
0
          unsigned int num_ats = std::stod(*tokens.begin());
828
0
          std::vector<unsigned int> oats;
829
0
          auto beg = tokens.begin();
830
0
          ++beg;
831
0
          std::transform(beg, tokens.end(), std::back_inserter(oats),
832
0
                         [](const std::string &a) { return std::stod(a); });
833
834
0
          auto idx_pos = std::find(oats.begin(), oats.end(), idx + 1);
835
0
          if (idx_pos != oats.end()) {
836
0
            oats.erase(idx_pos);
837
0
            --num_ats;
838
0
          }
839
0
          if (!num_ats) {
840
0
            bond->clearProp(RDKit::common_properties::_MolFileBondEndPts);
841
0
            bond->clearProp(common_properties::_MolFileBondAttach);
842
0
          } else {
843
0
            sprop = "(" + std::to_string(num_ats) + " ";
844
0
            for (auto &i : oats) {
845
0
              if (i > idx + 1) {
846
0
                --i;
847
0
              }
848
0
              sprop += std::to_string(i) + " ";
849
0
            }
850
0
            sprop[sprop.length() - 1] = ')';
851
0
            bond->setProp(RDKit::common_properties::_MolFileBondEndPts, sprop);
852
0
          }
853
0
        }
854
0
      }
855
0
    }
856
857
0
    removeSubstanceGroupsReferencingAtom(*this, idx);
858
859
    // Remove this atom from any stereo group
860
0
    removeAtomFromGroups(atom, d_stereo_groups);
861
0
    atom->setOwningMol(nullptr);
862
863
    // remove all connections to the atom:
864
0
    MolGraph::vertex_descriptor vd = boost::vertex(idx, d_graph);
865
0
    boost::clear_vertex(vd, d_graph);
866
    // finally remove the vertex itself
867
0
    boost::remove_vertex(vd, d_graph);
868
0
    delete atom;
869
0
    oldIndices[idx] = nullptr;
870
0
  }
871
872
  // reassign atom indices
873
0
  for (unsigned int i = 0; i < getNumAtoms(); i++) {
874
0
    Atom *atm = getAtomWithIdx(i);
875
0
    atm->setIdx(i);
876
0
  }
877
878
  // reassign atom indices in bonds
879
0
  for (auto bond : bonds()) {
880
0
    auto bgnidx = bond->getBeginAtomIdx();
881
0
    auto endidx = bond->getEndAtomIdx();
882
0
    Atom *bgn = oldIndices[bgnidx];
883
0
    Atom *end = oldIndices[endidx];
884
0
    CHECK_INVARIANT(bgn, "Atom mapping failed");
885
0
    CHECK_INVARIANT(end, "Atom mapping failed");
886
0
    bond->setBeginAtomIdx(bgn->getIdx());
887
0
    bond->setEndAtomIdx(end->getIdx());
888
0
    INT_VECT stereoAtoms;
889
0
    INT_VECT &oldStereoAtoms = bond->getStereoAtoms();
890
0
    if (oldStereoAtoms.size()) {
891
0
      for (auto &idx : oldStereoAtoms) {
892
0
        if (oldIndices[idx]) {
893
0
          stereoAtoms.push_back(oldIndices[idx]->getIdx());
894
0
        }
895
0
      }
896
0
      bond->getStereoAtoms().swap(stereoAtoms);
897
0
    }
898
0
  }
899
900
  // do the same with the coordinates in the conformations
901
0
  for (auto conf : d_confs) {
902
0
    RDGeom::POINT3D_VECT &positions = conf->getPositions();
903
0
    RDGeom::POINT3D_VECT newPositions;
904
0
    newPositions.reserve(getNumAtoms());
905
906
0
    for (RDGeom::POINT3D_VECT::size_type i = 0; i < positions.size(); ++i) {
907
0
      if (oldIndices[i] != nullptr) {
908
0
        newPositions.push_back(positions[i]);
909
0
      }
910
0
    }
911
0
    CHECK_INVARIANT(newPositions.size() == getNumAtoms(), "Lost coordinates!");
912
0
    positions.swap(newPositions);
913
0
  }
914
0
}
915
916
}  // namespace RDKit